ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [MSA] Spring Cloud Gateway๋กœ API ๊ฒŒ์ดํŠธ์›จ์ด ๊ตฌ์ถ•ํ•˜๊ธฐ
    STOVE DEVCAMP 3๊ธฐ/MSA 2023. 3. 10. 03:10

    ๐Ÿ“Œ Spring Cloud Gateway

    1) ์Šคํ”„๋ง ํด๋ผ์šฐ๋“œ ๊ฒŒ์ดํŠธ์›จ์ด๋ž€

    • ์Šคํ”„๋ง์—์„œ ์ œ๊ณตํ•˜๋Š” API ๊ฒŒ์ดํŠธ์›จ์ด ๊ตฌ์ถ•์„ ์œ„ํ•œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์ค‘ ํ•˜๋‚˜์ž„
    • API ๊ฒŒ์ดํŠธ์›จ์ด๊ฐ€ ์ถ”์ƒ์ ์ธ ๊ฐœ๋…์ด๋ผ๋ฉด, ์Šคํ”„๋ง ํด๋ผ์šฐ๋“œ ๊ฒŒ์ดํŠธ์›จ์ด๋Š” ๊ตฌํ˜„์ฒด ๋Š๋‚Œ

     

    2) ๊ตฌ์„ฑ ์š”์†Œ

    ํฌ๊ฒŒ 3๊ฐ€์ง€ ๊ตฌ์„ฑ์š”์†Œ๊ฐ€ ์กด์žฌํ•จ

    • Route
      • ๊ณ ์œ ID, ๋ชฉ์ ์ง€ URI, Predicate, Filter๋กœ ๊ตฌ์„ฑ๋จ
      • ๊ฒŒ์ดํŠธ์›จ์ด๋กœ ์š”์ฒญ๋œ URI์˜ ์กฐ๊ฑด์ด ์ฐธ์ผ ๊ฒฝ์šฐ, ๋งคํ•‘๋œ ํ•ด๋‹น ๊ฒฝ๋กœ๋กœ ์š”์ฒญ์„ ์ „๋‹ฌํ•ด์คŒ
    • Predicate
      • ์ฃผ์–ด์ง„ ์š”์ฒญ์ด ์กฐ๊ฑด์„ ์ถฉ์กฑํ•˜๋Š”์ง€ ํ…Œ์ŠคํŠธํ•˜๋Š” ๊ตฌ์„ฑ์š”์†Œ
      • ๊ฐ ์š”์ฒญ ๊ฒฝ๋กœ์— ๋Œ€ํ•ด ์ถฉ์กฑํ•ด์•ผ ํ•˜๋Š” 1๊ฐœ ์ด์ƒ์˜ ์กฐ๊ฑด์ž๋ฅผ ์ •์˜ํ•  ์ˆ˜ ์žˆ์Œ
      • ๋งŒ์•ฝ ์ •์˜๋œ ๋ชจ๋“  Predicate์— ๋งค์นญ๋˜์ง€ ์•Š๋Š”๋‹ค๋ฉด HTTP 404 Not Found๋ฅผ ๋ฐ˜ํ™˜ํ•จ
    • Filter
      • ์š”์ฒญ ๋ฐ ์‘๋‹ต์— ๋Œ€ํ•œ ์ „์ฒ˜๋ฆฌ, ํ›„์ฒ˜๋ฆฌ๋ฅผ ๋‹ด๋‹นํ•˜๋Š” ๊ตฌ์„ฑ์š”์†Œ
    • ์ด๋Ÿฌํ•œ ์„ค์ •์€ Java DSL๋กœ ์ •์˜ํ•˜๊ฑฐ๋‚˜ application.yml ๋“ฑ์˜ ์„ค์ • ํŒŒ์ผ์— ์ •์˜ํ•  ์ˆ˜ ์žˆ์Œ

     

    ์œ„ ์„ค์ •๋“ค์€ RouteDefinition ๊ฐ์ฒด๋กœ ๋งคํ•‘๋˜๊ณ , ๋งคํ•‘๋œ ์„ค์ •์ •๋ณด๋“ค์„ ๋ฐ”ํƒ•์œผ๋กœ ๊ฒŒ์ดํŠธ์›จ์ด๊ฐ€ ๋™์ž‘ํ•จ

    public class RouteDefinition {  
      
       private String id;  
      
       @NotEmpty  
       @Valid   private List<PredicateDefinition> predicates = new ArrayList<>();  
      
       @Valid  
       private List<FilterDefinition> filters = new ArrayList<>();  
      
       @NotNull  
       private URI uri;  
      
       private Map<String, Object> metadata = new HashMap<>();  
      
       private int order = 0;  
      
       public RouteDefinition() {  
       }
    
    	...
    }

     

    3) ๋™์ž‘ ๋ฐฉ์‹

    API ๊ฒŒ์ดํŠธ์›จ์ด๋กœ์„œ ์ž‘๋™ํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” API ๊ฒŒ์ดํŠธ์›จ์ด๊ฐ€ ์ œ๊ณตํ•˜๋Š” ๊ธฐ๋Šฅ๋“ค์„ ์ œ๊ณตํ•ด์ค˜์•ผ ํ•˜๋Š”๋ฐ, ์ด๋ฅผ ์œ„ํ•ด ์Šคํ”„๋ง ํด๋ผ์šฐ๋“œ ๊ฒŒ์ดํŠธ์›จ์ด ์–ด๋–ป๊ฒŒ ๊ตฌํ˜„๋˜์–ด ๋™์ž‘ํ•˜๋Š”๊ฐ€?

     

    ํ๋ฆ„

    1. ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์Šคํ”„๋ง ํด๋ผ์šฐ๋“œ ๊ฒŒ์ดํŠธ์›จ์ด๋กœ ์š”์ฒญํ•จ
    2. Gateway Handler Mapping๋Š” Route๋งˆ๋‹ค ์„ค์ •๋œ Predicate์„ ์ ์šฉํ•ด ์•Œ๋งž์€ Route Instance๋ฅผ ์ฐพ๊ณ , ์š”์ฒญ์„ Gateway Web Handler๋กœ ์ „๋‹ฌํ•จ
    3. Gateway Web Handler๊ฐ€ Route์— ์ •์˜๋œ Filter๋“ค๋กœ Filter Chain์„ ์ƒ์„ฑํ•˜์—ฌ ์‹คํ–‰ํ•จ
    4. ๋ชจ๋“  Pre-Filter ๋กœ์ง์ด ์ˆ˜ํ–‰๋˜๋ฉด, Proxy ์š”์ฒญ์ด ๋งŒ๋“ค์–ด์ง€๊ณ , ์ด์— ๋Œ€ํ•œ ์‘๋‹ต์ด ์˜ค๋ฉด, Post-Filter ๋กœ์ง์ด ์ˆ˜ํ–‰๋˜์–ด ์ตœ์ข…์ ์œผ๋กœ ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ๋ฐ˜ํ™˜๋จ

     

    ์ฆ‰, Filter๋ฅผ ์ •์˜ํ•ด ์ธ์ฆ/์ธ๊ฐ€ ๋ฐ ๋กœ๊น…, ๋ชจ๋‹ˆํ„ฐ๋ง ๋ฐ ๋ฉ”ํŠธ๋ฆญ ์ˆ˜์ง‘ ๋“ฑ์„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ์Œ

     

    ๐Ÿ“Œ ํด๋ฃจํ„ฐ์— ๋„์ž…ํ•˜๊ธฐ - API ๋ผ์šฐํŒ… ๋ฐ ๊ณตํ†ต ์ธ์ฆ ์ˆ˜ํ–‰

    1) API ๋ผ์šฐํŒ…

    • yaml ํŒŒ์ผ์— API ์—”๋“œํฌ์ธํŠธ๋ณ„ Route, Predicate, Filter๋ฅผ ์ •์˜ํ•จ

    ๊ฐœ์„  ํฌ์ธํŠธ

    • ์„œ๋น„์Šค์˜ ๊ฐœ์ˆ˜๊ฐ€ ๋งŽ์•„์งˆ ๊ฒฝ์šฐ, yaml ํŒŒ์ผ์˜ ์‚ฌ์ด์ฆˆ๊ฐ€ ์ปค์ง€๊ณ , API ๋ผ์šฐํŒ… ์กฐ๊ฑด ๊ด€๋ฆฌ๊ฐ€ ์–ด๋ ค์›Œ์ง
    • API ์—”๋“œํฌ์ธํŠธ ๊ด€๋ฆฌ์ž ํŽ˜์ด์ง€๋ฅผ ๋งŒ๋“ค์–ด ๋ผ์šฐํŒ… ์กฐ๊ฑด ์ถ”๊ฐ€, ์‚ญ์ œ๊ฐ€ ์‰ฝ๋„๋ก ๊ฐœ์„ ํ•  ์ˆ˜ ์žˆ์„ ๊ฒƒ์œผ๋กœ ์˜ˆ์ƒ๋จ

     

    2) ๊ณตํ†ต ์ธ์ฆ ์ˆ˜ํ–‰

    ์ˆ˜ํ–‰ ์ „

    ์ˆ˜ํ–‰ ํ›„

    • Authorization ํ•„์š”ํ•˜์ง€ ์•Š์€ ์š”์ฒญ - ๋กœ๊ทธ์ธ/ํšŒ์›๊ฐ€์ž…

    • Authorization ํ•„์š”ํ•œ ์š”์ฒญ

     

    ๊ตฌํ˜„ ๋ฐฉ์‹

    • ์œ„์˜ ์Šคํ”„๋ง ํด๋ผ์šฐ๋“œ ๊ฒŒ์ดํŠธ์›จ์ด ๋™์ž‘๋ฐฉ์‹์„ ๋ณด๋ฉด, ๊ณตํ†ต ์ธ์ฆ์„ ์ˆ˜ํ–‰ํ•  Filter๋ฅผ ์ •์˜ํ•ด ์ธ์ฆ์ด ํ•„์š”ํ•œ API์˜ ๊ฒฝ์šฐ ํ•ด๋‹น Filter๋ฅผ ๊ฑฐ์น˜๋„๋ก ์„ค์ •ํ•  ์ˆ˜ ์žˆ์Œ

     

    1) Filter ์ •์˜

    //๊ณตํ†ต ์ธ์ฆ์„ ์ˆ˜ํ–‰ํ•˜๋Š” AuthorizationHeaderFilter
    
    @Component
    public class AuthorizationHeaderGatewayFilterFactory extends AbstractGatewayFilterFactory<AuthorizationHeaderGatewayFilterFactory.Config> {
    
        private static final Logger logger = LoggerFactory.getLogger(AuthorizationHeaderGatewayFilterFactory.class);
    
        private final FilterUtils filterUtils;
        private final JwtUtils jwtUtils;
    
        private static final Pattern BEARER_TOKEN_PATTERN = Pattern.compile("Bearer (.*)");
    
        public AuthorizationHeaderGatewayFilterFactory(FilterUtils filterUtils, JwtUtils jwtUtils) {
            super(Config.class);
            this.filterUtils = filterUtils;
            this.jwtUtils = jwtUtils;
        }
    
        @Override
        public GatewayFilter apply(AuthorizationHeaderGatewayFilterFactory.Config config) {
            return (exchange, chain) -> {
                String token = resolveToken(exchange);
                String userId = getUserId(token);
                return chain.filter(filterUtils.setRequestHeader(exchange, JwtUtils.USER_ID, userId));
            };
        }
    
        private String resolveToken(ServerWebExchange exchange) {
            String token = filterUtils.getHeaderValue(exchange.getRequest().getHeaders(), JwtUtils.AUTHORIZATION);
            if (!StringUtils.hasText(token)) throw new GatewayErrorException(GatewayError.AUTHENTICATION_FAIL);
            Matcher matcher = BEARER_TOKEN_PATTERN.matcher(token);
            if (!matcher.matches()) throw new GatewayErrorException(GatewayError.AUTHENTICATION_FAIL);
            return matcher.group(1);
        }
    
        private String getUserId(String token) {
            String userId = jwtUtils.getUserId(token);
            if (!StringUtils.hasText(userId)) throw new GatewayErrorException(GatewayError.NO_USERID_IN_TOKEN);
            return userId;
        }
    
        static class Config { }
    }

    ์ฐธ๊ณ 

     

    2) ์ธ์ฆ์ด ํ•„์š”ํ•œ URI์— Filter ์ ์šฉ

    ์ฐธ๊ณ 

     

    3) Filter ์ ์šฉ ํ…Œ์ŠคํŠธ

    • WireMock์„ ์‚ฌ์šฉํ•ด ์„ค์ • ์กฐ๊ฑด์— ๋”ฐ๋ผ Filter๊ฐ€ ์ ์šฉ๋˜๋Š”์ง€ ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅํ•จ
    @SpringBootTest(
            webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
            classes = AuthorizationHeaderGatewayFilterFactoryTest.AuthorizationHeaderFilterTestConfig.class)
    @AutoConfigureWebTestClient
    public class AuthorizationHeaderGatewayFilterFactoryTest {
    
        @TestConfiguration
        static class AuthorizationHeaderFilterTestConfig {
    
            @Autowired
            AuthorizationHeaderGatewayFilterFactory authorizationHeaderGatewayFilterFactory;
    
            @Bean(destroyMethod = "stop")
            WireMockServer wireMockServer() {
                WireMockConfiguration options = WireMockConfiguration.wireMockConfig().dynamicPort();
                WireMockServer wireMock = new WireMockServer(options);
                wireMock.start();
                return wireMock;
            }
    
            @Bean
            RouteLocator testRoutes(RouteLocatorBuilder builder, WireMockServer wireMock) {
                AuthorizationHeaderGatewayFilterFactory.Config config = new AuthorizationHeaderGatewayFilterFactory.Config();
                GatewayFilter gatewayFilter = authorizationHeaderGatewayFilterFactory.apply(config);
                return builder.routes()
                        .route(predicateSpec -> predicateSpec
                                .path("/api/user/health")
                                .uri(wireMock.baseUrl()))
                        .route(predicateSpec -> predicateSpec
                                .path("/api/user/**")
                                .filters(filterSpec -> filterSpec.filter(gatewayFilter))
                                .uri(wireMock.baseUrl()))
                        .build();
            }
        }
    
        @Autowired
        WebTestClient webTestClient;
    
        @Autowired
        WireMockServer wireMockServer;
    
        @AfterEach
        void afterAll() {
            wireMockServer.resetAll();
        }
    
        @Value("${jwt.secret}")
        String secretKey;
    
        @Test
        @DisplayName("health check api๋Š” ์ธ๊ฐ€ ํ•„ํ„ฐ๋ฅผ ๊ฑฐ์น˜์ง€ ์•Š์Œ")
        void healthCheck() {
            //given
            wireMockServer.stubFor(WireMock.get("/api/user/health").willReturn(WireMock.ok()));
    
            //when, then
            webTestClient.get().uri("/api/user/health")
                    .exchange()
                    .expectStatus()
                    .isEqualTo(HttpStatus.OK);
        }
    
        @Test
        @DisplayName("ํ† ํฐ ๊ฒ€์ฆ ์„ฑ๊ณต ์‹œ ์š”์ฒญ ํ—ค๋”์— userId ๊ฐ’์ด ํฌํ•จ๋จ")
        void tokenValidationSuccess() {
            //given
            //ํ† ํฐ
            long now = new Date().getTime();
            Date exp = new Date(now + 1000 * 60 * 30);
    
            byte[] keyBytes = Decoders.BASE64.decode(secretKey);
            Key signingKey = Keys.hmacShaKeyFor(keyBytes);
    
            String token = Jwts.builder()
                    .setSubject("clutter")
                    .claim("auth", "ROLE_USER")
                    .claim("userId", "1")
                    .signWith(signingKey, SignatureAlgorithm.HS512)
                    .setExpiration(exp)
                    .compact();
            //์š”์ฒญ
            String expectedUserId = "1";
            wireMockServer.stubFor(WireMock.get("/api/user/1").willReturn(WireMock.ok()));
    
            //when, then
            webTestClient.get().uri("/api/user/1")
                    .headers(httpHeaders -> httpHeaders.add("Authorization", "Bearer " + token))
                    .exchange()
                    .expectStatus()
                    .isEqualTo(HttpStatus.OK);
    
            wireMockServer.verify(WireMock.getRequestedFor(WireMock.urlEqualTo("/api/user/1"))
                    .withHeader("userId", equalTo(expectedUserId)));
        }
    
        @Test
        @DisplayName("ํ† ํฐ ์œ ํšจ๊ธฐ๊ฐ„ ๋งŒ๋ฃŒ ์‹œ ์˜ˆ์™ธ ๋ฐ˜ํ™˜ํ•จ")
        void tokenInvalidThrowException() {
            //given
            //ํ† ํฐ
            long now = new Date().getTime();
            Date exp = new Date(now - 1000 * 60 * 30);
    
            byte[] keyBytes = Decoders.BASE64.decode(secretKey);
            Key signingKey = Keys.hmacShaKeyFor(keyBytes);
    
            String token = Jwts.builder()
                    .setSubject("clutter")
                    .claim("auth", "ROLE_USER")
                    .claim("userId", "1")
                    .signWith(signingKey, SignatureAlgorithm.HS512)
                    .setExpiration(exp)
                    .compact();
            //์š”์ฒญ
            wireMockServer.stubFor(WireMock.get("/api/user/1").willReturn(WireMock.ok()));
    
            //when, then
            webTestClient.get().uri("/api/user/1")
                    .headers(httpHeaders -> httpHeaders.add("Authorization", "Bearer " + token))
                    .exchange()
                    .expectStatus()
                    .isEqualTo(HttpStatus.UNAUTHORIZED)
                    .expectBody()
                    .jsonPath("$.code").isEqualTo("-521");
        }
    
        @Test
        @DisplayName("ํ† ํฐ์— userId๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ ์˜ˆ์™ธ ๋ฐ˜ํ™˜ํ•จ")
        void withoutUserIdThrowException() {
            //given
            //ํ† ํฐ
            long now = new Date().getTime();
            Date exp = new Date(now + 1000 * 60 * 30);
    
            byte[] keyBytes = Decoders.BASE64.decode(secretKey);
            Key signingKey = Keys.hmacShaKeyFor(keyBytes);
    
            String token = Jwts.builder()
                    .setSubject("clutter")
                    .claim("auth", "ROLE_USER")
    //                .claim("userId", "1")
                    .signWith(signingKey, SignatureAlgorithm.HS512)
                    .setExpiration(exp)
                    .compact();
            //์š”์ฒญ
            wireMockServer.stubFor(WireMock.get("/api/user/1").willReturn(WireMock.ok()));
    
            //when, then
            webTestClient.get().uri("/api/user/1")
                    .headers(httpHeaders -> httpHeaders.add("Authorization", "Bearer " + token))
                    .exchange()
                    .expectStatus()
                    .isEqualTo(HttpStatus.UNAUTHORIZED)
                    .expectBody()
                    .jsonPath("$.code").isEqualTo("-520")
                    .jsonPath("$.message").isEqualTo("ํ† ํฐ์— userId ๊ฐ’์ด ์—†์Šต๋‹ˆ๋‹ค.");
        }
    
        @Test
        @DisplayName("์š”์ฒญ์— Authorization ํ—ค๋”๊ฐ€ ์—†์œผ๋ฉด ์˜ˆ์™ธ ๋ฐ˜ํ™˜ํ•จ")
        void noAuthorizationThrowException() {
            //given
            wireMockServer.stubFor(WireMock.get("/api/user/1").willReturn(WireMock.ok()));
    
            //when, then
            webTestClient.get().uri("/api/user/1")
                    .exchange()
                    .expectStatus()
                    .isEqualTo(HttpStatus.UNAUTHORIZED)
                    .expectBody()
                    .jsonPath("$.code").isEqualTo("-522")
                    .jsonPath("$.message").isEqualTo("์ธ์ฆ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค.");
        }
    }

    ์ฐธ๊ณ 

    'STOVE DEVCAMP 3๊ธฐ > MSA' ์นดํ…Œ๊ณ ๋ฆฌ์˜ ๋‹ค๋ฅธ ๊ธ€

    [MSA] ํด๋ฃจํ„ฐ MSA ๋„์ž… ๊ณผ์ •  (0) 2023.03.10
    [MSA] ํด๋ฃจํ„ฐ MSA ๋„์ž… ํšŒ๊ณ   (0) 2023.03.10

    ๋Œ“๊ธ€

Designed by Tistory.