REST API 를 구현하기 위해서 Spring Security를 통해
@Bean
public SeucurityFilterChain filterChain(HttpSecurity http)throws Exception{
return http.csrf(CsrfConfigurer::disable)
.cors(CorsConfigurer::disable)
.authorizeHttpRequests(
authManagerConfig->
authManagerConfig.requestMatchers("/*").permitAll()
)
.formLogin(formLoginConfig->formLoginConfig
.usernameParameter("userid")
.passwordParameter("userpw")
.loginProcessingUrl("/login")
).build();
}
위와 같이 구성을 하면 postman으로 json 형식으로 body에 담아보내면 될 줄 알았다.
하지만, 이 것은 내가 formLogin에 대한 이해도가 떨어져서 이렇게 생각 했던 것이다. 기본적으로 Spring Security는 form data 형식을 통한 로그인을 지원하고 있기 때문에 body를 통한 접근을 했을 때, CustomUserDetailsService내의 파라미터에 아무것도 들어있지 않는 것을 알 수 있다.
이를 해결하는 것은 body를 통한 데이터를 처리할 수 있도록 filter를 가용해야한다.
즉 초기에 데이터를 받는 필터 부분을 커스텀마이징해야한다는 것이다.
트러블 슈팅이라고 하기에 조금 뭐하지만..
이러한 부분때문에 난항을 겪는 사람이 많을 것 같아서 올리게 되었다.
public class JsonUsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
private static final String DEFAULT_LOGIN_REQUEST_URL = "/login";
private static final String HTTP_METHOD = "POST";
private static final String CONTENT_TYPE = "application/json";
private static final AntPathRequestMatcher DEFAULT_LOGIN_PATH_REQUEST_MATCHER =
new AntPathRequestMatcher(DEFAULT_LOGIN_REQUEST_URL, HTTP_METHOD);
private final ObjectMapper objectMapper;
public JsonUsernamePasswordAuthenticationFilter(ObjectMapper objectMapper,
AuthenticationSuccessHandler authenticationSuccessHandler,
AuthenticationFailureHandler authenticationFailureHandler
) {
super(DEFAULT_LOGIN_PATH_REQUEST_MATCHER);
this.objectMapper = objectMapper;
setAuthenticationSuccessHandler(authenticationSuccessHandler);
setAuthenticationFailureHandler(authenticationFailureHandler);
}
@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());
}
LoginDto loginDto = objectMapper.readValue(StreamUtils.copyToString(request.getInputStream(), StandardCharsets.UTF_8), LoginDto.class);
String username = loginDto.getUsername();
String password = loginDto.getPassword();
if (username == null || password == null) {
throw new AuthenticationServiceException("DATA IS MISS");
}
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
protected void setDetails(HttpServletRequest request, UsernamePasswordAuthenticationToken authRequest) {
authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
}
@Data
private static class LoginDto {
String username;
String password;
}
}
위와 같은 filter를 만들고
http.addFilterBefore(jsonUsernamePasswordAuthenticationFilter(), LogoutFilter.class);
이런식으로 filter를 추가해주면 된다. 자세한 것은 AbstractAuthenticationProcessingFilter 를 다루는 게시물을 참조하면 좋을 것 같다. LogoutFilter 앞에 넣는 이유는 해당 게시물을 보면 바로 이해할 수 있을 것이다.