[Trouble Shooting] Spring Security를 통한 인증 구현 중 CustomUserDetailsService 의 loadUserByUsername의 username이 없는 오류

k·2024년 3월 5일
0

Trouble Shooting

목록 보기
3/3

사건 개요

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 앞에 넣는 이유는 해당 게시물을 보면 바로 이해할 수 있을 것이다.

profile
You must do the things you think you cannot do

0개의 댓글