잊지 않겠습니다.

'2014/01'에 해당되는 글 2건

  1. 2014.01.24 AspectJ + Spring을 이용한 AOP Programming
  2. 2014.01.08 Spring Security + RemoteAuthentication Provider 3

aspectJ기술은 AOP를 보다 쉽고 강력하게 적용하기 위한 기술입니다. aspectJ는 다음과 같은 특징을 가지고 있습니다.

  • compile된 bytecode에 대한 re-compile을 통한 새로운 bytecode의 작성으로 인한 AOP가 갖는 성능 저하의 최소
  • pointcut, advisor의 직접적인 선언을 이용한 보기 쉬운 코드의 작성

먼저, AspectJ를 실행하기 위해서는 다음 3개의 maven repository의 등록이 필요합니다.

compile 'org.aspectj:aspectjrt:1.7.4'
compile 'org.aspectj:aspectjtools:1.7.4'
compile 'org.aspectj:aspectjweaver:1.7.4'

aspectJ는 중요한 3개의 단어 정의가 먼저 필요합니다.

  1. Pointcut : AOP가 적용되는 시점을 정의합니다. 정의 방법은 @annotation, expression, method, beanName 등의 방법이 있습니다.
  2. Advice : Pointcut과 연동되어 AOP가 적용될 method를 의미합니다. 전/후 처리 method를 의미합니다.
  3. Advisor : Point + Advice를 정의된 class를 의미합니다. Spring에서 aspectJ에 대한 지원을 하기 위한 방법으로 사용됩니다.

예시로 가장 간단히 사용할 수 있는 @annotation을 이용한 AOP에 대한 예제에 대해서 알아보도록 하겠습니다.

먼저 AOP target method를 지정할 @interface를 생성합니다.

public @interface ToLowerOutput {
}

생성된 interface를 지정하는 Pointcut과 Advice를 포함한 Advisor class를 생성합니다.

@Aspect
@Component
public class HelloAdvisor {
    @Pointcut(value = "@annotation(me.xyzlast.bookstore.aop.ToLowerOutput)")
    private void convertToLowercast() {

    }

    @Around(value = "convertToLowercast()")
    public Object convertResultStringToLower(ProceedingJoinPoint pjp) throws Throwable {
        return pjp.proceed().toString().toLowerCase();
    }
}

만들어진 @annotation을 class에 적용합니다.

@Component
public class HelloImpl implements Hello {
    @ToLowerOutput
    @Override
    public String sayHello(String name) {
        return "Hello " + name + "!";
    }

    @ToLowerOutput
    @Override
    public String sayHi(String name) {
        return "Hi! " + name + "!";
    }

    @ToLowerOutput
    @Override
    public String sayThankYou(String name) {
        return "Thank you, " + name + "!";
    }
}

Spring configuration을 다음과 같이 수정합니다. 주의할점은 반드시 annotation auto proxy 설정이 bean 정의보다 먼저 위치해야지 됩니다!

    <aop:aspectj-autoproxy/>
    <bean name="helloImpl" class="me.xyzlast.bookstore.aop.HelloImpl"/>
    <bean name="aspectJAOP" class="me.xyzlast.bookstore.aop.HelloAdvisor"/>

이제 test code를 이용하면 모든 값들이 다 소문자로 나오게 되는 것을 볼 수 있습니다.

Posted by Y2K
,

개발시, 외부 SOAP 서비스 또는 REST 서비스를 이용해서 인증을 처리해줘야지 되는 경우가 자주 발생하게 됩니다. 특히, 기존의 인증 시스템을 가지고 있는 상태에서 신규 개발은 기존의 인증시스템을 그대로 사용해야지 될 때, 이런 경우가 자주 생기게 됩니다.

일단. 이와 같은 상황에서는 가장 널리 사용되고 있는 DaoAuthentication을 사용하지 못합니다. Application의 Database에는 사용자의 정보를 전혀 가지지 못하게 되니까요. 이때, 인증 process만을 외부에서 사용하고, 인증된 Process를 거친 후, Spring Security FIlter Chain을 통과하게 된다면 가장 좋은 방법이 될 것 같습니다.

Spring Security 인증 구성

