기존 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

 

+ Recent posts