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