공부기록/Spring Security

[실습] 스프링시큐리티 Json data Login 처리

jhs0129 2022. 12. 28. 15:15
320x100
반응형

목차

기존에 formLogin을 사용해서 진행했던 이전 프로젝트에서 크게 달라진 점은 없다

달라진 점이라곤 filter를 새로 정의하였고, filter를 설정하는 부분이 추가가 되었다는 점이다

로그인 처리 방식

기존에 formLogin을 사용하면 UsernamePasswordAuthenticationFilter가 추가되는 것은 알고 있을 것이다

해당 Filter를 보면 알 수 있듯이 사용자가 전달해준 정보를 request.getParameter()를 통해서 얻어오게 된다

하지만 json을 사용해서 requestBody에 담아서 보내게 된다면 해당 부분에서 null값이 넘어가게되어 처리가 불가능 하게 된다

이 부분에 대해서 로그인을 처리하는 방식은 크게 두가지를 생각을 해봤다

  1. json에서 값을 추출하여 request에 담아 UsernamePasswordAuthenticationFilter에 넘겨주기
  2. json을 통해 값을 받은 로그인을 처리하는 filter를 새로 생성하기

두가지 방식을 모두 구현을 해보자

JsonToHttpRequest

public class JsonToHttpRequestFilter implements Filter {

    private static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
    private static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
    private final ObjectMapper objectMapper;
    private RequestMatcher matcher;

    public JsonToHttpRequestFilter(ObjectMapper objectMapper) {
        this.objectMapper = objectMapper;
        this.matcher = new AntPathRequestMatcher("/login", "POST");
    }

    @Override
    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException {
        HttpRequestWithModifiableParameters request = new HttpRequestWithModifiableParameters((HttpServletRequest) req);

        if (matcher.matches(request)) {
            LoginDto loginData = objectMapper.readValue(request.getInputStream(), LoginDto.class);

            request.setParameter(SPRING_SECURITY_FORM_USERNAME_KEY, loginData.getUserId());
            request.setParameter(SPRING_SECURITY_FORM_PASSWORD_KEY, loginData.getPassword());
        }
        chain.doFilter(request, resp);
    }

    @Setter @Getter
    @NoArgsConstructor
    private static class LoginDto {
        private String userId;
        private String password;
    }
}
public class HttpRequestWithModifiableParameters extends HttpServletRequestWrapper {

    private Map<String, String[]> params;

    public HttpRequestWithModifiableParameters(HttpServletRequest request) {
        super(request);
        this.params = new HashMap<>(request.getParameterMap());
    }

    @Override
    public String getParameter(String name) {
        String returnValue = null;
        String[] paramArray = getParameterValues(name);
        if (paramArray != null && paramArray.length > 0) {
            returnValue = paramArray[0];
        }
        return returnValue;
    }

    @Override
    public String[] getParameterValues(String name) {
        String[] result = null;
        String[] temp = params.get(name);
        if (temp != null) {
            result = new String[temp.length];
            System.arraycopy(temp, 0, result, 0, temp.length);
        }
        return result;
    }

    public void setParameter(String name, String value) {
        params.put(name, new String[]{value});
    }
}

위 코드가 최종 구현한 코드이다

단순히 reqeust.setParameter()를 사용해서 데이터를 넘기면 되겠지라고 생각을 했지만 역시나 구현을 해나가면서 처음 생각과는 다르게 생각을 해야 할 문제들이 많이 생기게 되었다

문제점들

  1. /login일 때만 호출이 되도록 uri를 검증을 해야한다
  2. setParameter()라는 메소드따윈 없다 -> 이게 가장 큰 오산이었다 왜 있다고 생각을 했을까...

두가지 문제를 해결하기 위해 삽질이 시작되었다

1번을 해결하기 위해선 RequestMatcher를 생성하여 일치하는 uri만을 통과시키도록 했고

2번을 해결하기 위해서 HttpRequestWithModifiableParameters라는 추가 객체를 생성을 하게 되었다

이 두가지가 구현을 하면서 생긴 문제점들이고 구현을 완료 후 설정 파일에 filter를 추가하면서 'json을 받아서 처리를 하는데 굳이 formLogin을??' 이라는 생각이 들게 되었다

