Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ jjwt-api = { module = "io.jsonwebtoken:jjwt-api", version.ref = "jjwt" }
jjwt-impl = { module = "io.jsonwebtoken:jjwt-impl", version.ref = "jjwt" }
jjwt-jackson = { module = "io.jsonwebtoken:jjwt-jackson", version.ref = "jjwt" }
springdoc-openapi-webmvc-ui = { module = "org.springdoc:springdoc-openapi-starter-webmvc-ui", version.ref = "springdoc" }
springdoc-openapi-webflux-ui = { module = "org.springdoc:springdoc-openapi-starter-webflux-ui", version.ref = "springdoc" }
micrometer-context-propagation = { module = "io.micrometer:context-propagation" }
mockk = { module = "io.mockk:mockk", version.ref = "mockk" }

Expand Down
28 changes: 28 additions & 0 deletions rush-auth/src/main/kotlin/com/rush/auth/config/OpenApiConfig.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.rush.auth.config

import io.swagger.v3.oas.models.Components
import io.swagger.v3.oas.models.OpenAPI
import io.swagger.v3.oas.models.security.SecurityRequirement
import io.swagger.v3.oas.models.security.SecurityScheme
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration

/**
* 게이트웨이 Swagger UI의 Authorize에 넣은 JWT가 모든 요청의 Authorization 헤더로 붙도록 Bearer 인증을 정의합니다.
*/
@Configuration
class OpenApiConfig {
@Bean
fun openApi(): OpenAPI =
OpenAPI()
.components(
Components().addSecuritySchemes(
BEARER,
SecurityScheme().type(SecurityScheme.Type.HTTP).scheme("bearer").bearerFormat("JWT"),
),
).addSecurityItem(SecurityRequirement().addList(BEARER))

companion object {
const val BEARER = "bearer"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.rush.auth.config

import io.swagger.v3.oas.models.security.SecurityScheme
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test

class OpenApiConfigTest {
@Test
fun `Bearer 인증을 정의하고 전체 API에 적용한다`() {
val openApi = OpenApiConfig().openApi()

val scheme = openApi.components.securitySchemes.getValue(OpenApiConfig.BEARER)
assertEquals(SecurityScheme.Type.HTTP, scheme.type)
assertEquals("bearer", scheme.scheme)
assertTrue(openApi.security.any { it.containsKey(OpenApiConfig.BEARER) })
}
}
7 changes: 7 additions & 0 deletions rush-config/src/main/resources/config/rush-auth.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
# rush-auth 전용 설정
server:
port: 8081
# 게이트웨이가 넘긴 X-Forwarded-*로 Swagger 서버 주소를 게이트웨이 주소로 만듭니다.
forward-headers-strategy: framework

# 게이트웨이 Swagger UI가 /auth/** 라우트로 가져갈 수 있게 문서 경로에 prefix를 붙입니다.
springdoc:
api-docs:
path: /auth/v3/api-docs
7 changes: 7 additions & 0 deletions rush-config/src/main/resources/config/rush-minigame.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
# rush-minigame 전용 설정
server:
port: 8083
# 게이트웨이가 넘긴 X-Forwarded-*로 Swagger 서버 주소를 게이트웨이 주소로 만듭니다.
forward-headers-strategy: framework

# 게이트웨이 Swagger UI가 /minigame/** 라우트로 가져갈 수 있게 문서 경로에 prefix를 붙입니다.
springdoc:
api-docs:
path: /minigame/v3/api-docs
7 changes: 7 additions & 0 deletions rush-config/src/main/resources/config/rush-shop.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
# rush-shop 전용 설정
server:
port: 8082
# 게이트웨이가 넘긴 X-Forwarded-*로 Swagger 서버 주소를 게이트웨이 주소로 만듭니다.
forward-headers-strategy: framework

# 게이트웨이 Swagger UI가 /shop/** 라우트로 가져갈 수 있게 문서 경로에 prefix를 붙입니다.
springdoc:
api-docs:
path: /shop/v3/api-docs
7 changes: 7 additions & 0 deletions rush-config/src/main/resources/config/rush-stage.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
# rush-stage 전용 설정
server:
port: 8084
# 게이트웨이가 넘긴 X-Forwarded-*로 Swagger 서버 주소를 게이트웨이 주소로 만듭니다.
forward-headers-strategy: framework

# 게이트웨이 Swagger UI가 /stage/** 라우트로 가져갈 수 있게 문서 경로에 prefix를 붙입니다.
springdoc:
api-docs:
path: /stage/v3/api-docs
8 changes: 8 additions & 0 deletions rush-gateway/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,14 @@ WebSocket을 제외한 라우트는 서킷브레이커(Resilience4j)를 거칩
| `TRUSTED_PROXIES` | 없음 | 앞단 LB/프록시 대역 |
| `CORS_ALLOWED_ORIGINS` | 없음 | CORS 허용 Origin |

## Swagger

`http://localhost:8080/swagger-ui.html`에서 auth, shop, minigame, stage, user, sp의 API 문서를 함께 볼 수 있습니다.

- 각 서비스는 `/{prefix}/v3/api-docs`에 문서를 냅니다. 게이트웨이가 이 경로를 라우트로 가져오며, 토큰 없이 열립니다.
- "Try it out" 요청은 게이트웨이(8080)로 갑니다. 인증이 필요한 API는 Authorize에 JWT를 넣으세요.
- live, chat은 WebSocket뿐이라 제외했습니다.

## 실행

```bash
Expand Down
1 change: 1 addition & 0 deletions rush-gateway/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ dependencies {
implementation(libs.spring.boot.starter.actuator)
implementation(libs.spring.boot.starter.security)
implementation(libs.micrometer.context.propagation)
implementation(libs.springdoc.openapi.webflux.ui)
implementation(libs.jjwt.api)
runtimeOnly(libs.jjwt.impl)
runtimeOnly(libs.jjwt.jackson)
Expand Down
12 changes: 12 additions & 0 deletions rush-gateway/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,18 @@ spring:
uri: ${CHAT_URL:ws://localhost:8088}
predicates: [Path=/chat/**]

# 각 서비스의 OpenAPI 문서를 라우트로 가져와 /swagger-ui.html 한 곳에서 봅니다. 문서 경로는 토큰 없이 열립니다.
springdoc:
swagger-ui:
path: /swagger-ui.html
urls:
- { name: auth, url: /auth/v3/api-docs }
- { name: shop, url: /shop/v3/api-docs }
- { name: minigame, url: /minigame/v3/api-docs }
- { name: stage, url: /stage/v3/api-docs }
- { name: user, url: /user/v3/api-docs }
- { name: sp, url: /sp/v3/api-docs }

resilience4j:
circuitbreaker:
configs:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package com.rush.gateway.swagger

import com.sun.net.httpserver.HttpServer
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.test.context.DynamicPropertyRegistry
import org.springframework.test.context.DynamicPropertySource
import org.springframework.test.web.reactive.server.WebTestClient
import java.net.InetSocketAddress

@SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
properties = [
"spring.cloud.config.enabled=false",
"eureka.client.enabled=false",
"jwt.secret=test-secret-key-for-gateway-swagger-test-0123456789",
],
)
@AutoConfigureWebTestClient
class SwaggerAggregationTest {
@Autowired
lateinit var client: WebTestClient

@Test
fun `Swagger UI가 열린다`() {
client
.get()
.uri("/swagger-ui.html")
.exchange()
.expectStatus()
.is3xxRedirection
}

@Test
fun `Swagger UI 설정에 6개 서비스 문서가 등록된다`() {
client
.get()
.uri("/v3/api-docs/swagger-config")
.exchange()
.expectStatus()
.isOk
.expectBody()
.jsonPath("$.urls.length()")
.isEqualTo(6)
.jsonPath("$.urls[?(@.name == 'shop')].url")
.isEqualTo("/shop/v3/api-docs")
}

@Test
fun `서비스 문서는 토큰 없이 원문 그대로 가져온다`() {
client
.get()
.uri("/user/v3/api-docs")
.exchange()
.expectStatus()
.isOk
.expectBody()
.json(API_DOCS, true)
}

companion object {
private const val API_DOCS = """{"openapi":"3.0.0","info":{"title":"rush-user","version":"1.0"},"paths":{}}"""

private val docsServer =
HttpServer.create(InetSocketAddress("localhost", 0), 0).apply {
createContext("/user/v3/api-docs") { exchange ->
val body = API_DOCS.toByteArray()
exchange.responseHeaders.set("Content-Type", "application/json")
exchange.sendResponseHeaders(200, body.size.toLong())
exchange.responseBody.use { it.write(body) }
}
start()
}

@JvmStatic
@DynamicPropertySource
fun routes(registry: DynamicPropertyRegistry) {
registry.add("USER_URL") { "http://localhost:${docsServer.address.port}" }
}

@JvmStatic
@AfterAll
fun stopServer() {
docsServer.stop(0)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.rush.minigame.config

import io.swagger.v3.oas.models.Components
import io.swagger.v3.oas.models.OpenAPI
import io.swagger.v3.oas.models.security.SecurityRequirement
import io.swagger.v3.oas.models.security.SecurityScheme
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration

/**
* 게이트웨이 Swagger UI의 Authorize에 넣은 JWT가 모든 요청의 Authorization 헤더로 붙도록 Bearer 인증을 정의합니다.
*/
@Configuration
class OpenApiConfig {
@Bean
fun openApi(): OpenAPI =
OpenAPI()
.components(
Components().addSecuritySchemes(
BEARER,
SecurityScheme().type(SecurityScheme.Type.HTTP).scheme("bearer").bearerFormat("JWT"),
),
).addSecurityItem(SecurityRequirement().addList(BEARER))

companion object {
const val BEARER = "bearer"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.rush.minigame.config

import io.swagger.v3.oas.models.security.SecurityScheme
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test

class OpenApiConfigTest {
@Test
fun `Bearer 인증을 정의하고 전체 API에 적용한다`() {
val openApi = OpenApiConfig().openApi()

val scheme = openApi.components.securitySchemes.getValue(OpenApiConfig.BEARER)
assertEquals(SecurityScheme.Type.HTTP, scheme.type)
assertEquals("bearer", scheme.scheme)
assertTrue(openApi.security.any { it.containsKey(OpenApiConfig.BEARER) })
}
}
28 changes: 28 additions & 0 deletions rush-shop/src/main/kotlin/com/rush/shop/config/OpenApiConfig.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.rush.shop.config

import io.swagger.v3.oas.models.Components
import io.swagger.v3.oas.models.OpenAPI
import io.swagger.v3.oas.models.security.SecurityRequirement
import io.swagger.v3.oas.models.security.SecurityScheme
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration

/**
* 게이트웨이 Swagger UI의 Authorize에 넣은 JWT가 모든 요청의 Authorization 헤더로 붙도록 Bearer 인증을 정의합니다.
*/
@Configuration
class OpenApiConfig {
@Bean
fun openApi(): OpenAPI =
OpenAPI()
.components(
Components().addSecuritySchemes(
BEARER,
SecurityScheme().type(SecurityScheme.Type.HTTP).scheme("bearer").bearerFormat("JWT"),
),
).addSecurityItem(SecurityRequirement().addList(BEARER))

companion object {
const val BEARER = "bearer"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.rush.shop.config

import io.swagger.v3.oas.models.security.SecurityScheme
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Assertions.assertTrue
import org.junit.jupiter.api.Test

class OpenApiConfigTest {
@Test
fun `Bearer 인증을 정의하고 전체 API에 적용한다`() {
val openApi = OpenApiConfig().openApi()

val scheme = openApi.components.securitySchemes.getValue(OpenApiConfig.BEARER)
assertEquals(SecurityScheme.Type.HTTP, scheme.type)
assertEquals("bearer", scheme.scheme)
assertTrue(openApi.security.any { it.containsKey(OpenApiConfig.BEARER) })
}
}
Loading
Loading