모르지 않다는 것은 아는것과 다르다.

Spring Security

Ajax 인증처이

채마스 2022. 2. 28. 20:50

Ajax 인증 처리 흐름

  • 보는 것 처럼 인증의 경우 AjaxAuthenticationFilter 로 인가의 경우 FilterSecurityIntercepter 로 요청이 전달 되는 것을 확인할 수 있다.
  • ajax 방식으로 구현하기 위해서 각각의 클래스 구현해 줘야 하지만, 방식은 폼인증 방식과 크게 다르지 않다.
  • AbstractAuthenticationProcessingFilter 상속해서 구현할 수 있다.





AjaxAuthenticationFilter 구현

  • 폼인증 필터를 사용할 수 없기 때문에 AjaxLoginProcessingFilter 클래스 구현한다.
public class AjaxLoginProcessingFilter extends AbstractAuthenticationProcessingFilter {

    private ObjectMapper objectMapper = new ObjectMapper();

    public AjaxLoginProcessingFilter() {
        super(new AntPathRequestMatcher("/api/login"));
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {

        if(!isAjax(request)){
            throw new IllegalStateException("Authentication is not supported");
        }

        AccountDto accountDto = objectMapper.readValue(request.getReader(), AccountDto.class);
        if(StringUtils.isEmpty(accountDto.getUsername()) || StringUtils.isEmpty(accountDto.getPassword())){
            throw new IllegalArgumentException("Username or Passoword is empty");
        }

        AjaxAuthenticationToken ajaxAuthenticationToken = new AjaxAuthenticationToken(accountDto.getUsername(), accountDto.getPassword());

        return getAuthenticationManager().authenticate(ajaxAuthenticationToken);
    }

    private boolean isAjax(HttpServletRequest request) {

        if("XMLHttpRequest".equals(request.getHeader("X-Requested-With"))){
            return true;
        }
    return false;
    }
}
  • new AntPathRequestMatcher("/api/login") 를 통해서 "/api/login" 로 오는 요청을 처리하도록 설정할 수 있다.
  • isAjax 메소드를 통해서 해더의 값을 분석해서 ajax 요청을 구분할 수 있다.
  • 최종적으로 AuthenticationManager 에게 ajaxAuthenticationToken 넘기는 것을 확인할 수 있다.
  • 이제 AuthenticationManager 는 ajaxAuthenticationToken 와 적절하게 매치될 Provider 를 찾아서 요청을 위임하게 될 것이고, 해당 Provider 가 인증을 처리하게 된다.






AjaxAuthenticationToken 구현

  • UsernamePasswordAuthenticationToken 로 적절한 Provider를 불러올 수 없기 때문에 AjaxAuthenticationToken 을 따로 구현해 줘야 한다.
public class AjaxAuthenticationToken extends AbstractAuthenticationToken {

    private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;


    private final Object principal;
    private Object credentials;


    public AjaxAuthenticationToken(Object principal, Object credentials) {
        super(null);
        this.principal = principal;
        this.credentials = credentials;
        setAuthenticated(false);
    }


    public AjaxAuthenticationToken(Object principal, Object credentials,
                                               Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal = principal;
        this.credentials = credentials;
        super.setAuthenticated(true); // must use super, as we override
    }

    public Object getCredentials() {
        return this.credentials;
    }

    public Object getPrincipal() {
        return this.principal;
    }

    public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
        if (isAuthenticated) {
            throw new IllegalArgumentException(
                    "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
        }

        super.setAuthenticated(false);
    }

    @Override
    public void eraseCredentials() {
        super.eraseCredentials();
        credentials = null;
    }

}
  • AbstractAuthenticationToken 를 상속받아서 구현할 수 있다.
  • 위의 코드는 UsernamePasswordAuthenticationToken 의 코드를 가져와 복붙해 준 코드이다.





AjaxAuthenticationProvider 구현

  • AjaxAuthenticationProvider 클래스를 구현한다.
public class AjaxAuthenticationProvider implements AuthenticationProvider {

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    @Transactional
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {

        String loginId = authentication.getName();
        String password = (String) authentication.getCredentials();

        AccountContext accountContext = (AccountContext)userDetailsService.loadUserByUsername(loginId);

        if (!passwordEncoder.matches(password, accountContext.getPassword())) {
            throw new BadCredentialsException("Invalid password");
        }

        return new AjaxAuthenticationToken(accountContext.getAccount(), null, accountContext.getAuthorities());
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return authentication.equals(AjaxAuthenticationToken.class);
    }
}
  • AuthenticationProvider 인터페이스를 구현한다.
  • 위의 코드는 FormAuthenticationProvider 코드와 거의 비슷하게 구현했다.
  • AjaxAuthenticationToken 을 다룰 수 있도록 구현했다.
  • 구현에 맞게 authenticate, support 메소드를 구현해 주면 된다.
  • 최종적으로 사용자 정보와 권한 정보를 AjaxAuthenticationToken 에 담아서 AuthenticationManager 에게 전달해 준다.
  • AuthenticationManager 는 다시 AjaxAuthenticationFilter 에게 전달한다.





AjaxAuthenticationSuccessHandler