이렇게까진 굳이라는 생각이 들면서 두번째 방법을 생각해내게 되었다

JsonLoginProcessFilter

public class JsonLoginProcessFilter extends AbstractAuthenticationProcessingFilter {

    private static final String CONTENT_TYPE = "application/json";
    private static final String SPRING_SECURITY_FORM_USERNAME_KEY = "userId";
    private static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
    private static final String DEFAULT_FILTER_PROCESSES_URL = "/login";
    private final ObjectMapper objectMapper;

    public JsonLoginProcessFilter(ObjectMapper objectMapper, AuthenticationManager authenticationManager) {
        super(DEFAULT_FILTER_PROCESSES_URL, authenticationManager);
        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());
        }

        Map<String, String> parameter = objectMapper.readValue(request.getInputStream(), Map.class);
        String username = parameter.get(SPRING_SECURITY_FORM_USERNAME_KEY);
        String password = parameter.get(SPRING_SECURITY_FORM_PASSWORD_KEY);

        UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
        return this.getAuthenticationManager().authenticate(authRequest);
    }
}

UsernamePasswordAuthenticationFilter를 모티브로 비슷하게 구현을 하였다

  1. AbstractAuthenticationProcessingFilter을 상속을 받았고
  2. super(DEFAULT_FILTER_PROCESSES_URL)를 통해 로그인 uri처리를 하였고
  3. 데이터를 읽어오고(json으로 부터 읽는다는 차이는 있지만)
  4. authentication을 사용해서 manager.authenticate()를 호출하였다

결국 처음 방식보다 훨씬 간결해졌다

마지막으로 설정 방법을 알아보자

SecurityConfig

Filter등록

이 코드는 단순히 filter를 등록하는 것만 추가가 되었다

@Configuration
@RequiredArgsConstructor
public class SecurityConfig {

    private final JsonLoginProcessFilter jsonLoginProcessFilter;
    private final JsonToHttpRequestFilter jsonToHttpRequestFilter;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.csrf().disable();

        /*http.formLogin()
                .loginPage("/login")
                .successHandler((request, response, authentication) -> {
                    response.getWriter().println("success login");
                });

        http.addFilterAfter(jsonToHttpRequestFilter, LogoutFilter.class);*/
        http.addFilterAfter(jsonLoginProcessFilter, LogoutFilter.class);
        return http.build();
    }
}

Filter Bean 등록

UsernamePasswordAuthenticationFilter와 같이 authenticationManager를 등록을 해주었다

@Configuration
@RequiredArgsConstructor
public class SecurityFilterBeanConfig {

    private final ObjectMapper objectMapper;
    private final AuthenticationManager authenticationManager;

    @Bean
    public JsonLoginProcessFilter jsonLoginProcessFilter() {
        JsonLoginProcessFilter jsonLoginProcessFilter = new JsonLoginProcessFilter(objectMapper, authenticationManager);
        jsonLoginProcessFilter.setAuthenticationSuccessHandler((request, response, authentication) -> {
            response.getWriter().println("Success Login");
        });
        return jsonLoginProcessFilter;
    }

    @Bean
    public JsonToHttpRequestFilter jsonToHttpRequestFilter() {
        return new JsonToHttpRequestFilter(objectMapper);
    }
}

AuthenticationManager Bean 등록

위 과정에서 AuthenticationManager를 생성자를 통해 받아올려 했는데 Bean을 찾을 수 없다하여 두가지 방식을 통해서 직접 등록을 해주었다

  1. 직접 ProviderManager생성 후 등록
  2. AuthenticationConfiguration에서 manager가져오기
@Configuration
@RequiredArgsConstructor
public class SecurityBeanConfig {

    private final UserRepository userRepository;

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

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
        return configuration.getAuthenticationManager();
    }

    @Bean
    public DaoAuthenticationProvider daoAuthenticationProvider(){
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        provider.setPasswordEncoder(passwordEncoder());
        provider.setUserDetailsService(userService());
        return provider;
    }

    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Bean
    public UserService userService() {
        return new UserService(userRepository, passwordEncoder());
    }
}
320x100
반응형