Spring Cloud Gateway에서 인증, 인가처리를 하는 Filter를 만들어보겠습니다.
저는 먼저 MSA구조에서 Auth, User 서비스를 담당하는 서버를 미리 만들어 로그인, 회원가입을 처리하고 로그인처리 후 발급한 JWT 토큰을 Gateway에서 인증 및 인가처리를 해주기 위해서 만들게 되었습니다.
Auth Service JWT 발급
public String createToken(String userUid, List<String> roles){
logger.info("[JwtTokenProvider] createToken, 토큰 생성");
// Jwt token의 값을 넣기 위한 claims, sub 속성(제목)에 유저의 ID 삽입
Claims claims = Jwts.claims().setSubject(userUid);
// 유저의 권한 목록을 삽입
claims.put("roles", roles);
Date now = new Date();
String token = Jwts.builder()
.setClaims(claims)
// 토큰 생성 시간
.setIssuedAt(now)
// 토큰의 만료 기간을 설정
.setExpiration(new Date(now.getTime() + tokenValidMillisecond))
// 암호화 알고리즘 및 암호화에 사용되는 키 설정
.signWith(SignatureAlgorithm.HS256, secretKey)
.compact();
return token;
}
토큰에는 유저의 권한, 이메일을 담고 HS25 알고리즘으로 secretKey를 사용하여 서명을 진행하였습니다. 해당 토큰을 사용하여 gateway에서 인가처리를 해주어야하기 때문에 gateway에도 secretKey가 있어야 합니다.
Gateway
@Component
public class AuthorizationHeaderFilter extends AbstractGatewayFilterFactory<AuthorizationHeaderFilter.Config> {
}
gateway에서는 AbstractGatewayFilterFactory<AuthorizationHeaderFilter.Config>를 상속받아 구현해야 합니다.
Override
@Override
public GatewayFilter apply(AuthorizationHeaderFilter.Config config) {
return (exchange, chain) -> {
String requiredRole = config.getRequiredRole();
ServerHttpRequest request = exchange.getRequest();
logger.info("요청한 uri : "+request.getURI());
// Authorization 헤더 없다면 에러
if (!request.getHeaders().containsKey(HttpHeaders.AUTHORIZATION)) return onError(exchange, "No authorization header", HttpStatus.UNAUTHORIZED);
//헤더값
String authorizationHeader = request.getHeaders().get(HttpHeaders.AUTHORIZATION).get(0);
// jwt
String jwt = authorizationHeader.replace("Bearer ", "");
logger.info(jwt);
if (!isJwtValid(jwt)) return onError(exchange, "JWT token is not valid", HttpStatus.UNAUTHORIZED);
logger.info("JWT VALID");
logger.info("요청한 User : "+resolveTokenUser(jwt));
String userRole = resolveTokenRole(jwt).replace("[", "").replace("]", "");
logger.info(userRole);
// 인가처리
if(requiredRole.equalsIgnoreCase("role_admin")){
if(!userRole.equalsIgnoreCase("role_admin")) return onError(exchange, "do not have permission", HttpStatus.FORBIDDEN);
}else if(requiredRole.equalsIgnoreCase("role_seller")){
if(!userRole.equalsIgnoreCase("role_seller") && !userRole.equalsIgnoreCase("role_admin"))
return onError(exchange, "do not have permission", HttpStatus.FORBIDDEN);
}
logger.info("필요 유효성 : "+requiredRole+" 유효성 체크 완료 :" + userRole);
return chain.filter(exchange);
};
}
apply 메소드를 구현해주어야 합니다.
저는 먼저 유저의 요청에 대한 Authorization 헤더를 추출하여 JWT에 대한 검증을 진행하였습니다.
또한 관리자, 판매자, 유저에따라 다른 접근권한을 주어 인가처리를 진행하였습니다.
JWT 검증
private boolean isJwtValid(String token) {
logger.info("[JwtTokenProvider] validateToken, 토큰 유효성 체크");
try{
Jws<Claims> claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token);
return !claims.getBody().getExpiration().before(new Date());
} catch (Exception e){
e.printStackTrace();
logger.info("[JwtTokenProvider] validateToken, 토큰 유효성 체크 예외 발생");
return false;
}
}
먼저 JWT에 대한 유효성을 체크합니다.
AuthService와 같은 secretKey를 두어 해당 토큰이 HS256 알고리즘으로 secretKey로 서명된 토큰인지 검증하고 해당 토큰이 만료되었는지 검증합니다.
유저의 접근 권한 체크
private String resolveTokenRole(String token){
try {
String subject = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().get("roles").toString();
return subject;
}catch (Exception e){
logger.info("유저 권한 체크 실패");
return "e";
}
}
요청에대한 토큰에 있는 role 즉, 권한을 체크하여 현재 요청하고자하는 API에 대한 접근 권한이 있는지 체크합니다.
이렇게 만들어진 API에 대한 Filter는 Route Config에서 설정할 수 있습니다.
전체 코드
@Component
public class AuthorizationHeaderFilter extends AbstractGatewayFilterFactory<AuthorizationHeaderFilter.Config> {
// application.yml에서 값 추출 객
@Value(value = "${springboot.jwt.secret}")
private String secretKey;
private final static Logger logger = LoggerFactory.getLogger(AuthorizationHeaderFilter.class);
public AuthorizationHeaderFilter() {
super(Config.class);
}
@PostConstruct
protected void init(
){
logger.info("[JwtTokenProvider] init, secretKey 초기화");
secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes(StandardCharsets.UTF_8));
}
@Override
public GatewayFilter apply(AuthorizationHeaderFilter.Config config) {
return (exchange, chain) -> {
String requiredRole = config.getRequiredRole();
ServerHttpRequest request = exchange.getRequest();
logger.info("요청한 uri : "+request.getURI());
// Authorization 헤더 없다면 에러
if (!request.getHeaders().containsKey(HttpHeaders.AUTHORIZATION)) return onError(exchange, "No authorization header", HttpStatus.UNAUTHORIZED);
//헤더값
String authorizationHeader = request.getHeaders().get(HttpHeaders.AUTHORIZATION).get(0);
// jwt
String jwt = authorizationHeader.replace("Bearer ", "");
logger.info(jwt);
if (!isJwtValid(jwt)) return onError(exchange, "JWT token is not valid", HttpStatus.UNAUTHORIZED);
logger.info("JWT VALID");
logger.info("요청한 User : "+resolveTokenUser(jwt));
String userRole = resolveTokenRole(jwt).replace("[", "").replace("]", "");
logger.info(userRole);
// 인가처리
if(requiredRole.equalsIgnoreCase("role_admin")){
if(!userRole.equalsIgnoreCase("role_admin")) return onError(exchange, "do not have permission", HttpStatus.FORBIDDEN);
}else if(requiredRole.equalsIgnoreCase("role_seller")){
if(!userRole.equalsIgnoreCase("role_seller") && !userRole.equalsIgnoreCase("role_admin"))
return onError(exchange, "do not have permission", HttpStatus.FORBIDDEN);
}
logger.info("필요 유효성 : "+requiredRole+" 유효성 체크 완료 :" + userRole);
return chain.filter(exchange);
};
}
private Mono<Void> onError(ServerWebExchange exchange, String err, HttpStatus httpStatus) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(httpStatus);
logger.error(err);
return response.setComplete();
}
private String resolveTokenRole(String token){
try {
String subject = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().get("roles").toString();
return subject;
}catch (Exception e){
logger.info("유저 권한 체크 실패");
return "e";
}
}
private String resolveTokenUser(String token){
try {
String subject = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().get("sub").toString();
return subject;
}catch (Exception e){
logger.info("유저 권한 체크 실패");
return "e";
}
}
private boolean isJwtValid(String token) {
logger.info("[JwtTokenProvider] validateToken, 토큰 유효성 체크");
try{
Jws<Claims> claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token);
return !claims.getBody().getExpiration().before(new Date());
} catch (Exception e){
e.printStackTrace();
logger.info("[JwtTokenProvider] validateToken, 토큰 유효성 체크 예외 발생");
return false;
}
}
public static class Config {
private String requiredRole;
public String getRequiredRole() {
return requiredRole;
}
public void setRequiredRole(String requiredRole) {
this.requiredRole = requiredRole;
}
}
}
Route
먼저 Route Config의 gateway Routes 메소드의 매개변수로 헤더 필터를 가져옵니다.
@Bean
public RouteLocator gatewayRoutes(RouteLocatorBuilder builder,
AuthorizationHeaderFilter authFilter) {
}
만든 필터는 아래와같이 적용이 가능합니다.
.route("payment-service", r->r.path("/payment/**")
.filters(f->f.filter(authFilter.apply(config -> {config.setRequiredRole("role_user");})))
.uri("lb://PAYMENT-SERVICE"))
'웹' 카테고리의 다른 글
[MSA] 마이크로서비스 아키텍처의 비동기 처리 (0) | 2024.01.09 |
---|---|
[Spring Boot] Mockito를 사용한 단위 테스트 종속성 제거 (0) | 2024.01.09 |
Image 데이터를 RestTemplate로 통신하면서 발생한 에러 (0) | 2023.11.13 |
Spring Cloud Eureka란? (0) | 2023.10.30 |
Spring Boot에서 보안을 위한 JWT를 발급하는 방법 (0) | 2023.10.27 |