Spring
[JWT] Security Config 6.x.x
나는시화
2024. 11. 19. 21:46
common/config/WebSecurityConfig.java (전체코드)
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class WebSecurityConfig {
private final JwtAuthenticationFilter jwtAuthenticationFilter;
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
protected SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.cors(cors -> cors.configurationSource(corsConfigrationSourse()))
.csrf(CsrfConfigurer::disable) // csrf disable
.httpBasic(HttpBasicConfigurer::disable) // http basic 인증 방식 disable
.formLogin(FormLoginConfigurer::disable) // form 로그인 방식 disable
.sessionManagement(sessionManagement -> sessionManagement
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
).authorizeHttpRequests(request -> request
.requestMatchers("/", "/user/*", "/css/**","/js/**").permitAll() // permitAll(): 모든 권한 허용
.requestMatchers(HttpMethod.POST, "/api/user/**").permitAll()
.requestMatchers(HttpMethod.GET, "/api/board/**", "/api/user/**").permitAll()
.anyRequest().authenticated()
).exceptionHandling(exceptionHandling -> exceptionHandling.authenticationEntryPoint(new FailedAuthenticationEntryPoint()))
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return httpSecurity.build();
}
@Bean
protected CorsConfigurationSource corsConfigrationSourse() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("*"));
configuration.setAllowedMethods(Arrays.asList("GET","POST","PUT","DELETE","PATCH"));
configuration.setAllowedHeaders(Arrays.asList("X-Requested-With", "Content-Type", "Authorization", "X-XSRF-token"));
configuration.setAllowCredentials(false);
configuration.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
class FailedAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
response.setContentType("application/json");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("{\"code:\": \"AF\", \"message\": \"Authorization Failed.\"}");
}
}
1. httpSecurity.cors()
1.1) CORS(Cross-Origin Resource Sharing) 설정
다른 도메인에서 API를 호출할 때 허용 여부를 제어하는 규칙을 설정
- configuration.setAllowedOrigins(Arrays.asList("*"))
- 모든 도메인에서의 요청을 허용
- configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "PATCH"))
- 허용하는 메서드
- configuration.setAllowedHeaders(Arrays.asList("X-Requested-With", "Content-Type", "Authorization", "X-XSRF-token"))
- 클라이언트가 보낼 수 있는 요청 헤더를 지정
- 주로 인증에 필요한 Authorization 헤더 등을 포함
- configuration.setAllowCredentials(false) (링크O)
- 쿠키를 포함한 자격 증명을 허용하지 않도록 설정
- configuration.setMaxAge(3600L)
- 캐싱 가능한 시간(초)을 설정. 3600초(1시간) 동안 CORS 결과를 캐시.
- UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);- 모든 URL 경로에 대해 위의 CORS 설정을 적용
@Bean
protected CorsConfigurationSource corsConfigrationSourse() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("*"));
configuration.setAllowedMethods(Arrays.asList("GET","POST","PUT","DELETE","PATCH"));
configuration.setAllowedHeaders(Arrays.asList("X-Requested-With", "Content-Type", "Authorization", "X-XSRF-token"));
configuration.setAllowCredentials(false);
configuration.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
2. CSRF
CSRF는 사용자가 인증된 상태에서 악의적인 웹사이트가 사용자를 대신해 요청을 보내는 공격
- 공격자가 사용자의 쿠키를 이용해 사용자의 권한으로 서버에 요청을 보냄
- 주로 쿠키 기반 인증에서 문제가 발생
(예: 세션 ID가 쿠키에 저장된 상태에서 쿠키가 자동으로 서버에 전송되는 상황)
2.1) JWT와 CSRF
- JWT는 클라이언트가 명시적으로 헤더에 추가해야 서버가 인증 요청을 처리
- CSRF는 서버가 쿠키를 자동으로 전송하는 점을 악용하는 공격인데, JWT는 쿠키를 사용하지 않아 이러한 취약점에서 자유롭다.
JWT 인증은 쿠키가 아닌 HTTP 헤더(주로 Authorization: Bearer <JWT> 헤더)를 통해 토큰을 전송하기 때문에 CSRF 공격이 어렵다.
2.3) 왜 CSRF를 disable해야 하는가?
CSRF 방어는 쿠키를 기반으로 동작
- Spring Security는 기본적으로 CSRF 보호를 활성화하며, 이를 위해 CSRF 토큰을 쿠키로 전달하고 확인
- 그러나 JWT 방식은 쿠키 기반 세션 관리가 아닌, stateless(상태를 저장하지 않는) 방식으로 동작.
- 따라서 CSRF 보호를 유지하면 불필요한 오버헤드가 생기고, JWT 인증 로직에 혼란을 줄 수 있음
4. Stateless 환경에서 CSRF 방어가 불필요한 이유
- JWT 인증 방식에서는 서버가 클라이언트의 상태를 유지 X
즉, 세션이 없으므로 CSRF 토큰을 검증할 서버 측 데이터가 없음 - 또한, CSRF 공격은 사용자가 신뢰하지 않는 사이트를 방문하여 발생하므로, CORS 정책과 함께 사용하면 추가적인 방어가 가능
3. httpBasic (HttpBasicConfigurer::disable)
- HTTP Basic 인증 방식 비활성화:
- HTTP Basic은 사용자 이름과 비밀번호를 HTTP 요청 헤더에 포함시켜 인증하는 방식
- 이 코드는 Basic 인증 대신 JWT를 사용하기 때문에 이를 비활성화
4. formLogin(FormLoginConfigurer::disable)
폼 로그인 방식 비활성화:
- Spring Security에서 제공하는 기본 로그인 페이지와 로그인 처리를 비활성화
- JWT 기반 인증에서는 클라이언트가 직접 로그인 요청을 처리하고, 토큰을 발급받으므로 필요 없음
5. sessionManagement(sessionManagement -> sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
세션 정책 설정:
- SessionCreationPolicy.STATELESS: 서버가 사용자의 세션을 관리하지 않도록 설정
- JWT는 상태를 유지하지 않는(stateless) 방식으로 동작하므로, 세션을 사용하지 않음
6. authorizeHttpRequests
- authenticated(): 인증된 사용자만 접근을 허용
- fullyAuthenticated(): 완전히 인증된 사용자만 접근을 허용
- hasRole(String role): 특정 역할을 가진 사용자만 접근을 허용
- hasAnyRole(String... roles): 주어진 역할 중 하나라도 가진 사용자만 접근을 허용
- hasAuthority(String authority): 특정 권한을 가진 사용자만 접근을 허용
- hasAnyAuthority(String... authorities): 주어진 권한 중 하나라도 가진 사용자만 접근을 허용
- denyAll(): 모든 접근을 거부
- anonymous(): 익명 사용자만 접근을 허용
- rememberMe(): "Remember Me" 인증을 통해 인증된 사용자만 접근을 허용
.authorizeHttpRequests(request -> request
.requestMatchers("/", "/user/*", "/css/**","/js/**").permitAll() // permitAll(): 모든 권한 허용
.requestMatchers(HttpMethod.POST, "/api/user/**").permitAll()
.requestMatchers(HttpMethod.GET, "/api/board/**", "/api/user/**").hasAnyRole("ADMIN", "USER")
.anyRequest().authenticated()
7. exceptionHandling
- 인증 실패 시 동작을 정의함
- authenticationEntryPoint(new FailedAuthenticationEntryPoint())
- 인증 실패 시 커스텀한 동작을 수행하도록 설정.
- 아래 코드를 예로 들어서 설명하자면, 허용되지 않은 url로 접근을 하거나, 인증이 실패한 경우 "application/json" 타입으로 401 코드와 같이 이미지와 같은 메시지를 출력해줌.
- authenticationEntryPoint(new FailedAuthenticationEntryPoint())
class FailedAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
response.setContentType("application/json");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("{\"code:\": \"AF\", \"message\": \"Authorization Failed.\"}");
}
8. addFilterBefore
JWT 인증 필터를 Spring Security 필터 체인에 추가
- UsernamePasswordAuthenticationFilter 앞에 jwtAuthenticationFilter를 추가하여 JWT 인증을 처리하도록 설정.