Spring Security에서는 여러가지 인증방법을 제공하는데, 이를 지원하기 위해서는 AuthenticationManagerBuilder에 AuthenticationProvider를 구현한 객체를 설정해줘야지 됩니다.

Spring Security에서 일반적으로 인증을 구성하는 방식은 다음과 같습니다.

  1. AuthenticationProvider 구성
  2. AuthenticationManager 구성 후, Provider에 Bean Setting
  3. AuthenticationManagerBuilder에 설정된 AuthenticationProvider 설정

가장 기본적이라고 할 수 있는 DaoAuthenticationProvider를 이용한 설정은 다음과 같습니다.

@Bean
public DaoAuthenticationProvider daoAuthenticationProvider() throws Exception {
    DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
    daoAuthenticationProvider.setUserDetailsService(userDetailsServiceBean());
    daoAuthenticationProvider.setPasswordEncoder(passwordEncoder());
    return daoAuthenticationProvider;
}

@Override
@Bean(name = BeanIds.USER_DETAILS_SERVICE)
public UserDetailsService userDetailsServiceBean() throws Exception {
    CommonAppUserDetailService userDetailsService = new CommonAppUserDetailService();
    userDetailsService.setUserService(context.getBean(UserService.class));
    return userDetailsService;
}

RemoteAuthenticationProvider 의 구성

DaoAuthenticationProvider의 구성과 동일하게 코드는 다음과 같이 구성될 수 있습니다.

@Bean
public RemoteAuthenticationProvider remoteAuthenticationProvider() throws Exception {
    RemoteAuthenticationProvider provider = new RemoteAuthenticationProvider();
    provider.setRemoteAuthenticationManager(remoteAuthenticationManager());
    return provider;
}

여기에서 중요한 것이, setRemoteAuthenticationManager method입니다. 이제 외부에서 인증을 하는 Client code가 구성되는 곳이 RemoteAuthenticationManager이기 때문입니다.

RemoteAuthenticationManager interface는 다음과 같이 구현되어 있습니다.

public interface RemoteAuthenticationManager {
    Collection<? extends GrantedAuthority> attemptAuthentication(String username, String password) throws RemoteAuthenticationException;
}

매우 단순한 Interface입니다. 여기서 주의할 것이, username과 password는 모두 평문으로 들어오게 됩니다. DaoAuthenticationManager의 경우, Password를 따로 encoding을 할 수 있는 기능을 제공하지만 RemoteAuthenticationManager의 경우에는 plan text로 username과 password를 다뤄야지 됩니다. 하긴 이게 당연한것인지도 모릅니다. username과 password를 단순히 전달하는 역활을 하게 되는 것이 일반적이니까요.

개인적으로 만들어본 SOAP을 이용한 외부 서비스를 호출하는 RemoteAuthenticationManager입니다. RemoteAuthenticationManager를 통해 인증을 처리하고, 외부 SOAP API에 있는 사용자 정보를 Application DB에 옮기는 작업을 하도록 구성하였습니다. 그 이유는 UserDetailsService 때문입니다. UserDetailsService를 통해 username, password 이외의 사용자 정보를 활용할 수 있는 여지를 남겨주고 싶었습니다.

public class SsoAuthServiceImpl implements SsoAuthService, RemoteAuthenticationManager {
    private static final QName SERVICE_NAME = new QName("http://ws.daesung.co.kr/", "SSOWebServiceForJava");
    public static final String USERNAME_PASSWORD_IS_NOT_MATCHED = "username/password is not matched!";
    public static final String ROLE_USER = "ROLE_USER";

    @Getter
    @Setter
    private UserService userService;
    @Getter
    @Setter
    private ApplicationContext context;

    @Override
    public SsoAuthResult login(String username, String password) throws IOException {
        URL wsdlURL = SSOWebServiceForJava.WSDL_LOCATION;
        SSOWebServiceForJava service = new SSOWebServiceForJava(wsdlURL, SERVICE_NAME);
        SSOWebServiceForJavaSoap request = service.getSSOWebServiceForJavaSoap12();
        ArrayOfString result = request.ssoLoginArray(username, password, "NS");
        List<String> items = result.getString();

        SsoAuthResult authResult = context.getBean(SsoAuthResult.class);
        authResult.initialize(items);

        return authResult;
    }

