상황 : webflux 기반 서버는 기존의 mvc 서버와 다르게 설정이 필요하다.
SecurityConfig
@Configuration
@EnableWebFluxSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final JWTUtil jwtUtil;
private final RedisService redisService;
@Bean
public SecurityWebFilterChain securityFilterChain(ServerHttpSecurity http) {
http
.csrf(ServerHttpSecurity.CsrfSpec::disable);
http
.formLogin(ServerHttpSecurity.FormLoginSpec::disable);
http
.httpBasic(ServerHttpSecurity.HttpBasicSpec::disable);
http
.securityContextRepository(NoOpServerSecurityContextRepository.getInstance()); // session stateless 설정 방법
http
.authorizeExchange(auth -> auth
.pathMatchers("/api/users/test").permitAll()
.pathMatchers("/api/users/login").permitAll()
.pathMatchers(HttpMethod.POST, "/api/users").permitAll()
.pathMatchers(HttpMethod.GET, "/api/users").permitAll()
.anyExchange().authenticated());
http
.addFilterBefore(new JWTFilter(jwtUtil), SecurityWebFiltersOrder.AUTHENTICATION)
.addFilterBefore(new ReissueFilter(jwtUtil, redisService),
SecurityWebFiltersOrder.AUTHENTICATION)
.addFilterBefore(new CustomLogoutFilter(jwtUtil, redisService),
SecurityWebFiltersOrder.LOGOUT);
return http.build();
}
}
JWTFilter
public class JWTFilter implements WebFilter {
private final JWTUtil jwtUtil;
public JWTFilter(JWTUtil jwtUtil) {
this.jwtUtil = jwtUtil;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
String accessToken = request.getHeaders().getFirst("Authorization");
if (ObjectUtils.isEmpty(accessToken)) {
return chain.filter(exchange);
}
String path = request.getURI().getPath();
HttpMethod httpMethod = request.getMethod();
try {
jwtUtil.isExpired(accessToken);
} catch (ExpiredJwtException e) {
if (path.equals("/reissue") && httpMethod == HttpMethod.POST) {
exchange.getAttributes().put("reissue", true);
return chain.filter(exchange);
} else {
throw new RuntimeException("access token expired");
}
}
String category = jwtUtil.getCategory(accessToken);
if (!category.equals("access")) {
throw new RuntimeException("This is Invalid Token");
}
Long userId = jwtUtil.getUserId(accessToken);
String role = jwtUtil.getRole(accessToken);
List<GrantedAuthority> roles = new ArrayList<>();
roles.add(new SimpleGrantedAuthority(role));
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(userId, null, roles);
SecurityContext securityContext = new SecurityContextImpl(authenticationToken);
if (checkNeedUserIdPath(path, httpMethod)) {
request.mutate().header("X-User-Id", String.valueOf(userId));
}
return chain.filter(exchange).contextWrite(ReactiveSecurityContextHolder
.withSecurityContext(Mono.just(securityContext)));
}
private boolean checkNeedUserIdPath(String path, HttpMethod method) {
return path.equals("/api/users") && method.equals(HttpMethod.PATCH);
}
}
ReissueTokenFilter
public class ReissueFilter implements WebFilter {
private final JWTUtil jwtUtil;
private final RedisService redisService;
public ReissueFilter(JWTUtil jwtUtil, RedisService redisService) {
this.jwtUtil = jwtUtil;
this.redisService = redisService;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
Boolean reissue = exchange.getAttribute("reissue");
if (reissue!=null && reissue) {
String refresh = exchange.getRequest().getHeaders().getFirst("Refresh");
if (refresh == null) {
throw new RuntimeException("This is Invalid Token");
}
try {
jwtUtil.isExpired(refresh);
} catch (ExpiredJwtException e) {
throw new RuntimeException("This is Invalid Token");
}
String category = jwtUtil.getCategory(refresh);
if (!category.equals("refresh")) {
throw new RuntimeException("This is Invalid Token");
}
Long userId = jwtUtil.getUserId(refresh);
String role = jwtUtil.getRole(refresh);
if(!Objects.equals(redisService.getValue(RedisKeyPrefix.REFRESH,userId), refresh)){
throw new RuntimeException("This is Invalid Token");
}
ServerHttpResponse response = exchange.getResponse();
String newAccess = jwtUtil.createToken("access", userId, role, 600000L);
response.getHeaders().add("Authorization", newAccess);
response.setStatusCode(HttpStatus.OK);
return Mono.empty();
}
return chain.filter(exchange);
}
}
LogoutFilter
public class CustomLogoutFilter implements WebFilter{
private final JWTUtil jwtUtil;
private final RedisService redisService;
public CustomLogoutFilter(JWTUtil jwtUtil, RedisService redisService) {
this.jwtUtil = jwtUtil;
this.redisService = redisService;
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
String path = request.getURI().getPath();
HttpMethod httpMethod = request.getMethod();
if(path.equals("/logout")&&httpMethod==HttpMethod.POST){
String refresh = request.getHeaders().getFirst("Refresh");
if(!ObjectUtils.isEmpty(refresh)){
Long userId = jwtUtil.getUserId(refresh);
redisService.deleteKey(RedisKeyPrefix.REFRESH, userId);
}
ServerHttpResponse response = exchange.getResponse();
HttpHeaders headers = response.getHeaders();
response.setStatusCode(HttpStatus.OK);
headers.set("Authorization", null);
headers.set("Refresh", null);
return Mono.empty();
}
return chain.filter(exchange);
}
}
'Spring Boot > MSA' 카테고리의 다른 글
exception handling filter in gateway-webflux at spring security (0) | 2024.10.06 |
---|---|
jpa 연관관계 설정 in msa (0) | 2024.09.29 |
마이크로서비스로의 전환 (feat. 도메인 주도 설계) (0) | 2024.09.25 |
데이터베이스 in MSA (0) | 2024.09.24 |
마이크로서비스 핵심 원칙 (0) | 2024.09.23 |