사용자에게 특정 리소스나 기능에 액세스할 수 있는 권한을 부여하는 프로세스
💡 Authorization vs Authentication
- Authentication (인증)
본인이 누구인지 확인하는 것 (ex. 로그인)- Authorization (인가)
특정 리소스에 권한이 있는지 확인 (ex. 등급 권한)
🔗 출처: 쉽게 이해하는 Authentication vs Authorization 차이
➡ API Gateway에 AuthorizationHeaderFilter를 사용하여 인가(Authorization) 과정 추가
<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>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
</dependency>
package com.example.apigatewayservice.config;
import io.jsonwebtoken.JwtParser;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpHeaders;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.core.env.Environment;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
@Component
@Slf4j
public class AuthorizationHeaderFilter extends AbstractGatewayFilterFactory<AuthorizationHeaderFilter.Config> {
Environment env;
public AuthorizationHeaderFilter(Environment env) {
super(Config.class);
this.env = env;
}
public static class Config {
}
@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
ServerHttpRequest request = exchange.getRequest();
if(!request.getHeaders().containsKey(HttpHeaders.AUTHORIZATION)) { //정상적으로 인증이 됐는지 여부 판단
return onError(exchange, "No authorization header", HttpStatus.UNAUTHORIZED);
}
String authorizationHeader = request.getHeaders().get(HttpHeaders.AUTHORIZATION).get(0);
String jwt = authorizationHeader.replace("Bearer ", "");
if(!isJwtValid(jwt)){
return onError(exchange, "JWT token is not valid", HttpStatus.UNAUTHORIZED);
}
return chain.filter(exchange);
};
}
private boolean isJwtValid(String jwt) {
byte[] secretKeyBytes = Base64.getEncoder().encode(env.getProperty("token.secret").getBytes());
SecretKey signingKey = new SecretKeySpec(secretKeyBytes, SignatureAlgorithm.HS512.getJcaName());
boolean returnValue = true;
String subject = null;
try {
JwtParser jwtParser = Jwts.parserBuilder()
.setSigningKey(signingKey)
.build();
subject = jwtParser.parseClaimsJws(jwt).getBody().getSubject();
} catch (Exception ex) {
returnValue = false;
}
if (subject == null || subject.isEmpty()) {
returnValue = false;
}
return returnValue;
}
private Mono<Void> onError(ServerWebExchange exchange, String err, HttpStatus httpStatus) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(httpStatus);
log.error(err);
return response.setComplete();
}
}
AbstractGatewayFilterFactory<AuthorizationHeaderFilter.Config>
Spring Cloud Gateway에서 커스텀 필터를 만들 때 사용하는 클래스
GatewayFilter를 구현하는데 필요한 구조 제공하고 필터의 설정 정보(Config)를 관리하는데 사용
- 첫번째 파라미터(
AuthorizationHeaderFilter.Config)
필터의 설정 정보를 담을 클래스 타입- 두번째 파라미터 (
void또는Empty)
기본적으로는 사용하지 않거나, 설정 정보가 없는 경우에 사용AuthorizationHeaderFilter.Config
해당 필터에 대한 설정 정보를 담는 클래스
⭐
apply()메서드
GatewayFilter인터페이스를 구현한 메서드로, 필터가 적용될 때 호출됨
exchange.getRequest()
➡ 현재 HTTP 요청 객체 가져옴request.getHeaders().containsKey(HttpHeaders.AUTHORIZATION)
➡ 요청 헤더에Authorization키가 포함되어 있는지 확인Authorizaiton헤더가 없으면,onError()메서드 호출
➡ HTTP 401 Unauthorized 상태 코드와 함께 메시지 반환request.getHeaders().get(HttpHeaders.AUTHORIZATION).get(0)
➡Authorizaiton헤더에서 첫번째 값을 가져옴
➡ JWT 토큰은Bearer라는 접두사를 포함하여 전송되기 때문에, 이를replace로 제거하여 실제 JWT 토큰만 추출isJwtValid(jwt)
➡ JWT 토큰을 검증하여, 이 메서드의 결과가false인 경우onError()메서드 호출
➡ HTTP 401 Unauthorized 상태 코드와 함께 메시지 반환return chain.filter(exchange)
➡ JWT가 유효한 경우에 요청을 후속 필터나 처리기로 넘겨, 요청이 정상적으로 처리됨
⭐
isJwtValid()메서드
JWT(Json Web Token)가 유효한지 검증하는 함수
- 서명 키(JWT 검증 시 사용) 준비
1.env.getProperty("token.secret").getBytes()를 사용해서 비밀 키 가져옴
2. 비밀 키는 Base64 인코딩이 되어 있기 때문에, 먼저getBytes()를 통해 바이트 배열로 변환 후 Base64로 다시 인코딩
3.SecretKeySpec을 사용하여signingKey객체 생성
➡ 이 객체는 JWT를 검증하는데 사용되며, 서명 알고리즘은HS512임- JWT Parser 초기화 및 파싱
1.JwtParser: JWT를 파싱하고 서명을 검증하는데 사용
2.setSigningKey(signingKey): 서명 검증에 사용할 비밀 키 설정
3.build(): JWT 파서 생성
✅ 파싱(parsing) : 어떤 큰 자료에서 원하는 정보만 가공하고 뽑아서 원하는 때에 불러올 수 있게 하는 것
✅ 파서(parser) : 파싱을 수행하는 프로그램
🔗 출처: IT/개발용어::파싱? 파서? 무슨 뜻, 개발 직무 용어 살펴보기!- JWT 파싱 및 유효성 검사
1.jwtParser.parseClaimsJws(jwt)
➡ JWT를 파싱하여ClaimsJws객체 반환
2.getBody()
➡ JWT의 클레임(body)를 반환하고,getSubject()는 그 클레임에서subject필드만 추출
➡subject필드는 보통 사용자의 ID나 토큰을 발급한 주체와 관련된 정보를 담고 있음- 예외처리
1.catch를 통해 JWT 파싱 중 예외가 발생하면 JWT가 유효하지 않다고 판단하여false리턴
2.subject가 NULL이거나 비어있으면 JWT가 유효하지 않다고 판단하여false리턴
⭐
onError()메서드
요청 처리 중 오류가 발생했을 경우에 호출하는 메서드
- 반환타입
1.WebFlux를 사용하여 비동기적으로 처리를 완료하고,Mono<void>를 반환
2.Mono<void>는 결과값이 없음을 나타내며 비동기 처리 완료 후 반환할 때 사용
3. 데이터가 단 건의 경우엔Mono사용, 다 건인 경우Flux사용- 입력 파라미터
1.ServerWebExchange exchange
➡ 현재 HTTP 요청과 응답을 캡슐화하는 객체
2.String err
➡ 오류 메시지를 나타내는 문자열
3.HttpStatus httpStatus
➡ 오류 발생 시 클라이언트에게 반환할 HTTP 상태 코드
(ex. HttpStatus.UNAUTHORIZED => 401 을 나타냄)
spring:
cloud:
gateway:
routes:
- id: e-user-service
uri: lb://E-USER-SERVICE
predicates:
- Path=/e-user-service/**
- Method=GET
filters:
- RemoveRequestHeader=Cookie
- RewritePath=/e-user-service/(?<segment>.*), /$\{segment}
- AuthorizationHeaderFilter #필터 적용
token:
secret: make_my_secret_user_token #토큰 추가
해당 권한 필터는 GET으로 요청한 건에 대해서만 적용됨
회원가입, 로그인 등 POST로 요청하는 건에 대해서는 권한 필터가 적용되지 않음