  • 인증이 성공한 후 처리를 구현하면 된다.
public class AjaxAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    private ObjectMapper objectMapper = new ObjectMapper();

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {

        Account account = (Account)authentication.getPrincipal();

        response.setStatus(HttpStatus.OK.value());
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);

        objectMapper.writeValue(response.getWriter(), account);
    }
}
  • AuthenticationSuccessHandler 인터페이스를 구현한다.
  • response 에 Header 정보를 설정했다.
  • objectMapper 를 이용해서 JSON 형식으로 변환하여 인증 객체를 리턴했다.





AjaxAuthenticationFailureHandler

  • 인증이 실패한 후 처리를 구현한다.
public class AjaxAuthenticationFailureHandler implements AuthenticationFailureHandler {

    private ObjectMapper objectMapper = new ObjectMapper();

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {

        String errMsg = "Invalid Username or Password";

        response.setStatus(HttpStatus.UNAUTHORIZED.value());
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);

        if(exception instanceof BadCredentialsException) {
            errMsg = "Invalid Username or Password";
        } else if(exception instanceof DisabledException) {
            errMsg = "Locked";
        } else if(exception instanceof CredentialsExpiredException) {
            errMsg = "Expired password";
        }

        objectMapper.writeValue(response.getWriter(), errMsg);
    }
}
  • AuthenticationFailureHandler 인터페이스를 구현한다.
  • response 에 Header 정보를 설정했다.
  • JSON 형식으로 변환하여 오류 메시지 리턴 했다.






AjaxLoginUrlAuthenticationEntryPoint

  • ExceptionTranslationFilter 에서 인증 예외 시 호출한다.
  • 익명사용자의 경우에 호출된다.
public class AjaxLoginAuthenticationEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {

        response.sendError(HttpServletResponse.SC_UNAUTHORIZED,"UnAuthorized");

    }
}
  • 위와 같이 response 에 인증 오류 메시와 401 상태 코드 반환했다.





AjaxAccessDeniedHandler

  • ExceptionTranslationFilter 에서 인가 예외 시 호출
  • 인증은 됐지만, 권한이 없을때 실행된다.
public class AjaxAccessDeniedHandler implements AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {

        response.sendError(HttpServletResponse.SC_FORBIDDEN, "Access is denied");
    }
}
  • 위와 같이 response 에 인증 오류 메시와 401 상태 코드 반환했다.





AjaxSecurityConfig 구현

  • Ajax 용으로 SecurityConfig 구성한다.
@Configuration
@Order(0)
public class AjaxSecurityConfig extends WebSecurityConfigurerAdapter {

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

    @Bean
    public AuthenticationProvider ajaxAuthenticationProvider() {
        return new AjaxAuthenticationProvider();
    }

    @Bean
    public AuthenticationSuccessHandler ajaxAuthenticationSuccessHandler(){
        return new AjaxAuthenticationSuccessHandler();
    }

    @Bean
    public AuthenticationFailureHandler ajaxAuthenticationFailureHandler(){
        return new AjaxAuthenticationFailureHandler();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .antMatcher("/api/**")
                .authorizeRequests()
                .antMatchers("/api/messages").hasRole("MANAGER")
                .anyRequest().authenticated()
        .and()
                .addFilterBefore(ajaxLoginProcessingFilter(), UsernamePasswordAuthenticationFilter.class);

        http
                .exceptionHandling()
                .authenticationEntryPoint(new AjaxLoginAuthenticationEntryPoint())
                .accessDeniedHandler(ajaxAccessDeniedHandler());

        http.csrf().disable();
    }

    public AccessDeniedHandler ajaxAccessDeniedHandler() {
        return new AjaxAccessDeniedHandler();
    }

    @Bean
    public AjaxLoginProcessingFilter ajaxLoginProcessingFilter() throws Exception {
        AjaxLoginProcessingFilter ajaxLoginProcessingFilter = new AjaxLoginProcessingFilter();
        ajaxLoginProcessingFilter.setAuthenticationManager(authenticationManagerBean());
        ajaxLoginProcessingFilter.setAuthenticationSuccessHandler(ajaxAuthenticationSuccessHandler());
        ajaxLoginProcessingFilter.setAuthenticationFailureHandler(ajaxAuthenticationFailureHandler());
        return ajaxLoginProcessingFilter;
    }
}




REFERENCES

  • 정수원님의 스프링 시큐리티

'Spring Security' 카테고리의 다른 글

메소드 시큐리티  (0) 2022.02.28
Custom DSL 적용  (0) 2022.02.28
Custom Filter  (0) 2022.02.28
RememberMeAuthenticationFilter  (0) 2022.02.28
AccessDesicionManager 와 AccessDecisionVoter  (0) 2022.02.28