게시판 API 만들기 - 시큐리티 JSON 로그인 커스텀

Seung jun Cha·2022년 7월 26일
0
  • formLogin을 할 때 사용하는 AbstractAuthenticationProcessingFilter에는 인증 성공과 실패에 대한 대부분의 로직이 들어가 있기 때문에, 이 필터를 상속받아 데이터를 처리하는 방식만 json으로 바꾸어서 처리하도록 만들겠습니다.
public class JsonUsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

    private static final String DEFAULT_LOGIN_REQUEST_URL = "/login";  // /login/oauth2/ + ????? 로 오는 요청을 처리할 것이다

    private static final String HTTP_METHOD = "POST";    //HTTP 메서드의 방식은 POST 이다.

    private static final String CONTENT_TYPE = "application/json";//json 타입의 데이터로만 로그인을 진행한다.

    private final ObjectMapper objectMapper;
    
    private static final String USERNAME_KEY="username";
    private static final String PASSWORD_KEY="password";


    private static final AntPathRequestMatcher DEFAULT_LOGIN_PATH_REQUEST_MATCHER =
            new AntPathRequestMatcher(DEFAULT_LOGIN_REQUEST_URL, HTTP_METHOD); //=>   /login 의 요청에, POST로 온 요청에 매칭된다.

    public JsonUsernamePasswordAuthenticationFilter(ObjectMapper objectMapper) {

        super(DEFAULT_LOGIN_PATH_REQUEST_MATCHER);   // 위에서 설정한  /oauth2/login/* 의 요청에, GET으로 온 요청을 처리하기 위해 설정한다.

        this.objectMapper = objectMapper;
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
        if(request.getContentType() == null || !request.getContentType().equals(CONTENT_TYPE)  ) {
            throw new AuthenticationServiceException("Authentication Content-Type not supported: " + request.getContentType());
        }

        String messageBody = StreamUtils.copyToString(request.getInputStream(), StandardCharsets.UTF_8);

        Map<String, String> usernamePasswordMap = objectMapper.readValue(messageBody, Map.class);
        
        String username = usernamePasswordMap.get(USERNAME_KEY);
        String password = usernamePasswordMap.get(PASSWORD_KEY);

        UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);//principal 과 credentials 전달

        return this.getAuthenticationManager().authenticate(authRequest);
    }
}
  • Security Config 설정
@EnableWebSecurity // 시큐리티 설정파일임을 의미
@RequiredArgsConstructor
public class SecurityConfig {

    private final ObjectMapper objectMapper;
    private final LoginService loginService;

    public SecurityFilterChain configure(HttpSecurity http) throws Exception {

        http
                .formLogin().disable()   //formLogin 인증방법 비활성화  -> JSON을 통해 로그인 처리
                .httpBasic().disable()   // httpBasic 인증방법 비활성화(특정 리소스에 접근할 때 username과 password 물어봄)  -> 토큰을 통해 인증처리 할 것이므로
                .csrf().disable()   // rest api이므로 csrf 보안이 필요없으므로 disable처리.
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                // jwt token으로 인증하므로 stateless 하도록 처리.  STATELESS 는 인증 정보를 서버에 담아 두지 않는다. STATELESS 상태에서는 기존 로그인 이후 해당정보를 서버에서 관리 하지 않기에
                //  로그인 이후 권한이 필요한 페이지를 호출하면 403 에러가 발생한다.
                .and()
                .authorizeRequests() //   각 경로 path 별 권한 처리
                .antMatchers("/login", "/signUp","/").permitAll()
                .anyRequest().authenticated();

        http.addFilterAfter(jsonUsernamePasswordAuthenticationFilter(), LogoutFilter.class);

        return http.build();
    }
    
     @Bean
    public PasswordEncoder passwordEncoder(){
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }

    @Bean
    public AuthenticationManager authenticationManager(){
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        provider.setPasswordEncoder(passwordEncoder());
        return new ProviderManager(provider);
    }

    @Bean
    public LoginSuccessJWTProvideHandler loginSuccessJWTProvideHandler(){
        return new LoginSuccessJWTProvideHandler();
    }

    @Bean
    public LoginFailureHandler loginFailureHandler(){
        return new LoginFailureHandler();
    }

    @Bean
    public JsonUsernamePasswordAuthenticationFilter jsonUsernamePasswordAuthenticationFilter(){
        JsonUsernamePasswordAuthenticationFilter jsonUsernamePasswordLoginFilter  = new JsonUsernamePasswordAuthenticationFilter(objectMapper);
        jsonUsernamePasswordLoginFilter.setAuthenticationManager(authenticationManager());
        jsonUsernamePasswordLoginFilter.setAuthenticationSuccessHandler(loginSuccessJWTProvideHandler());
        jsonUsernamePasswordLoginFilter.setAuthenticationFailureHandler(loginFailureHandler());
        return jsonUsernamePasswordLoginFilter;
    }
  1. PasswordEncoderFactories에서 createDelegatingPasswordEncoder 생성
  2. DaoAuthenticationProvider provider를 생성하고 provider에 passwordEncoder와 UserDetailsService를 세팅 후, ProviderManager return
  3. jsonUsernamePasswordLoginFilter에 authenticationManager세팅

=> createDelegatingPasswordEncoder - DaoAuthenticationProvider (PasswordEncoder, UserDetailService) - AuthenticationManager 리턴 - Filter에 등록

  • 로그인 성공, 실패 처리 : SimpleUrlAuthenticationSuccessHandler를 상속받아 구현
@Slf4j
public class LoginSuccessJWTProvideHandler extends SimpleUrlAuthenticationSuccessHandler {

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        UserDetails userDetails = (UserDetails) authentication.getPrincipal();
        log.info( "로그인에 성공합니다 JWT를 발급합니다. username: {}" ,userDetails.getUsername());


      response.getWriter().write("success");
    }
@Slf4j
public class LoginFailureHandler extends SimpleUrlAuthenticationFailureHandler {

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        response.setStatus(HttpServletResponse.SC_OK);//보안을 위해 로그인 오류지만 200 반환
        response.getWriter().write("fail");
        log.info("로그인에 실패했습니다");
    }
}

참고 https://ttl-blog.tistory.com/269?category=910686#LoginFailureHandler%--%EC%--%-D%EC%--%B-

0개의 댓글