Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
cb62c8b
[BOOK-132] refactor: gateway - filter에서 예외처리, 예외메세지 구체적으로 표시 (#35)
minwoo1999 Jul 13, 2025
3ddb18d
[BOOK-132] feat: CustomAccessDeniedHanlder 추가 (#36)
minwoo1999 Jul 13, 2025
5bbeea2
[BOOK-132] feat: CustomAuthenticationEntryPoint 추가 (#36)
minwoo1999 Jul 13, 2025
3b23808
[BOOK-132] refactor: securityconfig customHanlder 적용 (#36)
minwoo1999 Jul 13, 2025
b84df13
[BOOK-132] chore: gateway - 불필요한 파일 삭제 (#36)
minwoo1999 Jul 13, 2025
0ce56dc
[BOOK-132] delete: gateway - 검증 주체를 Spring Security의 내장 필터로 이동하기 위해 삭제
move-hoon Jul 14, 2025
d383ab5
[BOOK-132] feat: global-utils - 역할을 관리하는 enum 생성
move-hoon Jul 14, 2025
a0db0ab
[BOOK-132] feat: domain, infra - Role 칼럼 추가
move-hoon Jul 14, 2025
eee7992
[BOOK-132] refactor: domain - 회원가입 시 기본 USER 권한으로 역할이 저장되도록 리팩토링
move-hoon Jul 14, 2025
18e92db
[BOOK-132] chore: buildSrc - oauth2-resource-server 의존성 추가
move-hoon Jul 14, 2025
ba04fe4
[BOOK-132] chore: gateway - oauth2-resource-server 의존성 implementation
move-hoon Jul 14, 2025
6b5ba94
[BOOK-132] refactor: gateway - 로그 레벨 변경 및 공통 로직 private 메서드로 분리
move-hoon Jul 14, 2025
b641c5b
[BOOK-132] feat: gateway - 검증 주체를 Spring Security로 이동
move-hoon Jul 14, 2025
55d3424
[BOOK-132] refactor: gateway - 검증 주체 변경으로 인해 로직 리팩토링 및 역할을 포함해서 토큰 생성…
move-hoon Jul 14, 2025
883ba5d
[BOOK-132] refactor: apis - 토큰 생성 로직 변경으로 인한 리팩토링
move-hoon Jul 14, 2025
76154b2
[BOOK-132] refactor: gateway - JWT 기반 OAuth2 리소스 서버 설정 추가 및 인증 컨버터 연동
move-hoon Jul 14, 2025
974930b
[BOOK-132] delete: gateway - JwtDecoder의 디코딩 예외 사용으로 인해 제거
move-hoon Jul 14, 2025
28ce8ca
[BOOK-132] feat: gateway - 화이트 리스트 도입
move-hoon Jul 14, 2025
fe39ccf
[BOOK-132] refactor: gateway - JwtEncoder, JwtDecoder를 사용하는 방식으로 변경
move-hoon Jul 14, 2025
5244be4
[BOOK-132] docs: gateway - javadoc 추가
move-hoon Jul 14, 2025
baae034
[BOOK-132] chore: gateway, buildSrc - jjwt 관련 의존성 제거
move-hoon Jul 14, 2025
3f300f6
[BOOK-132] chore: apis - jjwt 의존성 제거
move-hoon Jul 14, 2025
496d3b7
[BOOK-132] refactor: gateway - SecurityErrorResponseWriter 클래스 도입을 통한…
move-hoon Jul 14, 2025
b322869
[BOOK-132] chore: gateway - 가독성을 위한 개행 추가
move-hoon Jul 14, 2025
f4ed5e9
[BOOK-132] chore: gateway - 화이트 경로 리팩토링
move-hoon Jul 14, 2025
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
4 changes: 0 additions & 4 deletions apis/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,6 @@ dependencies {

implementation(Dependencies.Database.MYSQL_CONNECTOR)

implementation(Dependencies.Auth.JWT)
implementation(Dependencies.Auth.JWT_IMPL)
implementation(Dependencies.Auth.JWT_JACKSON)

implementation(Dependencies.Swagger.SPRINGDOC_OPENAPI_STARTER_WEBMVC_UI)

implementation(Dependencies.Logging.KOTLIN_LOGGING)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class AuthController(

@PostMapping("/refresh")
override fun refreshToken(@RequestBody @Valid request: TokenRefreshRequest): ResponseEntity<AuthResponse> {
val tokenPair = authUseCase.refreshToken(request.validRefreshToken())
val tokenPair = authUseCase.reissueTokenPair(request.validRefreshToken())
return ResponseEntity.ok(AuthResponse.fromTokenPair(tokenPair))
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,16 @@ import org.yapp.apis.auth.dto.response.TokenPairResponse
import org.yapp.apis.auth.service.TokenService
import org.yapp.gateway.jwt.JwtTokenService
import org.yapp.globalutils.annotation.Helper
import org.yapp.globalutils.auth.Role
import java.util.*

@Helper
class AuthTokenHelper(
private val tokenService: TokenService,
private val jwtTokenService: JwtTokenService
) {

fun generateTokenPair(userId: UUID): TokenPairResponse {
val accessToken = jwtTokenService.generateAccessToken(userId)
fun generateTokenPair(userId: UUID, role: Role): TokenPairResponse {
val accessToken = jwtTokenService.generateAccessToken(userId, role)
val refreshToken = jwtTokenService.generateRefreshToken(userId)
val expiration = jwtTokenService.getRefreshTokenExpiration()

Expand All @@ -26,10 +26,6 @@ class AuthTokenHelper(
return tokenService.getUserIdFromToken(refreshToken)
}

fun getUserIdFromAccessToken(accessToken: String): UUID {
return jwtTokenService.getUserIdFromToken(accessToken)
}

fun deleteToken(token: String) {
tokenService.deleteByToken(token)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package org.yapp.apis.auth.usecase

import org.springframework.transaction.annotation.Propagation
import org.springframework.transaction.annotation.Transactional
import org.yapp.apis.auth.dto.AuthCredentials
import org.yapp.apis.auth.dto.response.TokenPairResponse
Expand All @@ -20,20 +19,20 @@ class AuthUseCase(
private val tokenService: TokenService,
private val authTokenHelper: AuthTokenHelper
) {

@Transactional
fun signIn(credentials: AuthCredentials): TokenPairResponse {
val strategy = socialAuthService.resolve(credentials)
val userInfo = strategy.authenticate(credentials)
val user = userAuthService.findOrCreateUser(userInfo)
return authTokenHelper.generateTokenPair(user.id)
return authTokenHelper.generateTokenPair(user.id, user.role)
}

@Transactional
fun refreshToken(refreshToken: String): TokenPairResponse {
fun reissueTokenPair(refreshToken: String): TokenPairResponse {
val userId = authTokenHelper.validateAndGetUserIdFromRefreshToken(refreshToken)
authTokenHelper.deleteToken(refreshToken)
return authTokenHelper.generateTokenPair(userId)
val user = userAuthService.findUserById(userId)
return authTokenHelper.generateTokenPair(user.id, user.role)
}

@Transactional
Expand Down
7 changes: 1 addition & 6 deletions buildSrc/src/main/kotlin/Dependencies.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,14 @@ object Dependencies {
const val BOOT_STARTER_ACTUATOR = "org.springframework.boot:spring-boot-starter-actuator"
const val BOOT_STARTER_TEST = "org.springframework.boot:spring-boot-starter-test"
const val BOOT_STARTER_DATA_REDIS = "org.springframework.boot:spring-boot-starter-data-redis"
const val BOOT_STARTER_OAUTH2_RESOURCE_SERVER = "org.springframework.boot:spring-boot-starter-oauth2-resource-server"
const val KOTLIN_REFLECT = "org.jetbrains.kotlin:kotlin-reflect"
}

object Database {
const val MYSQL_CONNECTOR = "com.mysql:mysql-connector-j"
}

object Auth {
const val JWT = "io.jsonwebtoken:jjwt-api:0.11.5"
const val JWT_IMPL = "io.jsonwebtoken:jjwt-impl:0.11.5"
const val JWT_JACKSON = "io.jsonwebtoken:jjwt-jackson:0.11.5"
}

object Swagger {
const val SPRINGDOC_OPENAPI_STARTER_WEBMVC_UI = "org.springdoc:springdoc-openapi-starter-webmvc-ui:2.3.0"
}
Expand Down
12 changes: 9 additions & 3 deletions domain/src/main/kotlin/org/yapp/domain/user/User.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package org.yapp.domain.user

import org.yapp.domain.user.ProviderType
import org.yapp.globalutils.auth.Role
import java.time.LocalDateTime
import java.util.*

Expand All @@ -13,6 +13,7 @@ import java.util.*
* @property profileImageUrl The URL of the user's profile image.
* @property providerType The type of authentication provider.
* @property providerId The ID from the authentication provider.
* @property role The roles of the user (e.g., USER, ADMIN).
* @property createdAt The timestamp when the user was created.
* @property updatedAt The timestamp when the user was last updated.
* @property deletedAt The timestamp when the user was soft-deleted, or null if the user is not deleted.
Expand All @@ -24,6 +25,7 @@ data class User private constructor(
val profileImageUrl: String?,
val providerType: ProviderType,
val providerId: String,
val role: Role,
val createdAt: LocalDateTime,
val updatedAt: LocalDateTime,
val deletedAt: LocalDateTime? = null
Expand All @@ -37,15 +39,14 @@ data class User private constructor(
)
}

fun isDeleted(): Boolean = deletedAt != null

companion object {
fun create(
email: String,
nickname: String,
profileImageUrl: String?,
providerType: ProviderType,
providerId: String,
role: Role,
createdAt: LocalDateTime,
updatedAt: LocalDateTime,
deletedAt: LocalDateTime? = null
Expand All @@ -57,6 +58,7 @@ data class User private constructor(
profileImageUrl = profileImageUrl,
providerType = providerType,
providerId = providerId,
role = role,
createdAt = createdAt,
updatedAt = updatedAt,
deletedAt = deletedAt
Expand All @@ -70,6 +72,7 @@ data class User private constructor(
profileImageUrl: String?,
providerType: ProviderType,
providerId: String,
role: Role,
createdAt: LocalDateTime,
updatedAt: LocalDateTime,
deletedAt: LocalDateTime? = null
Expand All @@ -81,10 +84,13 @@ data class User private constructor(
profileImageUrl = profileImageUrl,
providerType = providerType,
providerId = providerId,
role = role,
createdAt = createdAt,
updatedAt = updatedAt,
deletedAt = deletedAt
)
}
}

private fun isDeleted(): Boolean = deletedAt != null
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package org.yapp.domain.user

import org.yapp.domain.user.vo.SocialUserProfile
import org.yapp.globalutils.annotation.DomainService
import org.yapp.globalutils.auth.Role
import org.yapp.globalutils.util.TimeProvider
import java.util.UUID

Expand Down Expand Up @@ -34,6 +35,7 @@ class UserDomainService(
profileImageUrl = profile.profileImageUrl,
providerType = profile.providerType,
providerId = profile.providerId,
role = Role.USER,
createdAt = now,
updatedAt = now
)
Expand Down
4 changes: 1 addition & 3 deletions gateway/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@ dependencies {
implementation(project(Dependencies.Projects.GLOBAL_UTILS))
implementation(Dependencies.Spring.BOOT_STARTER_WEB)
implementation(Dependencies.Spring.BOOT_STARTER_SECURITY)
implementation(Dependencies.Auth.JWT)
implementation(Dependencies.Auth.JWT_IMPL)
implementation(Dependencies.Auth.JWT_JACKSON)
implementation(Dependencies.Spring.BOOT_STARTER_OAUTH2_RESOURCE_SERVER)
testImplementation(Dependencies.Spring.BOOT_STARTER_TEST)
}

Expand Down
83 changes: 83 additions & 0 deletions gateway/src/main/kotlin/org/yapp/gateway/config/JwtConfig.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package org.yapp.gateway.config

import com.nimbusds.jose.jwk.JWKSet
import com.nimbusds.jose.jwk.OctetSequenceKey
import com.nimbusds.jose.jwk.source.ImmutableJWKSet
import com.nimbusds.jose.jwk.source.JWKSource
import com.nimbusds.jose.proc.SecurityContext
import org.springframework.beans.factory.annotation.Value
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.security.oauth2.jose.jws.MacAlgorithm
import org.springframework.security.oauth2.jwt.*
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter
import org.yapp.gateway.constants.JwtConstants
import javax.crypto.spec.SecretKeySpec

@Configuration
class JwtConfig(
@Value("\${jwt.secret-key}")
private val secretKey: String
) {
companion object {
private val SIGNATURE_ALGORITHM = MacAlgorithm.HS256
private const val PRINCIPAL_CLAIM = "sub"
}

/**
* JWT를 암호화하고 서명하는 데 사용될 키의 소스(`JWKSource`)를 생성하여 Bean으로 등록합니다.
* HMAC-SHA 알고리즘에 사용될 대칭 키를 `OctetSequenceKey` 형태로 래핑하여 제공합니다.
*
* @return 생성된 `JWKSource<SecurityContext>` 객체
*/
@Bean
fun jwkSource(): JWKSource<SecurityContext> {
val jwk: OctetSequenceKey = OctetSequenceKey.Builder(secretKey.toByteArray()).build()
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

명시적 문자 인코딩을 사용하세요.

toByteArray()는 플랫폼 기본 인코딩을 사용하므로 환경에 따라 다른 결과를 생성할 수 있습니다.

명시적으로 UTF-8 인코딩을 지정하세요:

-        val jwk: OctetSequenceKey = OctetSequenceKey.Builder(secretKey.toByteArray()).build()
+        val jwk: OctetSequenceKey = OctetSequenceKey.Builder(secretKey.toByteArray(Charsets.UTF_8)).build()
🤖 Prompt for AI Agents
In gateway/src/main/kotlin/org/yapp/gateway/config/JwtConfig.kt at line 36, the
call to toByteArray() uses the platform's default character encoding, which can
lead to inconsistent behavior across environments. Modify the code to explicitly
specify UTF-8 encoding when converting the secretKey string to a byte array to
ensure consistent and predictable results.

return ImmutableJWKSet(JWKSet(jwk))
}

/**
* JWT를 생성(인코딩)하는 `JwtEncoder`를 생성하여 Bean으로 등록합니다.
* 주입받은 `JWKSource`를 사용하여 토큰에 디지털 서명을 수행합니다.
*
* @param jwkSource 토큰 서명에 사용될 키 소스
* @return 생성된 `JwtEncoder` 객체
*/
@Bean
fun jwtEncoder(jwkSource: JWKSource<SecurityContext>): JwtEncoder {
return NimbusJwtEncoder(jwkSource)
}

/**
* 클라이언트로부터 받은 JWT를 검증(디코딩)하는 `JwtDecoder`를 생성하여 Bean으로 등록합니다.
* 토큰의 서명, 만료 시간(기본 검증) 및 발급자(issuer)가 일치하는지 확인합니다.
*
* @return 생성된 `JwtDecoder` 객체
*/
@Bean
fun jwtDecoder(): JwtDecoder {
val secretKeySpec = SecretKeySpec(secretKey.toByteArray(), SIGNATURE_ALGORITHM.name)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

일관된 문자 인코딩을 사용하세요.

JWT 인코딩과 디코딩에서 동일한 인코딩을 사용해야 합니다.

UTF-8 인코딩을 명시적으로 지정하세요:

-        val secretKeySpec = SecretKeySpec(secretKey.toByteArray(), SIGNATURE_ALGORITHM.name)
+        val secretKeySpec = SecretKeySpec(secretKey.toByteArray(Charsets.UTF_8), SIGNATURE_ALGORITHM.name)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
val secretKeySpec = SecretKeySpec(secretKey.toByteArray(), SIGNATURE_ALGORITHM.name)
val secretKeySpec = SecretKeySpec(secretKey.toByteArray(Charsets.UTF_8), SIGNATURE_ALGORITHM.name)
🤖 Prompt for AI Agents
In gateway/src/main/kotlin/org/yapp/gateway/config/JwtConfig.kt at line 60, the
secretKey is converted to a byte array without specifying the character
encoding, which can cause inconsistencies. Fix this by explicitly specifying
UTF-8 encoding when converting the secretKey string to a byte array to ensure
consistent JWT encoding and decoding.

val decoder = NimbusJwtDecoder.withSecretKey(secretKeySpec).build()
val validator = JwtValidators.createDefaultWithIssuer(JwtConstants.ISSUER)
decoder.setJwtValidator(validator)
return decoder
}

/**
* 유효성이 검증된 JWT를 Spring Security의 `Authentication` 객체로 변환하는 `JwtAuthenticationConverter`를 등록합니다.
* JWT의 'roles' 클레임을 애플리케이션의 권한 정보(`GrantedAuthority`)로 매핑하고, 'sub' 클레임을 사용자의 주체(Principal)로 설정합니다.
*
* @return 생성된 `JwtAuthenticationConverter` 객체
*/
@Bean
fun jwtAuthenticationConverter(): JwtAuthenticationConverter {
val converter = JwtAuthenticationConverter()
converter.setJwtGrantedAuthoritiesConverter { jwt ->
val roles = jwt.getClaimAsStringList(JwtConstants.ROLES_CLAIM) ?: emptyList()
roles.map { role -> SimpleGrantedAuthority(role) }
}
converter.setPrincipalClaimName(PRINCIPAL_CLAIM)
return converter
}
}
47 changes: 0 additions & 47 deletions gateway/src/main/kotlin/org/yapp/gateway/config/SecurityConfig.kt

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package org.yapp.gateway.constants

object JwtConstants {
const val ROLES_CLAIM = "roles"
const val ISSUER = "gateway"
}

This file was deleted.

Loading