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)를 사용한 인증 및 인가 프로세스를 구현하고 있다. 위에서 설명한 순서와 연계하여 해당 코드에서의 인증 프로세스를 설명하면 다음과 같다.
- HttpRequest가 들어오면 CorsFilter가 요청을 가로챈다. 여기서 CORS 설정이 적용된다.
- JwtFilter가 FilterChain 상에서 작동한다. 여기서 JWT 토큰 유효성 검증이 이루어진다.
- JWT 토큰이 유효하다면 JwtAuthenticationToken이 생성된다.
- DaoAuthenticationProvider와 CasAuthenticationProvider가 인증을 처리한다. AuthenticationFilter가 인증 요청을 처리한다.
- AuthenticationManager의 authenticate() 메서드가 호출되어 인증을 수행한다.
- ProviderManager가 적절한 AuthenticationProvider를 선택를 선택하는데 이 때, JwtAuthenticationProvider를 찾아 해당 Provider의 authenticate() 메서드를 호출한다.
- JwtAuthenticationProvider는 UserDetailsService (InMemoryUserDetailsService 또는 CustomUserDetailsService) 의 loadUserByUsername() 메서드를 호출하여 UserDetails 객체를 가져온다.
- JwtAuthenticationProvider는 UserDetails 객체와 JwtAuthenticationToken의 자격 증명을 비교하여 인증 여부를 결정한다. 인증 결과는 SecurityContext에 저장된다.
- 인증이 성공하면 JwtAuthenticationProvider는 인증된 JwtAuthenticationToken을 반환한다.
- AuthenticationManager는 인증된 JwtAuthenticationToken을 FilterChain에 전달한다.
- FilterChain의 나머지 필터들이 계속 실행된다.
- 최종적으로 요청에 대한 인가 처리가 이루어진다. 여기서는 /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 |