    @Override
    public Collection<? extends GrantedAuthority> attemptAuthentication(String username, String password) throws RemoteAuthenticationException {
       try {

            SsoAuthResult authResult = login(username, password);
            if (authResult.isLoginSuccess()) {
                User user = userService.findByUsername(username);
                if(user == null) {
                    user = userService.addNewUser(username, authResult.getName(), password, ROLE_USER);
                } else {
                    user.setName(authResult.getName());
                    userService.updateUser(user);
                }
                List<SimpleGrantedAuthority> authorities = new ArrayList<>();
                for(String role : user.getRoles()) {
                    authorities.add(new SimpleGrantedAuthority(role));
                }
                return authorities;
            } else {
                throw new RemoteAuthenticationException(USERNAME_PASSWORD_IS_NOT_MATCHED);
            }

        } catch (Exception e) {
            throw new RemoteAuthenticationException(USERNAME_PASSWORD_IS_NOT_MATCHED);
        }
//        return null;
    }
}

RemoteAuthenticationManager의 경우에는 attemptAuthentication method만을 갖는 interface입니다. 사용자의 username, password가 전달되기 때문에, 이를 이용해서 외부 API call을 해서 인증을 처리하는 것이 가능하게 됩니다.

이를 모두 반영시킨 Spring Security의 Configuration은 다음과 같습니다.

@EnableWebSecurity
@Configuration
@Slf4j
@ComponentScan("co.kr.daesung.app.center.api.web.cors")
@ImportResource(value = "classpath:cxf-servlet.xml")
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    public static final String CORS_SUPPORT_FILTER = "corsSupportFilter";
    public static final String ADMIN_ROLE = "ADMIN";

    @Autowired
    private UserService userService;
    @Autowired
    private ApplicationContext context;
    @Autowired
    private CorsSupportLoginUrlAuthenticationEntryPoint corsEntryPoint;
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .exceptionHandling().authenticationEntryPoint(corsEntryPoint)
             .and()
                .csrf().disable()
                .authorizeRequests()
                .antMatchers("/api/admin/**").hasRole(ADMIN_ROLE)
                .antMatchers("/api/apiKey/**").authenticated()
                .antMatchers("/api/auth/**").authenticated()
            .and()
                .addFilterAfter(digestAuthenticationFilter(), BasicAuthenticationFilter.class);
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(remoteAuthenticationProvider());
    }

    @Bean
    public RemoteAuthenticationProvider remoteAuthenticationProvider() throws Exception {
        RemoteAuthenticationProvider provider = new RemoteAuthenticationProvider();
        provider.setRemoteAuthenticationManager(remoteAuthenticationManager());
        return provider;
    }

    @Bean
    public RemoteAuthenticationManager remoteAuthenticationManager() throws Exception {
        SsoAuthServiceImpl ssoAuthService = new SsoAuthServiceImpl();
        ssoAuthService.setUserService(userService);
        ssoAuthService.setContext(context);
        return ssoAuthService;
    }

    /**
     * AuthenticationManager를 LoginController에서 사용하기 위해서는 반드시, @Bean으로 등록되어야지 된다.
     * @return
     * @throws Exception
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    /**
     * UserDetailsService를 구성해준다. Bean Name은 반드시 BeanIds.USER_DETAILS_SERVICE로 등록되어야지 된다.
     * @return
     * @throws Exception
     */
    @Override
    @Bean(name = BeanIds.USER_DETAILS_SERVICE)
    public UserDetailsService userDetailsServiceBean() throws Exception {
        CommonAppUserDetailService userDetailsService = new CommonAppUserDetailService();
        userDetailsService.setUserService(context.getBean(UserService.class));
        return userDetailsService;
    }

    @Bean
    public SsoAuthService ssoAuthService() {
        return new SsoAuthServiceImpl();
    }

}

UserDetailsService는 반드시 구현하는 것이 좋습니다. 그리고 외부 서비스를 이용해서 로그인을 진행하더라도, 기본적인 사용자 정보을 반드시 Application DB안에 넣는 것이 좋을 것 같습니다.

Posted by Y2K
,