Web Flux, 스프링 클라우드를 사용해 마이크로 서비스에서의 SSO를 구현 해 보자(1)

안상철·2023년 1월 7일
0

이모저모개발

목록 보기
6/8

기존 JWT를 사용한 회원관리 방법은 여기를 참조 하자.

위 포스팅과는 다르게 이번글은 하나의 프로젝트에서 회원 인증인가를 진행하지 않고 다른 프로젝트에서 인증인가를 거친 후 여러 서비스를 이용하도록 해 볼 것이다.

이렇게 스프링 클라우드, 게이트 웨이를 이용해 인증인가를 진행하게 되면

  • 회원 관리 db를 하나로 통합할 수 있고
  • 인증 인가 서비스를 하나의 프로젝트에서 진행할 수 있어서

많은 서비스를 사용해도 한군데에서 인증인가를 진행할 수 있기 때문에 마이크로 서비스 구현이 가능해진다.

단계별로 하나씩 구현 해 보자

1. build.gradle

언제나 그랬듯 build.gradle에 먼저 라이브러리를 받아준다.
주요 implemention은 아래 6개 되시겠다.

implementation 'org.springframework.cloud:spring-cloud-starter-gateway'
    implementation 'io.jsonwebtoken:jjwt:0.9.1'
    implementation group: 'org.glassfish.jaxb', name: 'jaxb-runtime', version: '2.3.2'
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'

스프링 클라우드와 JWT, 우리의 친구 롬복을 설치 해 준다.

2. JwtTokenProvider

들어온 요청의 JWT를 Parsing하고 검증한다.

역시 유효한 JWT가 아니라면 예외처리를 해 준다.

@Component
public class JwtTokenProvider {

    @Value("${app.security.jwtSecret}")
    private String jwtSecret;

    public Claims getUserFromJWT(String token) {
        try {
            return Jwts.parser().setSigningKey(jwtSecret)
                .parseClaimsJws(token).getBody();

        } catch (Exception e) {
            throw new UnauthorizedException(e.getMessage());
        }
    }

    public boolean verifyJWT(String token) {
        try {
            Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token);
            return true;
        } catch (Exception e) {
            throw new UnauthorizedException(e.getMessage());
        }
    }
}

3. AuthorizationHeaderFilter

이제 스프링 게이트웨이로 요청하는 API를 검사해 인증인가를 진행할 수 있도록 도와주는 필터를 작성한다.

@Component
public class AuthorizationHeaderFilter extends
    AbstractGatewayFilterFactory<AuthorizationHeaderFilter.Config> {

    private final JwtTokenProvider jwtTokenProvider;

    private final String HEADER_USER_ID = "userId";

    @Autowired
    private UserRepository userRepository;

    public AuthorizationHeaderFilter(JwtTokenProvider jwtTokenProvider) {
        super(Config.class);
        this.jwtTokenProvider = jwtTokenProvider;
    }

    @Override
    public GatewayFilter apply(Config config) {
        return (exchange, chain) -> {
            ServerHttpRequest request = exchange.getRequest();
            String jwt = getJwtFromRequest(request);
            try {
                if (StringUtils.hasText(jwt) && jwtTokenProvider.verifyJWT(jwt)) {
                    addAuthorizationHeaders(exchange.getRequest(), jwt);
                    return chain.filter(exchange);
                }
            } catch (Exception e) {
                throw new UnauthorizedException(e.getMessage());
            }
            throw new UnauthorizedException();
        };
    }

    private String getJwtFromRequest(ServerHttpRequest request) {
        if (containsAuthorization(request)) {
            String bearerToken = extractToken(request);
            if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
                return bearerToken.substring(7);
            }
        }
        throw new UnauthorizedException("헤더에 토큰이 존재하지않음");
    }

    // 검증 후 인증된 헤더로 요청을 변경
    private void addAuthorizationHeaders(ServerHttpRequest request, String jwt) {
        Claims users = jwtTokenProvider.getUserFromJWT(jwt);
        request.mutate()
                .headers(httpHeader -> httpHeader.setAll(usersConvertToString(users))).build();
    }

    private boolean containsAuthorization(ServerHttpRequest request) {
        return request.getHeaders().containsKey(HttpHeaders.AUTHORIZATION);
    }

    private String extractToken(ServerHttpRequest request) {
        return request.getHeaders().getOrEmpty(HttpHeaders.AUTHORIZATION).get(0);
    }


    private Map<String, String> usersConvertToString(Claims claims) {
        try {
            ObjectMapper objectMapper = new ObjectMapper();
            if (!claims.containsKey(HEADER_USER_ID) || ObjectUtils
                .isEmpty(claims.get(HEADER_USER_ID))) {
                throw new UnauthorizedException("토큰 user가 유효하지않음 ");
            }
            String userId = (String) claims.get(HEADER_USER_ID);
            Map<String, String> headers = new HashMap<>();

            if (userRepository.findByUserId(userId).isPresent()) {
                User user = userRepository.findByUserId(userId).get();
                String userJson = objectMapper.writeValueAsString(user);
                headers.put("userJson", userJson);
            } else {
                throw new GateWayUserNotFoundException("토큰 userId 값이 유효하지 않음.");
            }
            return headers;
        } catch (Exception e) {
            throw new UnauthorizedException(e.getMessage());
        }
    }

    public static class Config {
    }

}

