Spring/Spring Security

[Spring Security] ajax login

빅콜팝 2023. 2. 15. 08:33
728x90
반응형

- Spring Security 를 이용하여 (ajax)비동기 로그인 구현하기

 

인증 처리 구현 로직

 

인가 처리 구현 로직

SecurityConfig.java

@Configuration
@EnableWebSecurity
@Order(0)
@Slf4j
public class AjaxSecurityConfig extends WebSecurityConfigurerAdapter {
    private String[] permitAllResources = {"/api/public/**","/sample","/aws/**","/sample/**", "/login/**", "/outer/**", "/fonts/**", "/landing/**", "/error/**", "/aws/health/check"};

    @Override
    public void configure(WebSecurity web) throws Exception {
        // 정적 파일은 보안 필터 거치지 않음
        web.ignoring()
                .antMatchers(permitAllResources)
                .requestMatchers(PathRequest.toStaticResources().atCommonLocations());
    }

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

    @Bean
    private AuthenticationProvider authenticationProvider() {
        return new CustomAuthenticationProvider();
    }

    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Override
    protected void configure(final HttpSecurity http) throws Exception { // username, password
        http
                .authorizeRequests()
                .anyRequest().authenticated();
        http
                .addFilterBefore(ajaxLoginProcessingFilter(), UsernamePasswordAuthenticationFilter.class); // ajax 로그인

        http
                .exceptionHandling()
                .accessDeniedHandler(accessDeniedHandler())
        ;

        http
                .csrf().disable(); // post 방식일 때는 필수
    }

    @Override
    public AuthenticationManager authenticationManager() throws Exception {
        return super.authenticationManagerBean();
    }

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

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

    @Bean
    public AuthenticationFailureHandler ajaxAuthenticationFailureHandler(){
        return new AjaxAuthenticationFailureHandler();
    }
    @Bean
    public AccessDeniedHandler accessDeniedHandler() {
        AccessDeniedHandler accessDeniedHandler = new CustomAccessDeniedHandler();
        ((CustomAccessDeniedHandler) accessDeniedHandler).setErrorPage("/denied");
        return accessDeniedHandler;
    }
}

 

UsernamePasswordAuthenticationFilter.class 호출 전 ajaxLoginProcessingFilter() 먼저 체크 

http
        .addFilterBefore(ajaxLoginProcessingFilter(), UsernamePasswordAuthenticationFilter.class); // ajax 로그인

 

AjaxLoginProcessingFilter.java

public class AjaxLoginProcessingFilter extends AbstractAuthenticationProcessingFilter {

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

    private ObjectMapper objectMapper = new ObjectMapper();

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

        if(!isAjax(request)){
            throw new IllegalStateException("Ajax 요청이 아님");
        }

        Member member = objectMapper.readValue(request.getReader(), Member.class);

        if(StringUtils.isEmpty(member.getUserId()) || StringUtils.isEmpty(member.getPassword())){
            throw new IllegalStateException("아이디 또는 비밀번호가 존재하지 않습니다.");
        }

        AjaxAuthenticationToken ajaxAuthenticationToken =
                new AjaxAuthenticationToken(member.getUserId(), member.getPassword());

        return getAuthenticationManager().authenticate(ajaxAuthenticationToken);
    }

    private boolean isAjax(HttpServletRequest request){
        if("XMLHttpRequest".equals(request.getHeader("X-Requested-with"))){
            return true;
        }
        return false;
    }
}

 

Ajax용 로그인 토큰

/**
 * Ajax 로그인용 토큰
 */
public class AjaxAuthenticationToken extends AbstractAuthenticationToken {

    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);
    }

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

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

 

인증 처리

public class AjaxAuthenticationProvider implements AuthenticationProvider {

    @Autowired
    private UserDetailsService userDetailsService;
    @Autowired
    private PasswordEncoder passwordEncoder;

    /**
     * 인증에 관련된 검증 처리
     * @param authentication the authentication request object.
     * @return
     * @throws AuthenticationException
     */
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {

        // 사용자가 입력한 정보
        String username = authentication.getName();
        String password = (String)authentication.getCredentials();

        // DB에서 가져온 정보
        AccountContext accountContext = (AccountContext)userDetailsService.loadUserByUsername(username);

        passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();

        // 비밀번호 일치하지 않음
        if(!passwordEncoder.matches(password, accountContext.getMember().getPassword())){
            throw new BadCredentialsException("BadCredentialsException");
        }

        // 정책에 따라 추가적인 검증 가능


        // 최종적인 인증 객체
        return new AjaxAuthenticationToken(accountContext.getMember(), null, accountContext.getAuthorities());
    }

    @Override
    public boolean supports(Class<?> authentication) {
        // 토큰 타입과 파라미터 타입이 일치하면 인증 처리.
        return authentication.equals(AjaxAuthenticationToken.class);
    }
}

 

/**
 * 로그인 성공 시 반환 Json
 */
public class AjaxAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    private ObjectMapper objectMapper = new ObjectMapper();

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

        Member member = (Member)authentication.getPrincipal();
        response.setStatus(HttpStatus.OK.value());
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);

        objectMapper.writeValue(response.getWriter(),member);
    }
}

 

/**
 * 로그인 실패 시 반환 Json
 */
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 = "로그인 실패";

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

        // 인증에서 온 에러
        if(exception instanceof BadCredentialsException){
            errMsg = "Invalid Username or Password";
        }else if(exception instanceof InsufficientAuthenticationException){
            errMsg = "Invalid Secret Key";
        }

        objectMapper.writeValue(response.getWriter(), errMsg);
    }
}
728x90
반응형