일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |
- PostgreSQL
- chart.js
- VPN
- aws cicd
- codedeploy error
- bootstrap
- kubeflow
- 도커
- Python
- JavaScript
- COALESCE
- node
- Flux
- Jenkins
- docker
- codebuild
- Spring
- chartjs
- Airflow
- SQL
- Spring Error
- IntelliJ
- codedeploy
- aws
- redis
- Kafka
- or some instances in your deployment group are experiencing problems.
- java bigdecimal
- AWS CI/CD 구축하기
- codepipeline
- Today
- Total
Small Asteroid Blog
[Spring] Spring WebFlux에서의 인증 및 인가 처리 본문
WebFlux와 MVC 의 인증 인가 방식
WebFlux 환경에서 API 호출 후, Security Context가 null로 설정되는 현상을 발견했다.
이를 통해 Reactive 환경에서의 인증 및 인가 방식이 MVC와 다르다는 점을 알게 되었다.
Reactive 환경에서의 인증과 인가
Spring Security 의 Reactive 모듈을 사용할 경우, 기존 MVC 환경과 달리 Mono, Flux 기반의 비동기 방식으로 동작한다.
WebFlux에서는 비동기적으로 Security Context를 관리하고 사용자의 인증 및 권한을 수행해야한다.
Security Context 가 null 로 설정되는 이유
Reactive 환경에서는 요청 처리가 비동기적으로 진행되기 때문에,
Security Context 에 값이 적절히 초기화 되지 않으면 null 로 설정될 수 있다.
이는 Security Context 가 동기 방식으로 관리되는 MVC 환경과의 주요 차이점이다.
WebFlux 환경에서 JWT 기반 인증/인가 구현
1. 의존성 추가
spring-boot-starter-webflux를 포함하고, MVC 관련 의존성은 제거한다.
두 방식이 혼재하면 동기와 비동기간의 충돌로 예상치 못한 오류가 발생할 수 있다.
// webflux
implementation 'org.springframework.boot:spring-boot-starter-webflux'
// security
implementation "org.springframework.boot:spring-boot-starter-security"
// Swagger for WebFlux
implementation 'org.springdoc:springdoc-openapi-starter-webflux-ui:2.6.0'
// jwt
implementation group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.12.3'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.12.3'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.12.3'
// mongodb
implementation 'org.springframework.boot:spring-boot-starter-data-mongodb-reactive'
2. Swagger 설정
WebFlux 환경에서도 Swagger를 사용하기 위해 OpenAPI 설정을 추가한다.
import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
import lombok.RequiredArgsConstructor;
import org.springdoc.core.models.GroupedOpenApi;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@RequiredArgsConstructor
public class SwaggerConfig {
@Bean
public OpenAPI openAPI() {
return new OpenAPI()
.info(new Info()
.title("API Documentation")
.description("API description")
.version("v1.0.0"))
// Security Requirement를 추가
.addSecurityItem(new SecurityRequirement().addList("BearerAuth"))
.components(new Components()
.addSecuritySchemes("BearerAuth", new SecurityScheme()
.type(SecurityScheme.Type.HTTP) // HTTP 타입
.scheme("bearer") // Bearer 토큰 사용
.bearerFormat("JWT"))); // JWT 토큰 포맷 명시
}
}
3. JWT 기반 인증 필터 (JwtRequestFilter)
WebFlux에서는 WebFilter를 활용하여 인증 필터를 구현한다.
ReactiveSecurityContextHolder를 이용해 인증 정보를 비동기적으로 관리해야 한다.
import com.fasterxml.jackson.databind.ObjectMapper;
import com.test.config.domain.MemberContext;
import com.test.config.domain.PrincipalDetails;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;
import java.util.Collections;
import java.util.List;
@Slf4j
@Component
@RequiredArgsConstructor
public class JwtRequestFilter implements WebFilter {
private final JwtTokenProvider jwtTokenProvider;
private static final ObjectMapper objectMapper = new ObjectMapper();
/**
* 토큰의 인증정보를 SecurityContext 안에 저장하는 역할 수행
*/
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
String token = getBearerToken(exchange.getRequest());
if (token != null) {
MemberContext memberContext = jwtTokenProvider.getMember(token);
// 로그 출력
log.info("login - token : {}", token);
log.info("login - ldapId : {}", memberContext.getLdapId());
// 권한 설정
String authorityType = memberContext.getAuthorityType();
GrantedAuthority authority = new SimpleGrantedAuthority(authorityType);
PrincipalDetails principalDetails = new PrincipalDetails(memberContext);
memberContext.setMemberRole(authorityType);
Authentication authentication = new UsernamePasswordAuthenticationToken(
principalDetails, null, Collections.singletonList(authority)
);
// Reactor Context에 인증 정보 저장
return chain.filter(exchange)
.contextWrite(ReactiveSecurityContextHolder.withAuthentication(authentication));
}
return chain.filter(exchange);
}
private String getBearerToken(ServerHttpRequest request) {
List<String> authHeaders = request.getHeaders().get(HttpHeaders.AUTHORIZATION);
if (authHeaders != null && !authHeaders.isEmpty() && authHeaders.get(0).startsWith("Bearer ")) {
return authHeaders.get(0).substring(7);
}
return null;
}
}
4. JWT 인증 실패 처리 (JwtAuthenticationEntryPoint)
인증 실패 시 클라이언트에 적절한 오류 응답을 반환하는 역할로 JWT 인증 흐름에서 중요한 부분이다.
import com.fasterxml.jackson.databind.ObjectMapper;
import com.test.config.enums.ErrorCodeEnum;
import com.test.config.response.ErrorResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.server.ServerAuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Component
@Slf4j
public class JwtAuthenticationEntryPoint implements ServerAuthenticationEntryPoint {
private static final ObjectMapper objectMapper = new ObjectMapper();
@Override
public Mono<Void> commence(ServerWebExchange exchange, AuthenticationException authException) {
log.error("Unauthorized access: {}", authException.getMessage());
// ErrorResponse 객체 생성
ErrorResponse errorResponse = new ErrorResponse(ErrorCodeEnum.AUTH_FAIL);
// JSON 응답 생성
String jsonResponse;
try {
jsonResponse = objectMapper.writeValueAsString(errorResponse);
} catch (Exception e) {
log.error("Error while serializing ErrorResponse: {}", e.getMessage());
jsonResponse = "{\"errorCode\":\"INTERNAL_ERROR\",\"errorMessage\":\"An unexpected error occurred\"}";
}
// 응답 설정
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
exchange.getResponse().getHeaders().setContentType(MediaType.APPLICATION_JSON);
byte[] bytes = jsonResponse.getBytes();
// 논블로킹 방식으로 응답 작성
return exchange.getResponse()
.writeWith(Mono.just(exchange.getResponse()
.bufferFactory()
.wrap(bytes)));
}
}
5. 인가 실패 처리 (JwtAccessDeniedHandler)
권한이 없는 사용자가 접근하려고 할 경우 403 Forbidden 응답을 반환한다.
import com.fasterxml.jackson.databind.ObjectMapper;
import com.test.config.enums.ErrorCodeEnum;
import com.test.config.response.ErrorResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.server.authorization.ServerAccessDeniedHandler;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
@Component
@Slf4j
public class JwtAccessDeniedHandler implements ServerAccessDeniedHandler {
private static final ObjectMapper objectMapper = new ObjectMapper();
@Override
public Mono<Void> handle(ServerWebExchange exchange, AccessDeniedException deniedException) {
log.error("Access denied: {}", deniedException.getMessage());
// ErrorResponse 객체 생성
ErrorResponse errorResponse = new ErrorResponse(ErrorCodeEnum.FORBIDDEN);
// JSON 응답 생성
String jsonResponse;
try {
jsonResponse = objectMapper.writeValueAsString(errorResponse);
} catch (Exception e) {
log.error("Error while serializing ErrorResponse: {}", e.getMessage());
jsonResponse = "{\"errorCode\":\"INTERNAL_ERROR\",\"errorMessage\":\"An unexpected error occurred\"}";
}
// 응답 설정
exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
exchange.getResponse().getHeaders().setContentType(MediaType.APPLICATION_JSON);
byte[] bytes = jsonResponse.getBytes();
// 논블로킹 방식으로 응답 작성
return exchange.getResponse()
.writeWith(Mono.just(exchange.getResponse()
.bufferFactory()
.wrap(bytes))); }
}
6. API 경로별 권한 설정 (SecurityWebFilterChain)
WebFlux에서는 SecurityWebFilterChain을 사용하여 권한을 설정합니다.
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.SecurityWebFiltersOrder;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.web.server.SecurityWebFilterChain;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
@Slf4j
@Configuration
@EnableWebFluxSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtRequestFilter jwtRequestFilter;
private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
private final JwtAccessDeniedHandler jwtAccessDeniedHandler;
// whiteList 적용
private static final String[] ALL_WHITELIST = {
"/api/health_check", "/api/v1/auth/login",
"/swagger", "/swagger-resources/**", "/v3/api-docs/**", "/swagger-ui/**"
};
@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
return http
.csrf(ServerHttpSecurity.CsrfSpec::disable)
.authorizeExchange(auth -> auth
.pathMatchers(ALL_WHITELIST).permitAll()
.anyExchange().authenticated()
)
.addFilterAt(jwtRequestFilter, SecurityWebFiltersOrder.AUTHENTICATION)
.exceptionHandling(exceptions -> exceptions
.authenticationEntryPoint(jwtAuthenticationEntryPoint)
.accessDeniedHandler(jwtAccessDeniedHandler)
)
.build();
}
@Bean
public CorsWebFilter corsWebFilter() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.addAllowedOriginPattern("*"); // 모든 Origin 허용 (배포 시 제한 필요)
configuration.addAllowedMethod("*"); // 모든 HTTP 메서드 허용
configuration.addAllowedHeader("*"); // 모든 헤더 허용
configuration.setAllowCredentials(true); // 자격 증명 포함 허용
configuration.setMaxAge(600L); // PreFlight 요청 캐시 시간 (초 단위)
// 노출할 헤더 설정
configuration.addExposedHeader("Content-Disposition");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return new CorsWebFilter(source);
}
}
7. block() 호출이 Reactive 흐름에 미치는 영향
WebFlux에서는 block()을 사용하면 Reactive 스트림의 비동기 흐름이 깨지고, Security Context가 손실될 수 있다.
따라서 모든 인증 및 권한 관리는 Mono 또는 Flux를 활용한 비동기 방식으로 처리해야 한다.
@Operation(summary = "menu 정보 조회", description = "menu 정보 조회")
@GetMapping("/menu")
public Mono<ResponseEntity<BaseResponse>> getMenuList(
@AuthenticationPrincipal PrincipalDetails principalDetails,
ServerHttpRequest request
) {
return menuService.getSubMenuIdAndSearchFields()
.map(data -> new ResponseEntity<>(new BaseResponse(data), HttpStatus.OK));
}
WebFlux 환경에서는 인증 및 인가 로직이 비동기적으로 동작해야 하며, block() 호출을 피해야 한다.
이를 고려하여 Reactor Context를 활용한 인증 처리가 필요하다.
'백엔드 > Spring' 카테고리의 다른 글
[Spring] MongoDB 와 Redis 트랜잭션의 한계 와 대안 (0) | 2024.11.29 |
---|---|
[Spring] Spring Web MVC, WebFlux 의존성 동시 사용 (0) | 2024.11.28 |
[Spring] Reactive MongoDB Stream의 Flux, Mono 조회 방법 비교 (0) | 2024.11.22 |
[Spring] ReactiveMongoTemplate vs ReactiveMongoRepository (0) | 2024.11.07 |
[Spring] MongoDB 조회 - Stream, Aggregation 비교 (0) | 2024.11.04 |