From 2a43f519034b33cd7c628c1d45dc7f486754444c Mon Sep 17 00:00:00 2001 From: One-armed-boy Date: Thu, 2 May 2024 18:29:20 +0900 Subject: [PATCH 01/15] =?UTF-8?q?Spring=20Security=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/build.gradle b/build.gradle index a35c755..d6d08df 100644 --- a/build.gradle +++ b/build.gradle @@ -19,13 +19,21 @@ repositories { } dependencies { + // Default implementation 'org.springframework.boot:spring-boot-starter-webflux' - implementation 'com.fasterxml.jackson.module:jackson-module-kotlin' - implementation 'io.projectreactor.kotlin:reactor-kotlin-extensions' + testImplementation 'org.springframework.boot:spring-boot-starter-test' + implementation 'org.jetbrains.kotlin:kotlin-reflect' implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-reactor' - testImplementation 'org.springframework.boot:spring-boot-starter-test' + implementation 'io.projectreactor.kotlin:reactor-kotlin-extensions' testImplementation 'io.projectreactor:reactor-test' + + // Security + implementation 'org.springframework.boot:spring-boot-starter-security' + testImplementation 'org.springframework.security:spring-security-test' + + // Util + implementation 'com.fasterxml.jackson.module:jackson-module-kotlin' } tasks.withType(KotlinCompile) { From 2d8131adc222fa434f62b29c980ca631fd64cf72 Mon Sep 17 00:00:00 2001 From: One-armed-boy Date: Fri, 3 May 2024 23:54:57 +0900 Subject: [PATCH 02/15] =?UTF-8?q?Jwt=20=ED=99=95=EC=9D=B8=20=ED=95=84?= =?UTF-8?q?=ED=84=B0=20=EC=B6=94=EA=B0=80=20=EC=9E=91=EC=97=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 6 +++ .../auth/constant/JwtMetadata.kt | 9 +++++ .../auth/jwt/AccessTokenService.kt | 20 ++++++++++ .../jwt/JwtServerAuthenticationConverter.kt | 31 +++++++++++++++ .../TiketeerWaiting/config/SecurityConfig.kt | 38 +++++++++++++++++++ 5 files changed, 104 insertions(+) create mode 100644 src/main/kotlin/com/tiketeer/TiketeerWaiting/auth/constant/JwtMetadata.kt create mode 100644 src/main/kotlin/com/tiketeer/TiketeerWaiting/auth/jwt/AccessTokenService.kt create mode 100644 src/main/kotlin/com/tiketeer/TiketeerWaiting/auth/jwt/JwtServerAuthenticationConverter.kt create mode 100644 src/main/kotlin/com/tiketeer/TiketeerWaiting/config/SecurityConfig.kt diff --git a/build.gradle b/build.gradle index d6d08df..b249f5e 100644 --- a/build.gradle +++ b/build.gradle @@ -31,6 +31,12 @@ dependencies { // Security implementation 'org.springframework.boot:spring-boot-starter-security' testImplementation 'org.springframework.security:spring-security-test' + ext { + JJWT_VERSION = "0.12.5" + } + implementation "io.jsonwebtoken:jjwt-api:${JJWT_VERSION}" + runtimeOnly "io.jsonwebtoken:jjwt-gson:${JJWT_VERSION}" + runtimeOnly "io.jsonwebtoken:jjwt-impl:${JJWT_VERSION}" // Util implementation 'com.fasterxml.jackson.module:jackson-module-kotlin' diff --git a/src/main/kotlin/com/tiketeer/TiketeerWaiting/auth/constant/JwtMetadata.kt b/src/main/kotlin/com/tiketeer/TiketeerWaiting/auth/constant/JwtMetadata.kt new file mode 100644 index 0000000..88b73af --- /dev/null +++ b/src/main/kotlin/com/tiketeer/TiketeerWaiting/auth/constant/JwtMetadata.kt @@ -0,0 +1,9 @@ +package com.tiketeer.TiketeerWaiting.auth.constant + +enum class JwtMetadata(private val value: String) { + ACCESS_TOKEN("accessToken"), REFRESH_TOKEN("refreshToken"); + + fun value(): String { + return this.value + } +} diff --git a/src/main/kotlin/com/tiketeer/TiketeerWaiting/auth/jwt/AccessTokenService.kt b/src/main/kotlin/com/tiketeer/TiketeerWaiting/auth/jwt/AccessTokenService.kt new file mode 100644 index 0000000..0b7f595 --- /dev/null +++ b/src/main/kotlin/com/tiketeer/TiketeerWaiting/auth/jwt/AccessTokenService.kt @@ -0,0 +1,20 @@ +package com.tiketeer.TiketeerWaiting.auth.jwt + +import io.jsonwebtoken.Jwts +import io.jsonwebtoken.io.Decoders +import io.jsonwebtoken.security.Keys +import org.springframework.beans.factory.annotation.Value +import org.springframework.stereotype.Service +import javax.crypto.SecretKey + +@Service +class AccessTokenService(@Value("\${jwt.secret - key}") secretKey: String) { + private val secretKey: SecretKey = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretKey)) + + fun verifyToken(accessToken: String): AccessTokenPayload { + val claims = Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(accessToken).payload + return AccessTokenPayload(claims.subject) + } + + data class AccessTokenPayload(val email: String) +} diff --git a/src/main/kotlin/com/tiketeer/TiketeerWaiting/auth/jwt/JwtServerAuthenticationConverter.kt b/src/main/kotlin/com/tiketeer/TiketeerWaiting/auth/jwt/JwtServerAuthenticationConverter.kt new file mode 100644 index 0000000..2ded285 --- /dev/null +++ b/src/main/kotlin/com/tiketeer/TiketeerWaiting/auth/jwt/JwtServerAuthenticationConverter.kt @@ -0,0 +1,31 @@ +package com.tiketeer.TiketeerWaiting.auth.jwt + +import com.tiketeer.TiketeerWaiting.auth.constant.JwtMetadata +import org.springframework.http.HttpCookie +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken +import org.springframework.security.core.Authentication +import org.springframework.security.web.server.authentication.ServerAuthenticationConverter +import org.springframework.stereotype.Component +import org.springframework.web.server.ServerWebExchange +import reactor.core.publisher.Mono + +@Component +class JwtServerAuthenticationConverter( + private val accessTokenService: AccessTokenService +): ServerAuthenticationConverter { + override fun convert(exchange: ServerWebExchange): Mono { + return Mono.justOrEmpty(extractAccessToken(exchange)) + .map(HttpCookie::toString) + .map(accessTokenService::verifyToken) + .map { payload -> payload.email } + .map(this::createAuthentication) + } + + private fun extractAccessToken(exchange: ServerWebExchange): HttpCookie? { + return exchange.request.cookies.getFirst(JwtMetadata.ACCESS_TOKEN.value()) + } + + private fun createAuthentication(email: String): Authentication { + return UsernamePasswordAuthenticationToken(email, null) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/tiketeer/TiketeerWaiting/config/SecurityConfig.kt b/src/main/kotlin/com/tiketeer/TiketeerWaiting/config/SecurityConfig.kt new file mode 100644 index 0000000..7c3afe5 --- /dev/null +++ b/src/main/kotlin/com/tiketeer/TiketeerWaiting/config/SecurityConfig.kt @@ -0,0 +1,38 @@ +package com.tiketeer.TiketeerWaiting.config; + +import com.tiketeer.TiketeerWaiting.auth.jwt.JwtServerAuthenticationConverter +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.ReactiveAuthenticationManager +import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity +import org.springframework.security.config.web.server.SecurityWebFiltersOrder +import org.springframework.security.config.web.server.ServerHttpSecurity +import org.springframework.security.config.web.server.ServerHttpSecurity.CsrfSpec +import org.springframework.security.config.web.server.ServerHttpSecurity.FormLoginSpec +import org.springframework.security.config.web.server.ServerHttpSecurity.HttpBasicSpec +import org.springframework.security.web.server.SecurityWebFilterChain; +import org.springframework.security.web.server.authentication.AuthenticationWebFilter + +@Configuration +@EnableWebFluxSecurity +class SecurityConfig( + authenticationManager: ReactiveAuthenticationManager, + serverAuthenticationConverter: JwtServerAuthenticationConverter +) { + private val authenticationWebFilter: AuthenticationWebFilter = AuthenticationWebFilter(authenticationManager) + init { + authenticationWebFilter.setServerAuthenticationConverter(serverAuthenticationConverter) + } + @Bean + fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + return http + .csrf(CsrfSpec::disable) + .formLogin(FormLoginSpec::disable) + .httpBasic(HttpBasicSpec::disable) + .addFilterAt(authenticationWebFilter, SecurityWebFiltersOrder.AUTHENTICATION) + .authorizeExchange { + e -> e.anyExchange().authenticated() + } + .build() + } +} From 7ba58180ac7fd916830830192f29b3a2e13bf93a Mon Sep 17 00:00:00 2001 From: One-armed-boy Date: Fri, 3 May 2024 23:55:30 +0900 Subject: [PATCH 03/15] =?UTF-8?q?SecurityContext=EB=A1=9C=20=EB=B6=80?= =?UTF-8?q?=ED=84=B0=20Email=EC=9D=84=20=EA=B0=80=EC=A0=B8=EC=98=A4?= =?UTF-8?q?=EB=8A=94=20=EC=9E=91=EC=97=85=20=EB=A1=9C=EC=A7=81=EC=9D=84=20?= =?UTF-8?q?WaitingController=20=EB=82=B4=20=EC=9E=91=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../waiting/controller/WaitingController.kt | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/main/kotlin/com/tiketeer/TiketeerWaiting/domain/waiting/controller/WaitingController.kt b/src/main/kotlin/com/tiketeer/TiketeerWaiting/domain/waiting/controller/WaitingController.kt index 7d83234..6ba54e7 100644 --- a/src/main/kotlin/com/tiketeer/TiketeerWaiting/domain/waiting/controller/WaitingController.kt +++ b/src/main/kotlin/com/tiketeer/TiketeerWaiting/domain/waiting/controller/WaitingController.kt @@ -3,10 +3,12 @@ package com.tiketeer.TiketeerWaiting.domain.waiting.controller import com.tiketeer.TiketeerWaiting.domain.waiting.controller.dto.GetRankAndTokenResponseDto import com.tiketeer.TiketeerWaiting.domain.waiting.usecase.GetRankAndToken import com.tiketeer.TiketeerWaiting.domain.waiting.usecase.dto.GetRankAndTokenCommandDto +import org.springframework.security.core.Authentication import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RestController +import reactor.core.publisher.Mono import java.util.UUID @RestController @@ -15,10 +17,14 @@ class WaitingController( private val getRankAndTokenUseCase: GetRankAndToken ) { @GetMapping - fun getRankAndToken(@RequestParam(required = true) ticketingId: UUID): GetRankAndTokenResponseDto { - // TODO: JWT 디코딩 필터 적용 후 JWT 내에서 가져오도록 수정 - val email = "test@test.com" - val result = getRankAndTokenUseCase.getRankAndToken(GetRankAndTokenCommandDto(email, ticketingId)) - return GetRankAndTokenResponseDto.convertFromDto(result) + fun getRankAndToken( + authentication: Mono, + @RequestParam(required = true) ticketingId: UUID + ): Mono { + return authentication + .map { auth -> auth.name } + .map { email -> GetRankAndTokenCommandDto(email, ticketingId) } + .map(getRankAndTokenUseCase::getRankAndToken) + .map(GetRankAndTokenResponseDto::convertFromDto) } } \ No newline at end of file From 524b92ce3f3f2e3deb694a9754f2c276860d5ed9 Mon Sep 17 00:00:00 2001 From: One-armed-boy Date: Sun, 5 May 2024 13:16:54 +0900 Subject: [PATCH 04/15] =?UTF-8?q?=EB=B6=88=ED=95=84=EC=9A=94=ED=95=9C=20im?= =?UTF-8?q?port=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/waiting/controller/WaitingController.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/kotlin/com/tiketeer/TiketeerWaiting/domain/waiting/controller/WaitingController.kt b/src/main/kotlin/com/tiketeer/TiketeerWaiting/domain/waiting/controller/WaitingController.kt index 2081d7b..fa4fb82 100644 --- a/src/main/kotlin/com/tiketeer/TiketeerWaiting/domain/waiting/controller/WaitingController.kt +++ b/src/main/kotlin/com/tiketeer/TiketeerWaiting/domain/waiting/controller/WaitingController.kt @@ -9,7 +9,6 @@ import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestParam import org.springframework.web.bind.annotation.RestController import reactor.core.publisher.Mono -import java.time.LocalDateTime import java.util.UUID @RestController From ec276c4abe260e836a3a9256b5603a637b115050 Mon Sep 17 00:00:00 2001 From: One-armed-boy Date: Sun, 5 May 2024 19:24:52 +0900 Subject: [PATCH 05/15] =?UTF-8?q?jwt.secret-key=20=ED=99=98=EA=B2=BD?= =?UTF-8?q?=EB=B3=80=EC=88=98=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tiketeer/TiketeerWaiting/auth/jwt/AccessTokenService.kt | 2 +- src/test/resources/application.yml | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/com/tiketeer/TiketeerWaiting/auth/jwt/AccessTokenService.kt b/src/main/kotlin/com/tiketeer/TiketeerWaiting/auth/jwt/AccessTokenService.kt index 0b7f595..1c71ef9 100644 --- a/src/main/kotlin/com/tiketeer/TiketeerWaiting/auth/jwt/AccessTokenService.kt +++ b/src/main/kotlin/com/tiketeer/TiketeerWaiting/auth/jwt/AccessTokenService.kt @@ -8,7 +8,7 @@ import org.springframework.stereotype.Service import javax.crypto.SecretKey @Service -class AccessTokenService(@Value("\${jwt.secret - key}") secretKey: String) { +class AccessTokenService(@Value("\${jwt.secret-key}") secretKey: String) { private val secretKey: SecretKey = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretKey)) fun verifyToken(accessToken: String): AccessTokenPayload { diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml index 94448e0..580c246 100644 --- a/src/test/resources/application.yml +++ b/src/test/resources/application.yml @@ -14,4 +14,7 @@ spring: maxmemory: 10 waiting: - entry-size: 3 \ No newline at end of file + entry-size: 3 + +jwt: + secret-key: 68895db81e621a83a1ab3d9892c24e8c6478bfe8b23fa47d324a54770c081630ed270acaf45b76456b36935c46cdffdba2d22bee94126b43a015f82c36333d3c From d29589d81535aab30e493a74b25cb9a0a6e842c9 Mon Sep 17 00:00:00 2001 From: One-armed-boy Date: Sun, 5 May 2024 19:30:52 +0900 Subject: [PATCH 06/15] =?UTF-8?q?Config=20=ED=8C=8C=EC=9D=BC=20=EA=B2=BD?= =?UTF-8?q?=EB=A1=9C=20=EB=B3=80=EA=B2=BD=20config=20->=20configuration?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TiketeerWaiting/{config => configuration}/SecurityConfig.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/main/kotlin/com/tiketeer/TiketeerWaiting/{config => configuration}/SecurityConfig.kt (97%) diff --git a/src/main/kotlin/com/tiketeer/TiketeerWaiting/config/SecurityConfig.kt b/src/main/kotlin/com/tiketeer/TiketeerWaiting/configuration/SecurityConfig.kt similarity index 97% rename from src/main/kotlin/com/tiketeer/TiketeerWaiting/config/SecurityConfig.kt rename to src/main/kotlin/com/tiketeer/TiketeerWaiting/configuration/SecurityConfig.kt index 7c3afe5..0ec60fe 100644 --- a/src/main/kotlin/com/tiketeer/TiketeerWaiting/config/SecurityConfig.kt +++ b/src/main/kotlin/com/tiketeer/TiketeerWaiting/configuration/SecurityConfig.kt @@ -1,4 +1,4 @@ -package com.tiketeer.TiketeerWaiting.config; +package com.tiketeer.TiketeerWaiting.configuration; import com.tiketeer.TiketeerWaiting.auth.jwt.JwtServerAuthenticationConverter import org.springframework.context.annotation.Bean; From 6c7fc106805f0bb5f75ef38c6dc3c0bfa3eeed71 Mon Sep 17 00:00:00 2001 From: One-armed-boy Date: Sun, 5 May 2024 19:53:21 +0900 Subject: [PATCH 07/15] =?UTF-8?q?redis=20=EA=B4=80=EB=A0=A8=20=ED=99=98?= =?UTF-8?q?=EA=B2=BD=20=EB=B3=80=EC=88=98=20=EA=B2=BD=EB=A1=9C=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tiketeer/TiketeerWaiting/configuration/RedisConfig.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/com/tiketeer/TiketeerWaiting/configuration/RedisConfig.kt b/src/main/kotlin/com/tiketeer/TiketeerWaiting/configuration/RedisConfig.kt index c5b0805..edbaf53 100644 --- a/src/main/kotlin/com/tiketeer/TiketeerWaiting/configuration/RedisConfig.kt +++ b/src/main/kotlin/com/tiketeer/TiketeerWaiting/configuration/RedisConfig.kt @@ -19,13 +19,13 @@ import org.springframework.data.redis.serializer.RedisSerializationContext @Configuration @Profile("!test") class RedisConfig { - @Value("\${spring.redis.host}") + @Value("\${spring.data.redis.host}") private lateinit var host: String - @Value("\${spring.redis.port}") + @Value("\${spring.data.redis.port}") private lateinit var port: Number - @Value("\${spring.redis.password}") + @Value("\${spring.data.redis.password}") private lateinit var password: String @Bean From c163ad368cf50b27fd74e7840729ed1d42468c88 Mon Sep 17 00:00:00 2001 From: One-armed-boy Date: Sun, 5 May 2024 19:53:44 +0900 Subject: [PATCH 08/15] =?UTF-8?q?GetRankAndTokenUseCase=20=EB=82=B4=20?= =?UTF-8?q?=ED=86=A0=ED=81=B0=20=EC=83=9D=EC=84=B1=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/waiting/usecase/GetRankAndTokenUseCase.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/kotlin/com/tiketeer/TiketeerWaiting/domain/waiting/usecase/GetRankAndTokenUseCase.kt b/src/main/kotlin/com/tiketeer/TiketeerWaiting/domain/waiting/usecase/GetRankAndTokenUseCase.kt index a782c20..2f36d2b 100644 --- a/src/main/kotlin/com/tiketeer/TiketeerWaiting/domain/waiting/usecase/GetRankAndTokenUseCase.kt +++ b/src/main/kotlin/com/tiketeer/TiketeerWaiting/domain/waiting/usecase/GetRankAndTokenUseCase.kt @@ -16,7 +16,7 @@ class GetRankAndTokenUseCase @Autowired constructor(private val redisTemplate: R override fun getRankAndToken(dto: GetRankAndTokenCommandDto): Mono { val currentTime = dto.entryTime - val token = generateToken(dto.email, dto.ticketingId, currentTime) + val token = generateToken(dto.email, dto.ticketingId) val mono = redisTemplate.opsForZSet().rank(dto.ticketingId.toString(), token) .switchIfEmpty( redisTemplate.opsForZSet().add(dto.ticketingId.toString(), token, currentTime.toDouble()) @@ -26,7 +26,7 @@ class GetRankAndTokenUseCase @Autowired constructor(private val redisTemplate: R val ret: Mono = mono .flatMap { l -> if (l < entrySize.toInt()) { - Mono.just(GetRankAndTokenResultDto(l, generateToken(dto.email, dto.ticketingId, currentTime))) + Mono.just(GetRankAndTokenResultDto(l, generateToken(dto.email, dto.ticketingId))) } else { Mono.just(GetRankAndTokenResultDto(l)) } @@ -35,7 +35,7 @@ class GetRankAndTokenUseCase @Autowired constructor(private val redisTemplate: R return ret } - private fun generateToken(email: String, ticketingId: UUID, entryTime: Long) : String { + private fun generateToken(email: String, ticketingId: UUID) : String { return "${email}:${ticketingId}" } } \ No newline at end of file From ba487497fb4d5b841b20d63de1d8ef068f5a325b Mon Sep 17 00:00:00 2001 From: One-armed-boy Date: Sun, 5 May 2024 21:33:58 +0900 Subject: [PATCH 09/15] =?UTF-8?q?JwtAuthenticationManager=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/jwt/JwtAuthenticationManager.kt | 14 ++++++++++++++ .../configuration/SecurityConfig.kt | 4 ++-- 2 files changed, 16 insertions(+), 2 deletions(-) create mode 100644 src/main/kotlin/com/tiketeer/TiketeerWaiting/auth/jwt/JwtAuthenticationManager.kt diff --git a/src/main/kotlin/com/tiketeer/TiketeerWaiting/auth/jwt/JwtAuthenticationManager.kt b/src/main/kotlin/com/tiketeer/TiketeerWaiting/auth/jwt/JwtAuthenticationManager.kt new file mode 100644 index 0000000..9b10f63 --- /dev/null +++ b/src/main/kotlin/com/tiketeer/TiketeerWaiting/auth/jwt/JwtAuthenticationManager.kt @@ -0,0 +1,14 @@ +package com.tiketeer.TiketeerWaiting.auth.jwt + +import org.springframework.security.authentication.ReactiveAuthenticationManager +import org.springframework.security.core.Authentication +import org.springframework.stereotype.Component +import reactor.core.publisher.Mono + +@Component +class JwtAuthenticationManager: ReactiveAuthenticationManager { + override fun authenticate(authentication: Authentication?): Mono { + return Mono.justOrEmpty(authentication) + .filter { auth -> auth.principal is String && auth.principal != "" } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/tiketeer/TiketeerWaiting/configuration/SecurityConfig.kt b/src/main/kotlin/com/tiketeer/TiketeerWaiting/configuration/SecurityConfig.kt index 0ec60fe..6ae5721 100644 --- a/src/main/kotlin/com/tiketeer/TiketeerWaiting/configuration/SecurityConfig.kt +++ b/src/main/kotlin/com/tiketeer/TiketeerWaiting/configuration/SecurityConfig.kt @@ -1,9 +1,9 @@ package com.tiketeer.TiketeerWaiting.configuration; +import com.tiketeer.TiketeerWaiting.auth.jwt.JwtAuthenticationManager import com.tiketeer.TiketeerWaiting.auth.jwt.JwtServerAuthenticationConverter import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.security.authentication.ReactiveAuthenticationManager import org.springframework.security.config.annotation.web.reactive.EnableWebFluxSecurity import org.springframework.security.config.web.server.SecurityWebFiltersOrder import org.springframework.security.config.web.server.ServerHttpSecurity @@ -16,7 +16,7 @@ import org.springframework.security.web.server.authentication.AuthenticationWebF @Configuration @EnableWebFluxSecurity class SecurityConfig( - authenticationManager: ReactiveAuthenticationManager, + authenticationManager: JwtAuthenticationManager, serverAuthenticationConverter: JwtServerAuthenticationConverter ) { private val authenticationWebFilter: AuthenticationWebFilter = AuthenticationWebFilter(authenticationManager) From c632ec77b4b12d68149b09557e77d97251c8e1f8 Mon Sep 17 00:00:00 2001 From: One-armed-boy Date: Mon, 6 May 2024 00:08:02 +0900 Subject: [PATCH 10/15] =?UTF-8?q?JWT=20=EC=9D=B8=EC=A6=9D=20=EB=82=B4=20Ro?= =?UTF-8?q?le=20=EA=B4=80=EB=A0=A8=20=EB=82=B4=EC=9A=A9=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TiketeerWaiting/auth/jwt/AccessTokenService.kt | 8 +++++--- .../auth/jwt/JwtServerAuthenticationConverter.kt | 12 ++++++++---- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/src/main/kotlin/com/tiketeer/TiketeerWaiting/auth/jwt/AccessTokenService.kt b/src/main/kotlin/com/tiketeer/TiketeerWaiting/auth/jwt/AccessTokenService.kt index 1c71ef9..5f6786d 100644 --- a/src/main/kotlin/com/tiketeer/TiketeerWaiting/auth/jwt/AccessTokenService.kt +++ b/src/main/kotlin/com/tiketeer/TiketeerWaiting/auth/jwt/AccessTokenService.kt @@ -5,6 +5,7 @@ import io.jsonwebtoken.io.Decoders import io.jsonwebtoken.security.Keys import org.springframework.beans.factory.annotation.Value import org.springframework.stereotype.Service +import java.util.* import javax.crypto.SecretKey @Service @@ -12,9 +13,10 @@ class AccessTokenService(@Value("\${jwt.secret-key}") secretKey: String) { private val secretKey: SecretKey = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretKey)) fun verifyToken(accessToken: String): AccessTokenPayload { - val claims = Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(accessToken).payload - return AccessTokenPayload(claims.subject) + val payload = Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(accessToken).payload + val role = payload.get("role", String::class.java) + return AccessTokenPayload(payload.subject, role) } - data class AccessTokenPayload(val email: String) + data class AccessTokenPayload(val email: String, val role: String) } diff --git a/src/main/kotlin/com/tiketeer/TiketeerWaiting/auth/jwt/JwtServerAuthenticationConverter.kt b/src/main/kotlin/com/tiketeer/TiketeerWaiting/auth/jwt/JwtServerAuthenticationConverter.kt index 2ded285..ad0cbb3 100644 --- a/src/main/kotlin/com/tiketeer/TiketeerWaiting/auth/jwt/JwtServerAuthenticationConverter.kt +++ b/src/main/kotlin/com/tiketeer/TiketeerWaiting/auth/jwt/JwtServerAuthenticationConverter.kt @@ -4,6 +4,7 @@ import com.tiketeer.TiketeerWaiting.auth.constant.JwtMetadata import org.springframework.http.HttpCookie import org.springframework.security.authentication.UsernamePasswordAuthenticationToken import org.springframework.security.core.Authentication +import org.springframework.security.core.authority.SimpleGrantedAuthority import org.springframework.security.web.server.authentication.ServerAuthenticationConverter import org.springframework.stereotype.Component import org.springframework.web.server.ServerWebExchange @@ -15,9 +16,8 @@ class JwtServerAuthenticationConverter( ): ServerAuthenticationConverter { override fun convert(exchange: ServerWebExchange): Mono { return Mono.justOrEmpty(extractAccessToken(exchange)) - .map(HttpCookie::toString) + .map { cookie -> cookie.value } .map(accessTokenService::verifyToken) - .map { payload -> payload.email } .map(this::createAuthentication) } @@ -25,7 +25,11 @@ class JwtServerAuthenticationConverter( return exchange.request.cookies.getFirst(JwtMetadata.ACCESS_TOKEN.value()) } - private fun createAuthentication(email: String): Authentication { - return UsernamePasswordAuthenticationToken(email, null) + private fun createAuthentication(payload: AccessTokenService.AccessTokenPayload): Authentication { + return UsernamePasswordAuthenticationToken( + payload.email, + null, + listOf(SimpleGrantedAuthority(payload.role)) + ) } } \ No newline at end of file From 7a6d1c1bfef5c51fe0155d9def25ce3f1f62d78d Mon Sep 17 00:00:00 2001 From: One-armed-boy Date: Mon, 6 May 2024 00:08:46 +0900 Subject: [PATCH 11/15] =?UTF-8?q?Spring=20Security=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?=EB=82=B4=20=EC=84=B8=EC=85=98=20=EA=B4=80=EB=A0=A8=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95=20=EC=B6=94=EA=B0=80(Stateless)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../configuration/SecurityConfig.kt | 20 ++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/main/kotlin/com/tiketeer/TiketeerWaiting/configuration/SecurityConfig.kt b/src/main/kotlin/com/tiketeer/TiketeerWaiting/configuration/SecurityConfig.kt index 6ae5721..75b5453 100644 --- a/src/main/kotlin/com/tiketeer/TiketeerWaiting/configuration/SecurityConfig.kt +++ b/src/main/kotlin/com/tiketeer/TiketeerWaiting/configuration/SecurityConfig.kt @@ -12,23 +12,25 @@ import org.springframework.security.config.web.server.ServerHttpSecurity.FormLog import org.springframework.security.config.web.server.ServerHttpSecurity.HttpBasicSpec import org.springframework.security.web.server.SecurityWebFilterChain; import org.springframework.security.web.server.authentication.AuthenticationWebFilter +import org.springframework.security.web.server.context.NoOpServerSecurityContextRepository @Configuration @EnableWebFluxSecurity -class SecurityConfig( - authenticationManager: JwtAuthenticationManager, - serverAuthenticationConverter: JwtServerAuthenticationConverter -) { - private val authenticationWebFilter: AuthenticationWebFilter = AuthenticationWebFilter(authenticationManager) - init { - authenticationWebFilter.setServerAuthenticationConverter(serverAuthenticationConverter) - } +class SecurityConfig { @Bean - fun securityWebFilterChain(http: ServerHttpSecurity): SecurityWebFilterChain { + fun securityWebFilterChain( + http: ServerHttpSecurity, + authenticationManager: JwtAuthenticationManager, + serverAuthenticationConverter: JwtServerAuthenticationConverter + ): SecurityWebFilterChain { + val authenticationWebFilter = AuthenticationWebFilter(authenticationManager) + authenticationWebFilter.setServerAuthenticationConverter(serverAuthenticationConverter) + return http .csrf(CsrfSpec::disable) .formLogin(FormLoginSpec::disable) .httpBasic(HttpBasicSpec::disable) + .securityContextRepository(NoOpServerSecurityContextRepository.getInstance()) .addFilterAt(authenticationWebFilter, SecurityWebFiltersOrder.AUTHENTICATION) .authorizeExchange { e -> e.anyExchange().authenticated() From 8d69fce40e1560434efd450c327c7c890c093e8b Mon Sep 17 00:00:00 2001 From: One-armed-boy Date: Mon, 6 May 2024 00:08:53 +0900 Subject: [PATCH 12/15] =?UTF-8?q?WaitingControllerTest=20=EC=9E=91?= =?UTF-8?q?=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/WaitingControllerTest.kt | 112 ++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 src/test/kotlin/com/tiketeer/TiketeerWaiting/domain/waiting/controller/WaitingControllerTest.kt diff --git a/src/test/kotlin/com/tiketeer/TiketeerWaiting/domain/waiting/controller/WaitingControllerTest.kt b/src/test/kotlin/com/tiketeer/TiketeerWaiting/domain/waiting/controller/WaitingControllerTest.kt new file mode 100644 index 0000000..7968b34 --- /dev/null +++ b/src/test/kotlin/com/tiketeer/TiketeerWaiting/domain/waiting/controller/WaitingControllerTest.kt @@ -0,0 +1,112 @@ +package com.tiketeer.TiketeerWaiting.domain.waiting.controller + +import com.tiketeer.TiketeerWaiting.auth.constant.JwtMetadata +import com.tiketeer.TiketeerWaiting.configuration.EmbeddedRedisConfig +import com.tiketeer.TiketeerWaiting.domain.waiting.usecase.GetRankAndTokenUseCase +import com.tiketeer.TiketeerWaiting.domain.waiting.usecase.dto.GetRankAndTokenCommandDto +import io.jsonwebtoken.Jwts +import io.jsonwebtoken.io.Decoders +import io.jsonwebtoken.security.Keys +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.beans.factory.annotation.Value +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.context.annotation.Import +import org.springframework.data.redis.connection.ReactiveRedisConnectionFactory +import org.springframework.test.web.reactive.server.WebTestClient +import java.util.Date +import java.util.UUID + +@Import(EmbeddedRedisConfig::class) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +class WaitingControllerTest { + @Autowired + lateinit var webTestClient: WebTestClient + + @Autowired + lateinit var redisConnectionFactory: ReactiveRedisConnectionFactory + + @Autowired + lateinit var getRankAndTokenUseCase: GetRankAndTokenUseCase + + @Value("\${jwt.secret-key}") + lateinit var jwtSecretKey: String + + @Value("\${waiting.entry-size}") + lateinit var entrySize: Number + + @BeforeEach + fun init() { + val flushDb = redisConnectionFactory.reactiveConnection.serverCommands().flushDb() + flushDb.block() + } + + @Test + fun `토큰이 없는 유저 - waiting 요청 - 호출 실패`() { + // given + val ticketingId = UUID.randomUUID() + // when + webTestClient.get().uri("/waiting?ticketingId=$ticketingId") + // then + .exchange() + .expectStatus().isUnauthorized() + } + + @Test + fun `토큰이 있는 유저 - 빈 대기열에 waiting 요청 - 토큰 반환`() { + // given + val email = "test@test.com" + val role = "USER" + val ticketingId = UUID.randomUUID() + + // when + webTestClient.get().uri("/waiting?ticketingId=$ticketingId") + .cookie(JwtMetadata.ACCESS_TOKEN.value(), createAccessToken(email, role, Date())) + // then + .exchange() + .expectStatus().isOk() + .expectBody() + .jsonPath("rank").isEqualTo(0L) + .jsonPath("token").isEqualTo(createPurchaseToken(email, ticketingId)) + } + + @Test + fun `토큰이 있는 유저 - 가득찬 대기열에 waiting 요청 - 토큰 반환 X`() { + // given + val ticketingId = UUID.randomUUID() + for (i in 1..entrySize.toInt()) { + val email = "test${i}@test.com" + val entryTime = System.currentTimeMillis() + val result = getRankAndTokenUseCase.getRankAndToken(GetRankAndTokenCommandDto(email, ticketingId, entryTime)) + result.block() + } + val email = "test@test.com" + val role = "USER" + + // when + webTestClient.get().uri("/waiting?ticketingId=$ticketingId") + .cookie(JwtMetadata.ACCESS_TOKEN.value(), createAccessToken(email, role, Date())) + // then + .exchange() + .expectStatus().isOk() + .expectBody() + .jsonPath("rank").isEqualTo(entrySize.toLong()) + .jsonPath("token").isEmpty() + } + + private fun createPurchaseToken(email: String, ticketingId: UUID): String { + return "$email:$ticketingId" + } + + private fun createAccessToken(email: String, role: String, issuedAt: Date): String { + return Jwts.builder() + .subject(email) + .claim("role", role) + .issuer("tester") + .issuedAt(issuedAt) + .expiration(Date(issuedAt.time + 3 * 60 * 1000)) + .signWith(Keys.hmacShaKeyFor(Decoders.BASE64.decode(jwtSecretKey))) + .compact(); + } +} \ No newline at end of file From bce884d4976a61f0d27890d78205405dde0ad8b0 Mon Sep 17 00:00:00 2001 From: One-armed-boy Date: Mon, 6 May 2024 10:16:06 +0900 Subject: [PATCH 13/15] =?UTF-8?q?=EB=B6=88=ED=95=84=EC=9A=94=ED=95=9C=20im?= =?UTF-8?q?port=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/tiketeer/TiketeerWaiting/auth/jwt/AccessTokenService.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/kotlin/com/tiketeer/TiketeerWaiting/auth/jwt/AccessTokenService.kt b/src/main/kotlin/com/tiketeer/TiketeerWaiting/auth/jwt/AccessTokenService.kt index 5f6786d..334166f 100644 --- a/src/main/kotlin/com/tiketeer/TiketeerWaiting/auth/jwt/AccessTokenService.kt +++ b/src/main/kotlin/com/tiketeer/TiketeerWaiting/auth/jwt/AccessTokenService.kt @@ -5,7 +5,6 @@ import io.jsonwebtoken.io.Decoders import io.jsonwebtoken.security.Keys import org.springframework.beans.factory.annotation.Value import org.springframework.stereotype.Service -import java.util.* import javax.crypto.SecretKey @Service From 9e1eafa6c0c6556e6eddc635bb99a7319f27fb76 Mon Sep 17 00:00:00 2001 From: One-armed-boy Date: Mon, 6 May 2024 16:42:10 +0900 Subject: [PATCH 14/15] =?UTF-8?q?AccessTokenService=20=EA=B0=80=20Mono?= =?UTF-8?q?=EB=A5=BC=20=EB=B0=98=ED=99=98=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TiketeerWaiting/auth/jwt/AccessTokenService.kt | 9 +++++---- .../auth/jwt/JwtServerAuthenticationConverter.kt | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/main/kotlin/com/tiketeer/TiketeerWaiting/auth/jwt/AccessTokenService.kt b/src/main/kotlin/com/tiketeer/TiketeerWaiting/auth/jwt/AccessTokenService.kt index 334166f..51dfd03 100644 --- a/src/main/kotlin/com/tiketeer/TiketeerWaiting/auth/jwt/AccessTokenService.kt +++ b/src/main/kotlin/com/tiketeer/TiketeerWaiting/auth/jwt/AccessTokenService.kt @@ -5,16 +5,17 @@ import io.jsonwebtoken.io.Decoders import io.jsonwebtoken.security.Keys import org.springframework.beans.factory.annotation.Value import org.springframework.stereotype.Service +import reactor.core.publisher.Mono import javax.crypto.SecretKey @Service class AccessTokenService(@Value("\${jwt.secret-key}") secretKey: String) { private val secretKey: SecretKey = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretKey)) - fun verifyToken(accessToken: String): AccessTokenPayload { - val payload = Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(accessToken).payload - val role = payload.get("role", String::class.java) - return AccessTokenPayload(payload.subject, role) + fun verifyToken(accessToken: String): Mono { + return Mono.just(accessToken) + .map {token -> Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).payload} + .map {payload -> AccessTokenPayload(payload.subject, payload.get("role", String::class.java))} } data class AccessTokenPayload(val email: String, val role: String) diff --git a/src/main/kotlin/com/tiketeer/TiketeerWaiting/auth/jwt/JwtServerAuthenticationConverter.kt b/src/main/kotlin/com/tiketeer/TiketeerWaiting/auth/jwt/JwtServerAuthenticationConverter.kt index ad0cbb3..d55cb84 100644 --- a/src/main/kotlin/com/tiketeer/TiketeerWaiting/auth/jwt/JwtServerAuthenticationConverter.kt +++ b/src/main/kotlin/com/tiketeer/TiketeerWaiting/auth/jwt/JwtServerAuthenticationConverter.kt @@ -17,7 +17,7 @@ class JwtServerAuthenticationConverter( override fun convert(exchange: ServerWebExchange): Mono { return Mono.justOrEmpty(extractAccessToken(exchange)) .map { cookie -> cookie.value } - .map(accessTokenService::verifyToken) + .flatMap(accessTokenService::verifyToken) .map(this::createAuthentication) } From 31b90fa55381a3b8863ed2aab1f78b1f3b4dd906 Mon Sep 17 00:00:00 2001 From: One-armed-boy Date: Mon, 6 May 2024 17:04:02 +0900 Subject: [PATCH 15/15] =?UTF-8?q?indent=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../tiketeer/TiketeerWaiting/auth/constant/JwtMetadata.kt | 8 ++++---- .../TiketeerWaiting/auth/jwt/AccessTokenService.kt | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/main/kotlin/com/tiketeer/TiketeerWaiting/auth/constant/JwtMetadata.kt b/src/main/kotlin/com/tiketeer/TiketeerWaiting/auth/constant/JwtMetadata.kt index 88b73af..3a46c39 100644 --- a/src/main/kotlin/com/tiketeer/TiketeerWaiting/auth/constant/JwtMetadata.kt +++ b/src/main/kotlin/com/tiketeer/TiketeerWaiting/auth/constant/JwtMetadata.kt @@ -1,9 +1,9 @@ package com.tiketeer.TiketeerWaiting.auth.constant enum class JwtMetadata(private val value: String) { - ACCESS_TOKEN("accessToken"), REFRESH_TOKEN("refreshToken"); + ACCESS_TOKEN("accessToken"), REFRESH_TOKEN("refreshToken"); - fun value(): String { - return this.value - } + fun value(): String { + return this.value + } } diff --git a/src/main/kotlin/com/tiketeer/TiketeerWaiting/auth/jwt/AccessTokenService.kt b/src/main/kotlin/com/tiketeer/TiketeerWaiting/auth/jwt/AccessTokenService.kt index 51dfd03..674ed3f 100644 --- a/src/main/kotlin/com/tiketeer/TiketeerWaiting/auth/jwt/AccessTokenService.kt +++ b/src/main/kotlin/com/tiketeer/TiketeerWaiting/auth/jwt/AccessTokenService.kt @@ -10,13 +10,13 @@ import javax.crypto.SecretKey @Service class AccessTokenService(@Value("\${jwt.secret-key}") secretKey: String) { - private val secretKey: SecretKey = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretKey)) + private val secretKey: SecretKey = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretKey)) - fun verifyToken(accessToken: String): Mono { + fun verifyToken(accessToken: String): Mono { return Mono.just(accessToken) .map {token -> Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).payload} .map {payload -> AccessTokenPayload(payload.subject, payload.get("role", String::class.java))} - } + } - data class AccessTokenPayload(val email: String, val role: String) + data class AccessTokenPayload(val email: String, val role: String) }