[MSA] Authorization 추가

jineey·2024년 11월 14일

MSA

목록 보기
19/36

Authorization

📌 Authorization 이란?

사용자에게 특정 리소스나 기능에 액세스할 수 있는 권한을 부여하는 프로세스

💡 Authorization vs Authentication

📌 개요

➡ API Gateway에 AuthorizationHeaderFilter를 사용하여 인가(Authorization) 과정 추가

📌 소스코드

  • pom.xml 수정
<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>
  • AuthorizationHeaderFilter.java 생성
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 을 나타냄)
  • application.yml 수정
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로 요청하는 건에 대해서는 권한 필터가 적용되지 않음

1. 권한 없는 상태로 GET 요청


2. 권한 부여 후 GET 요청


profile
새싹 개발자

0개의 댓글