API 요청 헤더에 들어있는 userId를 통해 JWT로 인증인가를 진행 할 것이다.

    @Override
    public GatewayFilter apply(Config config) {
        return (exchange, chain) -> {
            ServerHttpRequest request = exchange.getRequest();
            String jwt = getJwtFromRequest(request);
            try {
                if (StringUtils.hasText(jwt) && jwtTokenProvider.verifyJWT(jwt)) {
                    addAuthorizationHeaders(exchange.getRequest(), jwt);
                    return chain.filter(exchange);
                }
            } catch (Exception e) {
                throw new UnauthorizedException(e.getMessage());
            }
            throw new UnauthorizedException();
        };
    }

    private String getJwtFromRequest(ServerHttpRequest request) {
        if (containsAuthorization(request)) {
            String bearerToken = extractToken(request);
            if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
                return bearerToken.substring(7);
            }
        }
        throw new UnauthorizedException("헤더에 토큰이 존재하지않음");
    }

    private void addAuthorizationHeaders(ServerHttpRequest request, String jwt) {
        Claims users = jwtTokenProvider.getUserFromJWT(jwt);
        request.mutate()
                .headers(httpHeader -> httpHeader.setAll(usersConvertToString(users))).build();
    }

게이트웨이 필터의 기본 설정이다. request객체해서 JWT를 찾아 getJwtFromRequest를 통해 extractToken로 토큰을 추출하고 JWT 부분을 찾아준다.

api 요청 헤더에 jwt가 올바르게 들어가 있다면 addAuthorizationHeaders를 통해 인증된 헤더로 요청을 변경 해 준다.

addAuthorizationHeaders 에서는 usersConvertToString를 통해 claims의 userId를 찾아 user를 판별하고, 유효한 유저가 아니라면 예외처리를 해 준다.

이 때 userRepository에는 userId로 회원을 조회하는 부분을 미리 만들어둬야 한다.

이미 존재하는 유저, 즉 유효한 유저라면 요청 헤더에 유저 정보를 넣어준다.

4. application.yml

스프링 게이트웨이에서 가장 중요한 yml설정이다. cloud부분에 여러 서비스를 등록하고 api를 다른 서비스에 연결 해 주며, 클라우드 세팅을 진행할 수 있다.

app.security.jwtSecret: SecretKey

server:
  port: 8080
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/dbName?characterEncoding=UTF-8&serverTimezone=Asia/Seoul
    username: root
    password: Admin
    driverClassName: com.mysql.cj.jdbc.Driver
  jpa:
    show-sql: false
    open-in-view: false
    properties.hibernate:
      format_sql: false
    hibernate:
      ddl-auto: none
      naming:
        physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
        implicit-strategy: org.hibernate.boot.model.naming.ImplicitNamingStrategyLegacyJpaImpl
  cloud:
    gateway:
      globalcors:
        corsConfigurations:
          '[/**]':
            allowedOrigins: 'http://localhost:3000'
            allowedMethods: '*'
            allowedHeaders: '*'
            allowCredentials: true
      default-filters:
        - DedupeResponseHeader=Access-Control-Allow-Origin Access-Control-Allow-Credentials, RETAIN_UNIQUE
      routes:
        - id: admin-api
          uri: http://localhost:8090/
          predicates:
            - Path=/admin/api/**
          filters:
            - name: CustomLogFilter
              args:
                baseMessage: Spring Cloud Gateway

기본 JPA 설정은 넘어가고 cloud 부분을 살펴보자

  cloud:
    gateway:
      globalcors:
        corsConfigurations:
          '[/**]':
            allowedOrigins: 'http://localhost:3000'
            allowedMethods: '*'
            allowedHeaders: '*'
            allowCredentials: true

allowedOrigins 에는 들어오는 요청의 도메인을 작성 해 준다.
allowedMethods 에는 들어오는 요청의 Http Method를, Header또한 모두 받아줄 것이므로 * 를 작성한다.
allowCredentials로 쿠키요청을 허용한다. 이 부분은 CORS 설정에도 영향을 준다.

      default-filters:
        - DedupeResponseHeader=Access-Control-Allow-Origin Access-Control-Allow-Credentials, RETAIN_UNIQUE

Access-Control-Allow-Origin을 허용해준다. CORS에러가 나지 않도록..

      routes:
        - id: admin-api
          uri: http://localhost:8090/
          predicates:
            - Path=/admin/api/**
          filters:
            - name: CustomLogFilter
              args:
                baseMessage: Spring Cloud Gateway AdminApiFilter

스프링 게이트웨이에서 route부분에는

  • 서비스의 이름인 id
  • 해당 서비스 요청이 들어오는 uri
  • 요청 url인 predicates
  • 요청 로그를 남길 filter와 기본 메세지

등등을 설정 해 준다.

이제 route에 정의된 url로 들어오는 모든 요청을 스프링 게이트웨이가 받아 다른 서비스로 연결 해 줄 준비가 되었다.

다음 포스팅에서는 개별 서비스에서 어떻게 설정하는지 알아보도록 하겠다.

profile
웹 개발자(FE / BE) anna입니다.

0개의 댓글