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 코드와 같이 이미지와 같은 메시지를 출력해줌.
      •  

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 인증을 처리하도록 설정.