본문 바로가기
Spring

Spring Security와 JWT

by 슈슈슉민 2024. 6. 10.
package com.softnet.oceanback.config;

import com.softnet.oceanback.config.jwt.JwtAuthenticationEntryPoint;
import com.softnet.oceanback.config.jwt.JwtSecurityConfig;
import com.softnet.oceanback.config.jwt.TokenProvider;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import java.util.List;

@RequiredArgsConstructor
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class WebSecurityConfig {

    private final TokenProvider tokenProvider;
    private final JwtAuthenticationEntryPoint entryPoint;

    private static final String[] AUTH_WHITELIST = {
            "/error",
            "/favicon.ico",
            "/user/login"
    };

    private static final String[] AUTH_ADMIN = {
            "/admin/**"
    };

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }


    @Bean
    public static PasswordEncoder passwordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }



    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .httpBasic(AbstractHttpConfigurer::disable)
                .cors(httpSecurityCorsConfigurer -> corsConfigurationSource())
                .csrf(AbstractHttpConfigurer::disable)
                .sessionManagement(sessionManagement ->
                        sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS))

//                .exceptionHandling((exception ->
//                        exception.authenticationEntryPoint(jwtAuthenticationEntryPoint).accessDeniedHandler(jwtAccessDeniedHandler)))
                .authorizeHttpRequests(request ->
                        request.requestMatchers(AUTH_WHITELIST).permitAll()
                                .requestMatchers(AUTH_ADMIN).hasRole("ADMIN")
                               .anyRequest()
                               .authenticated()
                )
                .logout(request -> request.logoutUrl("/logout")
                        .logoutSuccessUrl("/user/logout")
                )
                .addFilterBefore(new ExceptionHandlerFilter(), UsernamePasswordAuthenticationFilter.class)
                .exceptionHandling(handler -> handler.authenticationEntryPoint(entryPoint))
                .apply(new JwtSecurityConfig(tokenProvider));

        return http.build();
    }

    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration config = new CorsConfiguration();

        config.setAllowCredentials(true);
        config.setAllowedOrigins(List.of("http://localhost:3000"));
        config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"));
        config.setAllowedHeaders(List.of("*"));
        config.setExposedHeaders(List.of("*"));

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", config);
        return source;
    }
}

 

이 코드에서는 JWT(JSON Web Token)를 사용한 인증 및 인가 프로세스를 구현하고 있다. 위에서 설명한 순서와 연계하여 해당 코드에서의 인증 프로세스를 설명하면 다음과 같다.

코드와 순서도를 번갈아보며 설명을 읽기를 바란다.

  1. HttpRequest가 들어오면 CorsFilter가 요청을 가로챈다. 여기서 CORS 설정이 적용된다.
  2. JwtFilter가 FilterChain 상에서 작동한다. 여기서 JWT 토큰 유효성 검증이 이루어진다.
  3. JWT 토큰이 유효하다면 JwtAuthenticationToken이 생성된다.
  4. DaoAuthenticationProvider와 CasAuthenticationProvider가 인증을 처리한다. AuthenticationFilter가 인증 요청을 처리한다.
  5. AuthenticationManager의 authenticate() 메서드가 호출되어 인증을 수행한다.
  6. ProviderManager가 적절한 AuthenticationProvider를 선택를 선택하는데  이 때, JwtAuthenticationProvider를 찾아 해당 Provider의 authenticate() 메서드를 호출한다.
  7. JwtAuthenticationProvider는 UserDetailsService (InMemoryUserDetailsService 또는 CustomUserDetailsService) 의 loadUserByUsername() 메서드를 호출하여 UserDetails 객체를 가져온다.
  8. JwtAuthenticationProvider는 UserDetails 객체와 JwtAuthenticationToken의 자격 증명을 비교하여 인증 여부를 결정한다. 인증 결과는 SecurityContext에 저장된다.
  9. 인증이 성공하면 JwtAuthenticationProvider는 인증된 JwtAuthenticationToken을 반환한다.
  10. AuthenticationManager는 인증된 JwtAuthenticationToken을 FilterChain에 전달한다.
  11. FilterChain의 나머지 필터들이 계속 실행된다.
  12. 최종적으로 요청에 대한 인가 처리가 이루어진다. 여기서는 /admin/** 경로에 대해 ROLE_ADMIN 권한을 가진 사용자만 접근할 수 있도록 설정되어 있다.

 이 코드에서 주목할 점은 JwtSecurityConfig와 JwtAuthenticationProvider를 통해 JWT 토큰 기반 인증을 구현하고 있다는 것이다. 또한 ExceptionHandlerFilter를 통해 예외 처리를 하고 있으며, 인증 실패 시 JwtAuthenticationEntryPoint가 호출되어 적절한 응답을 반환한다.

 

JwtAuthenticationEntryPoint는 Spring Security에서 인증 예외(Authentication Exception)가 발생했을 때 실행되는 컴포넌트이다. 이 컴포넌트의 주요 역할은 클라이언트에게 적절한 응답(Response)을 제공하는 것이다. 예를 들어, 인증 실패 시 HTTP 상태 코드(이경우 401)와 에러 메시지를 클라이언트에게 반환하게 된다. JwtAuthenticationEntryPoint는 AuthenticationEntryPoint 인터페이스를 구현하며, commence 메서드를 오버라이드한다. JwtAuthenticationEntryPoint는 일반적으로 HttpSecurity 객체를 통해 설정된다. 

 

package com.softnet.oceanback.config.jwt;

import com.softnet.oceanback.exception.GlobalExceptionHandler;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerExceptionResolver;

import java.io.IOException;

@Component
@Slf4j
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
    private final HandlerExceptionResolver resolver;
    private final GlobalExceptionHandler exceptionHandler;

    public JwtAuthenticationEntryPoint(@Qualifier("handlerExceptionResolver") HandlerExceptionResolver resolver, RequestEndpointChecker endpointChecker, GlobalExceptionHandler globalExceptionHandler, GlobalExceptionHandler exceptionHandler) {
        this.resolver = resolver;
        this.exceptionHandler = exceptionHandler;
    }
 
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
        authException.printStackTrace();
        StackTraceElement[] stackTrace = authException.getStackTrace();
        for (StackTraceElement stackTraceElement : stackTrace) {
            log.error(stackTraceElement.toString());
        }
        log.error(authException.getMessage());
        resolver.resolveException(request, response, exceptionHandler, (Exception) request.getAttribute("Exception"));
    }
}

 

 

reference[https://www.javainuse.com/webseries/spring-security-jwt/chap3]

 

Understand Spring Security Architecture and implement Spring Boot Security | JavaInUse

 

www.javainuse.com

'Spring' 카테고리의 다른 글

Spring Boot 와 Oauth2 구축  (2) 2024.06.10
Spring Security Architecture 이해하기  (2) 2024.06.10
[토비의 스프링] 스프링의 이해와 원리  (1) 2024.03.23
static 과 Bean  (0) 2024.02.15