user service에서 jwt를 통해 토큰을 발급받는 기능을 추가했으니 gateway에서 filter 기능을 통해 로그인 인증 처리를 해보자.
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.1</version>
</dependency>
jwt를 추가해준다.
javax.xml.bind의 경우 아래 예제를 진행하다보면 getBody() 메서드를 실행했을 때 해당 라이브러리가 비어 있어서 오류가 발생하기 때문에 따로 라이브러리를 추가했다.
@Component
@Slf4j
public class AuthorizationHeaderFilter extends AbstractGatewayFilterFactory<AuthorizationHeaderFilter.Config> {
private Environment env;
//생성시 Config class를 상속받은 Factory로 넘겨줘야해서 lombok을 사용하지 않고 다음과 같이 처리
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();
//header에 HttpHeaders.AUTHORIZATION 값이 존재하는지 확인
if(!request.getHeaders().containsKey(HttpHeaders.AUTHORIZATION)){
return onError(exchange, "no authorization header", HttpStatus.UNAUTHORIZED);
}
String authorizationHeader = request.getHeaders().get(org.springframework.http.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);
});
}
//token이 유효한지 확인
private boolean isJwtValid(String jwt) {
boolean returnValue = true;
String subject = null;
try {
subject = Jwts.parser().setSigningKey(env.getProperty("token.secret")) //secret key 값을 통해 parse
.parseClaimsJws(jwt).getBody() //token의 내용을 가져옴
.getSubject();
}catch (Exception e){
returnValue = false;
}
if(subject == null || subject.equals("")){
returnValue = false;
}
return returnValue;
}
//에러 발생시 에러 값을 response
//Mono, Flux -> Spring WebFlux 개념 / 데이터 단위 단일=Mono, 복수=Flux
private Mono<Void> onError(ServerWebExchange exchange, String err, HttpStatus httpStatus) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(httpStatus);
log.error(err);
return response.setComplete(); //Mono 데이터 return
}
}
AuthorizationHeaderFilter
파일을 만들어서 내용을 다음과 같이 입력한다. 여기서 token secret 값은 yml에서 설정하지 않았기 때문에 yml의 설정에도 추가해줘야한다.
...
spring:
application: #gateway service 이름
name: apigateway-service
cloud:
gateway: #gateway 설정
default-filters:
- name : GlobalFilter #Global Filter로 지정된 java 파일 이름
args:
baseMessage: Spring Cloud Gateway Global Filter
preLogger: true
postLogger: true
routes:
# ...
- id: user-service
uri: lb://USER-SERVICE
predicates:
- Path=/user-service/**
- Method=GET
filters:
- RemoveRequestHeader=Cookie
- RewritePath=/user-service(?<segment>.*), /$\{segment}
- AuthorizationHeaderFilter
...
token:
expiration_time: 86400000
secret: user_token
yml의 중요 내용만 설정해준다면 위의 내용과 같고 user-service에서 AuthorizationHeaderFilter
filter를 추가해주자.
로그인하여 발급 받은 토큰이 없을 때 health_check를 실행시켜보자
이제 GET user-service/** 에서는 token 없이 접근할 경우 401 에러가 떠버린다.
발급 받은 토큰을 사용하여 다시 요청해보자.
발급 받은 토큰 정보를 입력한 뒤 요청하면 다음과 같이 정상적으로 처리됨을 확인할 수 있다. 그럼 이 정보가 어떻게 진행되는지 로직을 살펴보자
우리가 작성한 AuthorizationHeaderFilter
의 apply()
에 요청이 들어오고
우리가 요청한 Header에 Authorization : value 가 존재하는지 확인을 먼저 해준다.
우리는 Bearer type으로 요청했기 때문에 value에 Bearer + token으로 값이 넘어오는데 해당 값을 replace()로 처리해주고 isJwtValid()
를 통해 유효성을 체크한다.
넘어온 값은 Jwt를 통해 parse해주고 해당 subject를 확인하면 81cae99b-0402-4076-9c57-585a145033eb 값이 들어있는데 이 값은 우리가 token을 만들때 사용했던 userId값이다.
정말인지 확인을 위해
login 했을 때 반환된 값을 보면 우리가 반환 받은 값과 동일한 값을 확인할 수 있다.
정상적으로 token 정보를 넘긴 반환 값을 받을 수 있었다.