목차
- [이론] 스프링 시큐리티 1
- [이론] 스프링 시큐리티2
- [실습] 스프링 시큐리티 Form Login
- [추가] CustomAuthenticationProvider vs DaoAuthenticationProvider
- [이론] 스프링 시큐리티3
- [이론] 스프링 시큐리티4
- [추가] AuthorizeReqeusts vs AuthorizeHttpRequests
- [실습] 스프링 시큐리티 Json data Login 처리
- [실습] 스프링 시큐리티 JWT 설정
- [실습] 스프링 시큐리티 JWT 처리
- OAuth2
기존에 formLogin을 사용해서 진행했던 이전 프로젝트에서 크게 달라진 점은 없다
달라진 점이라곤 filter를 새로 정의하였고, filter를 설정하는 부분이 추가가 되었다는 점이다
로그인 처리 방식
기존에 formLogin을 사용하면 UsernamePasswordAuthenticationFilter가 추가되는 것은 알고 있을 것이다
해당 Filter를 보면 알 수 있듯이 사용자가 전달해준 정보를 request.getParameter()
를 통해서 얻어오게 된다
하지만 json을 사용해서 requestBody에 담아서 보내게 된다면 해당 부분에서 null값이 넘어가게되어 처리가 불가능 하게 된다
이 부분에 대해서 로그인을 처리하는 방식은 크게 두가지를 생각을 해봤다
- json에서 값을 추출하여 request에 담아 UsernamePasswordAuthenticationFilter에 넘겨주기
- 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()
를 사용해서 데이터를 넘기면 되겠지라고 생각을 했지만 역시나 구현을 해나가면서 처음 생각과는 다르게 생각을 해야 할 문제들이 많이 생기게 되었다
문제점들
- /login일 때만 호출이 되도록 uri를 검증을 해야한다
- 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를 모티브로 비슷하게 구현을 하였다
- AbstractAuthenticationProcessingFilter을 상속을 받았고
- super(DEFAULT_FILTER_PROCESSES_URL)를 통해 로그인 uri처리를 하였고
- 데이터를 읽어오고(json으로 부터 읽는다는 차이는 있지만)
- 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을 찾을 수 없다하여 두가지 방식을 통해서 직접 등록을 해주었다
- 직접 ProviderManager생성 후 등록
- 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());
}
}
'공부기록 > Spring Security' 카테고리의 다른 글
[실습] 스프링 시큐리티 JWT 처리 (0) | 2023.02.20 |
---|---|
[실습] 스프링 시큐리티 JWT 설정 (0) | 2023.01.26 |
[추가] AuthorizeRequests vs AuthorizeHttpRequests (0) | 2022.12.28 |
[이론] 스프링 시큐리티4 (0) | 2022.12.28 |
[이론] 스프링 시큐리티3 (0) | 2022.12.26 |