-
[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 ๊ฒ์ดํธ์จ์ด๊ฐ ์ ๊ณตํ๋ ๊ธฐ๋ฅ๋ค์ ์ ๊ณตํด์ค์ผ ํ๋๋ฐ, ์ด๋ฅผ ์ํด ์คํ๋ง ํด๋ผ์ฐ๋ ๊ฒ์ดํธ์จ์ด ์ด๋ป๊ฒ ๊ตฌํ๋์ด ๋์ํ๋๊ฐ?
ํ๋ฆ
- ํด๋ผ์ด์ธํธ๊ฐ ์คํ๋ง ํด๋ผ์ฐ๋ ๊ฒ์ดํธ์จ์ด๋ก ์์ฒญํจ
- Gateway Handler Mapping๋ Route๋ง๋ค ์ค์ ๋ Predicate์ ์ ์ฉํด ์๋ง์ Route Instance๋ฅผ ์ฐพ๊ณ , ์์ฒญ์ Gateway Web Handler๋ก ์ ๋ฌํจ
- Gateway Web Handler๊ฐ Route์ ์ ์๋ Filter๋ค๋ก Filter Chain์ ์์ฑํ์ฌ ์คํํจ
- ๋ชจ๋ 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