[Spring] 로그인 시 Cors 에러 해결
기존 SSR(Thymeleaf + SpringBoot) 방식의 프로젝트를 CSR(React + SpringBoot)로 바꾸다가 발생한 에러이다.
로그인 방식
기존에 폼 로그인과 세션을 사용해서 인증하던 방식을 JWT를 사용하도록 변경하고 있었다..
그 과정에서 로그인은 UsernamePasswordAuthenticationFilter를 상속한 클래스를 만들어서 처리할 수 있도록 했다.
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private final AuthenticationManager authenticationManager;
private final JwtTokenProvider jwtTokenProvider;
@SneakyThrows
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) {
ObjectMapper om = new ObjectMapper();
LoginRequestDto dto = om.readValue(request.getInputStream(), LoginRequestDto.class);
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
dto.username(),
dto.password());
return authenticationManager.authenticate(authToken);
}
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response,
FilterChain chain, Authentication authResult) throws ServletException, IOException {
String accessToken = jwtTokenProvider.generateAccessToken(authResult);
String refreshToken = jwtTokenProvider.generateRefreshToken(authResult);
response.setHeader("Authorization", "Bearer " + accessToken);
response.setHeader("Refresh", refreshToken);
this.getSuccessHandler().onAuthenticationSuccess(request, response, authResult);
}
}
해당 클래스를 SecurityConfiguration 클래스에서 사용하여 인증을 처리할 수 있도록 했다.
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfiguration {
private final JwtTokenProvider jwtTokenProvider;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
...
http
.apply(new JwtSecurityConfig());
return http.build();
}
public class JwtSecurityConfig extends AbstractHttpConfigurer<JwtSecurityConfig, HttpSecurity> {
@Override
public void configure(HttpSecurity builder) throws Exception {
AuthenticationManager am = builder.getSharedObject(AuthenticationManager.class);
JwtAuthenticationFilter jwtAuthFilter = new JwtAuthenticationFilter(am, jwtTokenProvider);
jwtAuthFilter.setFilterProcessesUrl("/auth/login");
jwtAuthFilter.setAuthenticationSuccessHandler(new CustomAuthenticationSuccessHandler());
jwtAuthFilter.setAuthenticationFailureHandler(new CustomAuthenticationFailureHandler());
JwtFilter jwtFilter = new JwtFilter(jwtTokenProvider);
builder
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
.addFilter(jwtAuthFilter);
}
}
}
추가적으로 CORS에 대해서는 CorsFilter로 설정해주었다.
@Component
public class CorsFilter implements Filter {
private static final String CLIENT_URL = "http://localhost:3000";
@Override
public void init(FilterConfig filterConfig) throws ServletException {
Filter.super.init(filterConfig);
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws
IOException,
ServletException {
HttpServletRequest req = (HttpServletRequest)request;
HttpServletResponse res = (HttpServletResponse)response;
// 요청을 허용하는 출처
res.setHeader("Access-Control-Allow-Origin", CLIENT_URL);
// 요청을 허용하는 HTTP 메소드
res.setHeader("Access-Control-Allow-Methods", "*");
// 쿠키 요청 허용
res.setHeader("Access-Control-Allow-Credentials", "true");
// 요청을 허용하는 헤더 이름
res.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization");
// 클라이언트에서 preflight 요청 결과를 저장할 시간
res.setHeader("Access-Control-Max-Age", "3600");
if ("OPTIONS".equalsIgnoreCase(req.getMethod())) {
res.setStatus(HttpServletResponse.SC_OK);
} else {
chain.doFilter(request, response);
}
}
@Override
public void destroy() {
Filter.super.destroy();
}
}
에러 발생
위와 같이 서버 코드가 작성되었고, 클라이언트 측에서 로그인 요청을 하면 아래와 같은 에러가 발생한다.
CORS 정책에 막혔다고 하는 것을 볼 수 있다.
다른 url 요청에 대해서는 문제가 없지만 login 요청에 대해서만 거부된다.
서버 측에서는 아래와 같은 에러가 발생한다.
com.fasterxml.jackson.databind.exc.MismatchedInputException: No content to map due to end-of-input
at [Source: (org.apache.catalina.connector.CoyoteInputStream); line: 1, column: 0]
at com.fasterxml.jackson.databind.exc.MismatchedInputException.from(MismatchedInputException.java:59) ~[jackson-databind-2.15.2.jar:2.15.2]
at com.fasterxml.jackson.databind.ObjectMapper._initForReading(ObjectMapper.java:4916) ~[jackson-databind-2.15.2.jar:2.15.2]
at com.fasterxml.jackson.databind.ObjectMapper._readMapAndClose(ObjectMapper.java:4818) ~[jackson-databind-2.15.2.jar:2.15.2]
at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:3809) ~[jackson-databind-2.15.2.jar:2.15.2]
at com.e.commerce.global.security.jwt.JwtAuthenticationFilter.attemptAuthentication(JwtAuthenticationFilter.java:30) ~[main/:na]
...
확인해보면 JwtAuthenticationFilter 클래스에서 사용한 ObjectMapper에서 null을 받아 발생한 에러이다.
문제점 파악
Postman을 사용한 로그인 시에는 정상적으로 로그인이 되지만, 리액트앱을 사용했을 때만 에러가 발생한다.
그리고 구글링하던 중 다음과 같은 글을 보았다.
하지만 UsernamePasswordAuthenticationFilter는 특징이 있어 이 특징을 잘 처리해줘야한다.
- 해당 FIlter를 거치고 다음 Filter로 가지 않는다. 그렇기 때문에 인증 성공 여부에 따른 메서드 successAuthenticaiton/unSuccessfulAuthentication을 구현해야한다.
출처 : https://jaewoo2233.tistory.com/72
Spring Security - JWT 설정
해당 url로 { "email":"user", "password":"1111" } 데이터를 바디에 담아 요청하면 토큰이 발행되도록 만들려고 한다. Spring 2.7.0 기준 일단 먼저일반적으로 Spring Security에 로그인 요청을 날리면 UsernamePassword
jaewoo2233.tistory.com
문제 해결
이에 UsernamePasswordAuthenticationFilter를 사용한 로그인 방식이 CorsFilter를 거치지 않기 때문에
CORS 정책에서 거부당했다고 생각하여 CorsFilter를 CorsConfig를 설정해주는 형식으로 변경하였다.
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfiguration {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
// cors 설정
http
.cors(cors -> cors.configurationSource(
request -> {
CorsConfiguration config = new CorsConfiguration();
config.addAllowedOrigin("http://localhost:3000");
config.addAllowedMethod("*");
config.setAllowCredentials(true);
config.setAllowedHeaders(List.of("Origin", "X-Requested-With", "Content-Type", "Accept",
"Authorization", "Refresh"));
config.setExposedHeaders(List.of("Authorization", "Refresh"));
config.setMaxAge(3600L);
return config;
}
));
...
return http.build();
}
}
이제 로그인이 성공적으로 된다.
서버 측에서도 성공적으로 인증이 된 것을 볼 수 있다.
CustomAuthenticationSuccessHandler : ### 1. UsernamePasswordAuthenticationToken [Principal=jwanna, Credentials=[PROTECTED], Authenticated=true, Details=null, Granted Authorities=[ROLE_USER]]
CustomAuthenticationSuccessHandler : ### 2. jwanna
CustomAuthenticationSuccessHandler : # Authenticated Successful
'Spring' 카테고리의 다른 글
[Spring] Thymeleaf를 통한 페이지네이션 구현 (0) | 2023.09.07 |
---|---|
[Thymeleaf] th:href 경로에 변수를 사용한 경로 설정 (0) | 2023.09.07 |
[Thymeleaf] layout fragment로 변수 넘겨주기 (0) | 2023.09.07 |
[Spring/Thymeleaf] 세션 방식 로그인 및 템플릿 구현 (0) | 2023.08.29 |
[Spring/Error] WebSecurityCustomizer를 통해 정적 자원에 대한 Ignore가 안될때 (0) | 2023.08.28 |