MSA 아키텍처에서는 각 마이크로서비스마다 인증 로직을 반복 구현해줘야 하는데, 이러한 단점을 보완하며 모든 서비스에 동일한 인증 및 권한 부여 정책을 적용하기위해 게이트웨이 패턴을 적용하기로 했다.
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
package com.nhnacademy.gateway.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.web.server.SecurityWebFilterChain;
@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {
@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http){
return http
.csrf(ServerHttpSecurity.CsrfSpec::disable)
.authorizeExchange(exchanges -> exchanges
.pathMatchers("/api/auth/**", "/login", "/register", "/css/**", "/js/**")
.permitAll()
.anyExchange().authenticated()
)
.build();
}
}
💡 리액티브 환경
비동기&논블로킹 방식으로 동작하는 환경
요청이 들어와도 쓰레드를 계속 점유하지 않고, 다른 요청을 처리할 수 있음🛠 블로킹 vs 논블로킹 비교
1️⃣ 블로킹 I/O (Spring MVC 환경)
@GetMapping("/user") public String getUser() { return userService.getUser(); // DB에서 데이터를 가져올 때까지 스레드 대기 (Blocking) }요청이 들어오면 getUser() 실행 후 완료될 때까지 스레드가 블로킹됨.
동시 요청이 많아지면 스레드가 부족해지고 성능 저하 발생.
2️⃣ 논블로킹 I/O (Spring WebFlux 환경)@GetMapping("/user") public Mono<String> getUser() { return userService.getUser(); // DB에서 데이터 가져오는 동안 스레드는 다른 작업 수행 (Non-blocking) }요청을 받으면 바로 Mono을 반환하고 스레드는 다른 요청을 처리.
DB 응답이 오면 그때 결과를 전달.
@Component
public class JwtAuthenticationFilter implements GlobalFilter, Ordered {
private final JwtProvider jwtProvider;
private final List<String> allowedPaths;
public JwtAuthenticationFilter(JwtProvider jwtProvider) {
this.jwtProvider = jwtProvider;
this.allowedPaths = List.of("/api/auth/login", "/api/auth/register", "/login", "/register");
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
return null;
}
@Override
public int getOrder() {
return 0;
}
}
생성자
import org.springframework.http.server.reactive.ServerHttpRequest;
Spring-Cloud-Gateway는 리액티브 기반이기에 패키지 클래스 임포트
package com.nhnacademy.gateway.security;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.security.Key;
@Component
public class JwtProvider {
private final Key key;
public JwtProvider(@Value("${jwt.secret}") String secretKey) {
byte[] keyBytes = secretKey.getBytes();
this.key = Keys.hmacShaKeyFor(keyBytes);
}
// 토큰 검증
public boolean validateToken(String token) {
try {
Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token);
return true;
} catch (Exception e) {
return false;
}
}
// 토큰에서 이메일 추출
public String getEmailFromToken(String token) {
Claims claims = Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
.getBody();
return claims.getSubject();
}
}
filter 메서드는 Spring Cloud Gateway에서 모든 요청이 통과하는 필터 로직을 구현
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain)
파라미터로 웹 요청과 응답에 대한 컨테이너인 SeverWebExchange, 필터 체인을 위한 파라미터
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 1. 요청 정보 추출
ServerHttpRequest request = exchange.getRequest(); // Spring WebFlux에서 HTTP 요청 정보를 담고 있는 객체
String path = request.getURI().getPath(); // 요청 URL에서 경로 부분만 추출 ("/api/auth/login")
// 2. 인증 예외 경로 확인
if(isAllowedPath(path)){
return chain.filter(exchange); // 인증 필요없는 경로면 다음 필터로 요청 전달
}
// 3. 정적 리소스 확인
if (path.startsWith("/css/") || path.startsWith("/js/")) {
return chain.filter(exchange);
}
// 4. JWT 토큰 추출
String token = extractToken(request); // Authorization" 헤더에서 "Bearer " 다음에 오는 문자열을 토큰으로 추출
// 5. 토큰 검증 및 오류 처리
if (token == null || !jwtProvider.validateToken(token)) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
// 6. 사용자 정보 추가
String email = jwtProvider.getEmailFromToken(token);
ServerHttpRequest modifiedRequest = request.mutate()
.header("X-User-Email", email)
.build();
// 7. 수정된 요청 전달
return chain.filter(exchange.mutate().request(modifiedRequest).build());
}
@Override
public int getOrder() {
return -1;
}
이제 JWT 인증필터가 게이트웨이에 구현되었기에
백엔드 서비스는 게이트웨이가 추가한 X-User-Email 헤더를 통해 사용자를 식별할 수 있으며, 추가적인 권한 검사나 비즈니스 로직에 집중할 수 있게 됨