Skip to content

Commit 0f4758b

Browse files
refactor: JwtAuthenticationFilter 예외 처리 로직 개선 (#36)
* [BOOK-132] refactor: gateway - filter에서 예외처리, 예외메세지 구체적으로 표시 (#35) * [BOOK-132] feat: CustomAccessDeniedHanlder 추가 (#36) * [BOOK-132] feat: CustomAuthenticationEntryPoint 추가 (#36) * [BOOK-132] refactor: securityconfig customHanlder 적용 (#36) * [BOOK-132] chore: gateway - 불필요한 파일 삭제 (#36) * [BOOK-132] delete: gateway - 검증 주체를 Spring Security의 내장 필터로 이동하기 위해 삭제 * [BOOK-132] feat: global-utils - 역할을 관리하는 enum 생성 * [BOOK-132] feat: domain, infra - Role 칼럼 추가 * [BOOK-132] refactor: domain - 회원가입 시 기본 USER 권한으로 역할이 저장되도록 리팩토링 * [BOOK-132] chore: buildSrc - oauth2-resource-server 의존성 추가 * [BOOK-132] chore: gateway - oauth2-resource-server 의존성 implementation * [BOOK-132] refactor: gateway - 로그 레벨 변경 및 공통 로직 private 메서드로 분리 * [BOOK-132] feat: gateway - 검증 주체를 Spring Security로 이동 * [BOOK-132] refactor: gateway - 검증 주체 변경으로 인해 로직 리팩토링 및 역할을 포함해서 토큰 생성하도록 변경 * [BOOK-132] refactor: apis - 토큰 생성 로직 변경으로 인한 리팩토링 * [BOOK-132] refactor: gateway - JWT 기반 OAuth2 리소스 서버 설정 추가 및 인증 컨버터 연동 * [BOOK-132] delete: gateway - JwtDecoder의 디코딩 예외 사용으로 인해 제거 * [BOOK-132] feat: gateway - 화이트 리스트 도입 * [BOOK-132] refactor: gateway - JwtEncoder, JwtDecoder를 사용하는 방식으로 변경 * [BOOK-132] docs: gateway - javadoc 추가 * [BOOK-132] chore: gateway, buildSrc - jjwt 관련 의존성 제거 * [BOOK-132] chore: apis - jjwt 의존성 제거 * [BOOK-132] refactor: gateway - SecurityErrorResponseWriter 클래스 도입을 통한 공통 예외처리 리팩토링 * [BOOK-132] chore: gateway - 가독성을 위한 개행 추가 * [BOOK-132] chore: gateway - 화이트 경로 리팩토링 --------- Co-authored-by: DongHoon Lee <dhl1924@naver.com>
1 parent e72e753 commit 0f4758b

File tree

24 files changed

+324
-317
lines changed

24 files changed

+324
-317
lines changed

apis/build.gradle.kts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,6 @@ dependencies {
1414

1515
implementation(Dependencies.Database.MYSQL_CONNECTOR)
1616

17-
implementation(Dependencies.Auth.JWT)
18-
implementation(Dependencies.Auth.JWT_IMPL)
19-
implementation(Dependencies.Auth.JWT_JACKSON)
20-
2117
implementation(Dependencies.Swagger.SPRINGDOC_OPENAPI_STARTER_WEBMVC_UI)
2218

2319
implementation(Dependencies.Logging.KOTLIN_LOGGING)

apis/src/main/kotlin/org/yapp/apis/auth/controller/AuthController.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ class AuthController(
2929

3030
@PostMapping("/refresh")
3131
override fun refreshToken(@RequestBody @Valid request: TokenRefreshRequest): ResponseEntity<AuthResponse> {
32-
val tokenPair = authUseCase.refreshToken(request.validRefreshToken())
32+
val tokenPair = authUseCase.reissueTokenPair(request.validRefreshToken())
3333
return ResponseEntity.ok(AuthResponse.fromTokenPair(tokenPair))
3434
}
3535

apis/src/main/kotlin/org/yapp/apis/auth/helper/AuthTokenHelper.kt

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,16 +4,16 @@ import org.yapp.apis.auth.dto.response.TokenPairResponse
44
import org.yapp.apis.auth.service.TokenService
55
import org.yapp.gateway.jwt.JwtTokenService
66
import org.yapp.globalutils.annotation.Helper
7+
import org.yapp.globalutils.auth.Role
78
import java.util.*
89

910
@Helper
1011
class AuthTokenHelper(
1112
private val tokenService: TokenService,
1213
private val jwtTokenService: JwtTokenService
1314
) {
14-
15-
fun generateTokenPair(userId: UUID): TokenPairResponse {
16-
val accessToken = jwtTokenService.generateAccessToken(userId)
15+
fun generateTokenPair(userId: UUID, role: Role): TokenPairResponse {
16+
val accessToken = jwtTokenService.generateAccessToken(userId, role)
1717
val refreshToken = jwtTokenService.generateRefreshToken(userId)
1818
val expiration = jwtTokenService.getRefreshTokenExpiration()
1919

@@ -26,10 +26,6 @@ class AuthTokenHelper(
2626
return tokenService.getUserIdFromToken(refreshToken)
2727
}
2828

29-
fun getUserIdFromAccessToken(accessToken: String): UUID {
30-
return jwtTokenService.getUserIdFromToken(accessToken)
31-
}
32-
3329
fun deleteToken(token: String) {
3430
tokenService.deleteByToken(token)
3531
}

apis/src/main/kotlin/org/yapp/apis/auth/usecase/AuthUseCase.kt

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
package org.yapp.apis.auth.usecase
22

3-
import org.springframework.transaction.annotation.Propagation
43
import org.springframework.transaction.annotation.Transactional
54
import org.yapp.apis.auth.dto.AuthCredentials
65
import org.yapp.apis.auth.dto.response.TokenPairResponse
@@ -20,20 +19,20 @@ class AuthUseCase(
2019
private val tokenService: TokenService,
2120
private val authTokenHelper: AuthTokenHelper
2221
) {
23-
2422
@Transactional
2523
fun signIn(credentials: AuthCredentials): TokenPairResponse {
2624
val strategy = socialAuthService.resolve(credentials)
2725
val userInfo = strategy.authenticate(credentials)
2826
val user = userAuthService.findOrCreateUser(userInfo)
29-
return authTokenHelper.generateTokenPair(user.id)
27+
return authTokenHelper.generateTokenPair(user.id, user.role)
3028
}
3129

3230
@Transactional
33-
fun refreshToken(refreshToken: String): TokenPairResponse {
31+
fun reissueTokenPair(refreshToken: String): TokenPairResponse {
3432
val userId = authTokenHelper.validateAndGetUserIdFromRefreshToken(refreshToken)
3533
authTokenHelper.deleteToken(refreshToken)
36-
return authTokenHelper.generateTokenPair(userId)
34+
val user = userAuthService.findUserById(userId)
35+
return authTokenHelper.generateTokenPair(user.id, user.role)
3736
}
3837

3938
@Transactional

buildSrc/src/main/kotlin/Dependencies.kt

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,14 @@ object Dependencies {
88
const val BOOT_STARTER_ACTUATOR = "org.springframework.boot:spring-boot-starter-actuator"
99
const val BOOT_STARTER_TEST = "org.springframework.boot:spring-boot-starter-test"
1010
const val BOOT_STARTER_DATA_REDIS = "org.springframework.boot:spring-boot-starter-data-redis"
11+
const val BOOT_STARTER_OAUTH2_RESOURCE_SERVER = "org.springframework.boot:spring-boot-starter-oauth2-resource-server"
1112
const val KOTLIN_REFLECT = "org.jetbrains.kotlin:kotlin-reflect"
1213
}
1314

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

18-
object Auth {
19-
const val JWT = "io.jsonwebtoken:jjwt-api:0.11.5"
20-
const val JWT_IMPL = "io.jsonwebtoken:jjwt-impl:0.11.5"
21-
const val JWT_JACKSON = "io.jsonwebtoken:jjwt-jackson:0.11.5"
22-
}
23-
2419
object Swagger {
2520
const val SPRINGDOC_OPENAPI_STARTER_WEBMVC_UI = "org.springdoc:springdoc-openapi-starter-webmvc-ui:2.3.0"
2621
}

domain/src/main/kotlin/org/yapp/domain/user/User.kt

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package org.yapp.domain.user
22

3-
import org.yapp.domain.user.ProviderType
3+
import org.yapp.globalutils.auth.Role
44
import java.time.LocalDateTime
55
import java.util.*
66

@@ -13,6 +13,7 @@ import java.util.*
1313
* @property profileImageUrl The URL of the user's profile image.
1414
* @property providerType The type of authentication provider.
1515
* @property providerId The ID from the authentication provider.
16+
* @property role The roles of the user (e.g., USER, ADMIN).
1617
* @property createdAt The timestamp when the user was created.
1718
* @property updatedAt The timestamp when the user was last updated.
1819
* @property deletedAt The timestamp when the user was soft-deleted, or null if the user is not deleted.
@@ -24,6 +25,7 @@ data class User private constructor(
2425
val profileImageUrl: String?,
2526
val providerType: ProviderType,
2627
val providerId: String,
28+
val role: Role,
2729
val createdAt: LocalDateTime,
2830
val updatedAt: LocalDateTime,
2931
val deletedAt: LocalDateTime? = null
@@ -37,15 +39,14 @@ data class User private constructor(
3739
)
3840
}
3941

40-
fun isDeleted(): Boolean = deletedAt != null
41-
4242
companion object {
4343
fun create(
4444
email: String,
4545
nickname: String,
4646
profileImageUrl: String?,
4747
providerType: ProviderType,
4848
providerId: String,
49+
role: Role,
4950
createdAt: LocalDateTime,
5051
updatedAt: LocalDateTime,
5152
deletedAt: LocalDateTime? = null
@@ -57,6 +58,7 @@ data class User private constructor(
5758
profileImageUrl = profileImageUrl,
5859
providerType = providerType,
5960
providerId = providerId,
61+
role = role,
6062
createdAt = createdAt,
6163
updatedAt = updatedAt,
6264
deletedAt = deletedAt
@@ -70,6 +72,7 @@ data class User private constructor(
7072
profileImageUrl: String?,
7173
providerType: ProviderType,
7274
providerId: String,
75+
role: Role,
7376
createdAt: LocalDateTime,
7477
updatedAt: LocalDateTime,
7578
deletedAt: LocalDateTime? = null
@@ -81,10 +84,13 @@ data class User private constructor(
8184
profileImageUrl = profileImageUrl,
8285
providerType = providerType,
8386
providerId = providerId,
87+
role = role,
8488
createdAt = createdAt,
8589
updatedAt = updatedAt,
8690
deletedAt = deletedAt
8791
)
8892
}
8993
}
94+
95+
private fun isDeleted(): Boolean = deletedAt != null
9096
}

domain/src/main/kotlin/org/yapp/domain/user/UserDomainService.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package org.yapp.domain.user
22

33
import org.yapp.domain.user.vo.SocialUserProfile
44
import org.yapp.globalutils.annotation.DomainService
5+
import org.yapp.globalutils.auth.Role
56
import org.yapp.globalutils.util.TimeProvider
67
import java.util.UUID
78

@@ -34,6 +35,7 @@ class UserDomainService(
3435
profileImageUrl = profile.profileImageUrl,
3536
providerType = profile.providerType,
3637
providerId = profile.providerId,
38+
role = Role.USER,
3739
createdAt = now,
3840
updatedAt = now
3941
)

gateway/build.gradle.kts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,7 @@ dependencies {
44
implementation(project(Dependencies.Projects.GLOBAL_UTILS))
55
implementation(Dependencies.Spring.BOOT_STARTER_WEB)
66
implementation(Dependencies.Spring.BOOT_STARTER_SECURITY)
7-
implementation(Dependencies.Auth.JWT)
8-
implementation(Dependencies.Auth.JWT_IMPL)
9-
implementation(Dependencies.Auth.JWT_JACKSON)
7+
implementation(Dependencies.Spring.BOOT_STARTER_OAUTH2_RESOURCE_SERVER)
108
testImplementation(Dependencies.Spring.BOOT_STARTER_TEST)
119
}
1210

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package org.yapp.gateway.config
2+
3+
import com.nimbusds.jose.jwk.JWKSet
4+
import com.nimbusds.jose.jwk.OctetSequenceKey
5+
import com.nimbusds.jose.jwk.source.ImmutableJWKSet
6+
import com.nimbusds.jose.jwk.source.JWKSource
7+
import com.nimbusds.jose.proc.SecurityContext
8+
import org.springframework.beans.factory.annotation.Value
9+
import org.springframework.context.annotation.Bean
10+
import org.springframework.context.annotation.Configuration
11+
import org.springframework.security.core.authority.SimpleGrantedAuthority
12+
import org.springframework.security.oauth2.jose.jws.MacAlgorithm
13+
import org.springframework.security.oauth2.jwt.*
14+
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter
15+
import org.yapp.gateway.constants.JwtConstants
16+
import javax.crypto.spec.SecretKeySpec
17+
18+
@Configuration
19+
class JwtConfig(
20+
@Value("\${jwt.secret-key}")
21+
private val secretKey: String
22+
) {
23+
companion object {
24+
private val SIGNATURE_ALGORITHM = MacAlgorithm.HS256
25+
private const val PRINCIPAL_CLAIM = "sub"
26+
}
27+
28+
/**
29+
* JWT를 암호화하고 서명하는 데 사용될 키의 소스(`JWKSource`)를 생성하여 Bean으로 등록합니다.
30+
* HMAC-SHA 알고리즘에 사용될 대칭 키를 `OctetSequenceKey` 형태로 래핑하여 제공합니다.
31+
*
32+
* @return 생성된 `JWKSource<SecurityContext>` 객체
33+
*/
34+
@Bean
35+
fun jwkSource(): JWKSource<SecurityContext> {
36+
val jwk: OctetSequenceKey = OctetSequenceKey.Builder(secretKey.toByteArray()).build()
37+
return ImmutableJWKSet(JWKSet(jwk))
38+
}
39+
40+
/**
41+
* JWT를 생성(인코딩)하는 `JwtEncoder`를 생성하여 Bean으로 등록합니다.
42+
* 주입받은 `JWKSource`를 사용하여 토큰에 디지털 서명을 수행합니다.
43+
*
44+
* @param jwkSource 토큰 서명에 사용될 키 소스
45+
* @return 생성된 `JwtEncoder` 객체
46+
*/
47+
@Bean
48+
fun jwtEncoder(jwkSource: JWKSource<SecurityContext>): JwtEncoder {
49+
return NimbusJwtEncoder(jwkSource)
50+
}
51+
52+
/**
53+
* 클라이언트로부터 받은 JWT를 검증(디코딩)하는 `JwtDecoder`를 생성하여 Bean으로 등록합니다.
54+
* 토큰의 서명, 만료 시간(기본 검증) 및 발급자(issuer)가 일치하는지 확인합니다.
55+
*
56+
* @return 생성된 `JwtDecoder` 객체
57+
*/
58+
@Bean
59+
fun jwtDecoder(): JwtDecoder {
60+
val secretKeySpec = SecretKeySpec(secretKey.toByteArray(), SIGNATURE_ALGORITHM.name)
61+
val decoder = NimbusJwtDecoder.withSecretKey(secretKeySpec).build()
62+
val validator = JwtValidators.createDefaultWithIssuer(JwtConstants.ISSUER)
63+
decoder.setJwtValidator(validator)
64+
return decoder
65+
}
66+
67+
/**
68+
* 유효성이 검증된 JWT를 Spring Security의 `Authentication` 객체로 변환하는 `JwtAuthenticationConverter`를 등록합니다.
69+
* JWT의 'roles' 클레임을 애플리케이션의 권한 정보(`GrantedAuthority`)로 매핑하고, 'sub' 클레임을 사용자의 주체(Principal)로 설정합니다.
70+
*
71+
* @return 생성된 `JwtAuthenticationConverter` 객체
72+
*/
73+
@Bean
74+
fun jwtAuthenticationConverter(): JwtAuthenticationConverter {
75+
val converter = JwtAuthenticationConverter()
76+
converter.setJwtGrantedAuthoritiesConverter { jwt ->
77+
val roles = jwt.getClaimAsStringList(JwtConstants.ROLES_CLAIM) ?: emptyList()
78+
roles.map { role -> SimpleGrantedAuthority(role) }
79+
}
80+
converter.setPrincipalClaimName(PRINCIPAL_CLAIM)
81+
return converter
82+
}
83+
}

gateway/src/main/kotlin/org/yapp/gateway/config/SecurityConfig.kt

Lines changed: 0 additions & 47 deletions
This file was deleted.

0 commit comments

Comments
 (0)