Gateway에서 1차 토큰 검증
목차
1. Gateway에서의 1차 검증 개요
Spring Cloud Gateway를 활용하여 각 마이크로서비스로 라우팅하기 전 JWT 토큰의 존재 여부를 검증하는 방법 구현 예시입니다.
2. 검증 구조
1
2
3
4
5
6
7
Client Request
↓
Gateway (JWT 토큰 존재 확인)
↓
라우팅 필터 적용
↓
각 마이크로서비스 (실제 토큰 유효성 검증)
3. 주요 구성 요소
TokenProvider - JWT 토큰 생성 및 관리
JWT 토큰의 생성, 추출, 검증을 담당하는 서비스
핵심 기능
- 토큰 생성: 사용자 정보를 기반으로 JWT 토큰 생성
- 토큰 추출: HTTP 헤더 또는 쿠키에서 토큰 추출
- 토큰 검증: 토큰의 유효성 검증 (실제로는 각 서비스에서 수행)
주요 코드
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
...
public class TokenProvider {
public static String AUTH_HEADER = "Authorization";
private static String SUBJECT_TITLE = "your service test";
// JWT 토큰 생성
public String createToken(MemberVM memberInfo) {
Date now = Date.from(Instant.now());
// 만료기한 : 토큰 생성 일시로부터 2시간 후
Date expireDate = Date.from(Instant.now().plus(2, ChronoUnit.HOURS));
Claims claims = Jwts.claims();
claims.put(Auth.ROLE_KEY, memberInfo.getRole());
claims.put(Auth.USERNAME_KEY, memberInfo.getName());
claims.put(Auth.ADDR_KEY, memberInfo.getAddress());
return Jwts.builder()
.setSubject(SUBJECT_TITLE)
.setClaims(claims)
.signWith(genKey())
.setIssuer(MAIN_ISSUER)
.setIssuedAt(now)
.setExpiration(expireDate)
.compact();
}
// 토큰 존재 여부 확인
public boolean validateToken(ServerHttpRequest request) {
String bearerToken = request.getHeaders().getFirst(AUTH_HEADER);
return bearerToken != null;
}
// 토큰 추출 (헤더 또는 쿠키에서)
// gateway 서비스에선 사용 X, 각 서비스에서 필요한 코드
public String extractToken(ServerHttpRequest request) {
String bearerToken = request.getHeaders().getFirst(AUTH_HEADER);
if (StringUtils.hasText(bearerToken)) {
if (bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return bearerToken;
}
// 쿠키에서 토큰 확인
MultiValueMap<String, HttpCookie> cookies = request.getCookies();
// ... 쿠키 처리 로직
return null;
}
}
AuthGlobalFilter - JWT 토큰 존재 확인 필터
Gateway에서 라우팅 전 JWT 토큰의 존재 여부만 확인하는 커스텀 필터
핵심 기능
- 토큰 존재 확인: Authorization 헤더에 토큰이 있는지 확인
- 인증 생략 경로 설정: 특정 경로는 토큰 검증을 생략 (로그인, 회원가입 등)
- 환경별 설정: Properties 파일을 통해 환경별로 인증 생략 경로 관리
주요 코드
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
...
public class AuthGlobalFilter extends AbstractGatewayFilterFactory<AuthGlobalFilter.Config> {
// application.properties에 작성한 설정 값
// Application Context Provider로 해당 값 가져옴
private static String IGNORE_PATH_KEY = "cloud.gateway.ignore-auth-paths";
public static class Config {
// 필터에 필요한 추가 설정 정의
// 테스트 시엔 공백으로
private List<String> ignorePaths;
public List<String> getIgnorePaths() {
return ignorePaths;
}
public void setIgnorePaths(List<String> ignorePaths) {
this.ignorePaths = ignorePaths;
}
}
public AuthGlobalFilter() {
super(Config.class);
}
@Override
public Config newConfig() {
// Spring 환경 변수에서 Config에 필요한 값 읽어오기
Environment env = ApplicationContextProvider.getEnvironment();
String raw = env.getProperty(IGNORE_PATH_KEY);
List<String> paths = (raw != null && !raw.isBlank())
? Arrays.asList(raw.trim().split(","))
: List.of();
Config config = new Config();
config.setIgnorePaths(paths);
return config;
}
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
String path = exchange.getRequest().getURI().getPath();
List<String> ignorePaths = config.getIgnorePaths();
// 인증 생략 경로 확인
if (ignorePaths.stream().anyMatch(path::startsWith)) {
return chain.filter(exchange);
}
// Authorization 헤더에서 토큰 확인
String token = exchange.getRequest()
.getHeaders()
.getFirst(TokenProvider.AUTH_HEADER);
// 토큰이 없으면 예외 발생
if (token == null) {
throw new GatewayException(ErrorCode.UNAUTHORIZED);
}
return chain.filter(exchange);
};
}
}
RouteConfig - 라우팅 설정 및 필터 적용
마이크로서비스별 라우팅 규칙을 정의하고 AuthGlobalFilter를 적용
이전 게시글의 RouteConfig를 수정
핵심 기능
- 라우팅 정의: 각 서비스별 경로 설정
- 필터 적용: 인증 필터, Circuit Breaker 등 적용
- 경로 재작성: URL 패턴 변환
주요 코드
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
31
32
@Configuration
@RequiredArgsConstructor
public class RouteConfig {
private final AuthGlobalFilter authFilter;
@Bean
public RouteLocator gatewayRoutes(RouteLocatorBuilder builder) {
return builder.routes()
// 인증이 필요한 경로
.route("admin-service-auth", r -> r.path(adminServiceAuthPath)
.filters(f -> f
.addRequestHeader(CALL_ORIGIN_NAME, CALL_ORIGIN_VALUE)
.rewritePath(adminRewritePtn, REWRITE_REPLACE_STRING)
.filter(authFilter.apply(new AuthGlobalFilter.Config()))
.circuitBreaker(c -> c
.setName("adminServiceCircuitBreaker")
.setFallbackUri("forward:/fallback/admin-service")))
.uri(adminUri))
// 인증이 필요 없는 경로
.route("admin-service", r -> r.path(adminServicePath)
.filters(f -> f
.addRequestHeader(CALL_ORIGIN_NAME, CALL_ORIGIN_VALUE)
.rewritePath(adminRewritePtn, REWRITE_REPLACE_STRING)
.circuitBreaker(c -> c
.setName("adminServiceCircuitBreaker")
.setFallbackUri("forward:/fallback/admin-service")))
.uri(adminUri))
.build();
}
}
4. 동작 흐름
1. 인증이 필요한 요청
sequenceDiagram
participant Client
participant Gateway
participant AuthFilter as AuthGlobalFilter
participant AdminService as Admin Service
Client->>Gateway: GET /api/admin/users<br/>Authorization: Bearer <token>
Gateway->>AuthFilter: 요청 검증
Note over AuthFilter: 1. 경로 확인: /api/admin/users<br/>2. ignore-auth-paths 확인: 해당 없음<br/>3. Authorization 헤더 확인: 존재 ✓
AuthFilter->>Gateway: 검증 통과
Gateway->>AdminService: GET /users<br/>Authorization: Bearer <token><br/>call-origin: gateway-service
Note over AdminService: 1. 토큰 유효성 검증<br/>2. Claims 파싱<br/>3. 권한 확인<br/>4. 비즈니스 로직 처리
AdminService-->>Gateway: 200 OK<br/>응답 데이터
Gateway-->>Client: 200 OK<br/>응답 데이터
설명:
- 클라이언트가 Authorization 헤더에 JWT 토큰을 포함하여 요청
- AuthGlobalFilter가 토큰의 존재 여부만 확인
- Gateway가 요청을 Admin Service로 라우팅
- Admin Service에서 실제 토큰 유효성 검증 및 비즈니스 로직 수행
2. 인증이 생략되는 요청
sequenceDiagram
participant Client
participant Gateway
participant AuthFilter as AuthGlobalFilter
participant AuthService as Auth Service
Client->>Gateway: POST /api/login<br/>Body: {address, password}
Gateway->>AuthFilter: 요청 검증
Note over AuthFilter: 1. 경로 확인: /api/login<br/>2. ignore-auth-paths 확인: 포함됨 ✓<br/>3. 토큰 검증 생략
AuthFilter->>Gateway: 검증 생략하고 통과
Gateway->>AuthService: POST /login<br/>call-origin: gateway-service<br/>Body: {address, password}
Note over AuthService: 1. 사용자 정보 조회<br/>2. 비밀번호 검증<br/>3. JWT 토큰 생성
AuthService-->>Gateway: 200 OK<br/>{name, role, token}
Gateway-->>Client: 200 OK<br/>{name, role, token}
설명:
- 클라이언트가 로그인 요청 (토큰 없이)
- AuthGlobalFilter가
/api/login경로를 ignore-auth-paths에서 확인 - 토큰 검증을 생략하고 Auth Service로 라우팅
- Auth Service에서 사용자 인증 후 JWT 토큰 생성 및 반환
3. 인증 실패 케이스
sequenceDiagram
participant Client
participant Gateway
participant AuthFilter as AuthGlobalFilter
Client->>Gateway: GET /api/admin/users<br/>(Authorization 헤더 없음)
Gateway->>AuthFilter: 요청 검증
Note over AuthFilter: 1. 경로 확인: /api/admin/users<br/>2. ignore-auth-paths 확인: 해당 없음<br/>3. Authorization 헤더 확인: 없음 ✗
AuthFilter->>AuthFilter: GatewayException(UNAUTHORIZED) 발생
AuthFilter-->>Gateway: 예외 전파
Gateway-->>Client: 401 Unauthorized<br/>{error: "UNAUTHORIZED"}
설명:
- 클라이언트가 토큰 없이 인증이 필요한 경로 요청
- AuthGlobalFilter가 Authorization 헤더가 없음을 감지
- GatewayException(UNAUTHORIZED) 발생
- 클라이언트에게 401 Unauthorized 응답 반환
5. 참고 자료
이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.