Skip to content

Commit

Permalink
MARA-56 : Refresh Token을 위한 Redis 추가 (#24)
Browse files Browse the repository at this point in the history
* feat: redis 도입을 위한 초기 세팅

* feat: refreshToken 시간 업데이트, request 추가, 메소드분리, validation 추가

* refactor: refresh-duration-mins 추가

* Revert "refactor: refresh-duration-mins 추가"

This reverts commit 4ce43ea.

* refactor: accessToken 만료시 refreshToken 확인 후 자동 연장

* refactor: Swagger refresh-token header 추가

* refactor: RefreshToken 연장 로직 제거

* refactor: RefreshToken 연장 로직 제거

* refactor: refresh-duration-mins 수치 변경, 만료 log 추가

---------

Co-authored-by: gwon188 <[email protected]>
  • Loading branch information
Jiwon-cho and gwon188 authored Feb 16, 2024
1 parent b6fa680 commit 8ff9e59
Show file tree
Hide file tree
Showing 11 changed files with 166 additions and 33 deletions.
4 changes: 4 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ dependencies {
// db
runtimeOnly("org.mariadb.jdbc:mariadb-java-client")

// redis
implementation("org.springframework.data:spring-data-redis:3.1.2")
implementation("io.lettuce:lettuce-core:6.2.5.RELEASE")

// s3
implementation("org.springframework.cloud:spring-cloud-starter-aws:2.0.1.RELEASE")
implementation("javax.xml.bind:jaxb-api:2.3.1")
Expand Down
32 changes: 29 additions & 3 deletions src/main/kotlin/mara/server/auth/jwt/JwtFilter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import org.springframework.web.filter.OncePerRequestFilter

@Component
class JwtFilter(private val jwtProvider: JwtProvider) : OncePerRequestFilter() {
private val ok: String = "ok"
private val reissue: String = "reissue"

private fun HttpServletRequest.getToken(): String? {
val rawToken = this.getHeader("Authorization")
Expand All @@ -23,11 +25,35 @@ class JwtFilter(private val jwtProvider: JwtProvider) : OncePerRequestFilter() {
filterChain: FilterChain,
) {
val jwt = request.getToken()
if (jwt != null) {
if (jwtProvider.validate(jwt) == ok) {
SecurityContextHolder.getContext().authentication =
jwtProvider.getAuthentication(jwt)
}
if (jwtProvider.validate(jwt) == reissue) {
reissueAccessToken(request, response)
}
}

filterChain.doFilter(request, response)
}

if (jwt != null && jwtProvider.validate(jwt)) {
private fun reissueAccessToken(request: HttpServletRequest, response: HttpServletResponse) {
try {
val refreshToken = jwtProvider.validRefreshToken(request.getHeader("Refresh-Token")).refreshToken
val oldAccessToken = request.getToken()
var newAccessToken = ""
if (oldAccessToken != null) {
newAccessToken = jwtProvider.recreateAccessToken(oldAccessToken)
}
SecurityContextHolder.getContext().authentication =
jwtProvider.getAuthentication(jwt)
jwtProvider.getAuthentication(newAccessToken)

response.setHeader("New-Access-Token", newAccessToken)
response.setHeader("Refresh-Token", refreshToken)
} catch (e: Exception) {
e.printStackTrace()
request.setAttribute("exception", e)
}
filterChain.doFilter(request, response)
}
}
43 changes: 39 additions & 4 deletions src/main/kotlin/mara/server/auth/jwt/JwtProvider.kt
Original file line number Diff line number Diff line change
@@ -1,25 +1,38 @@
package mara.server.auth.jwt

import com.fasterxml.jackson.databind.ObjectMapper
import io.jsonwebtoken.ExpiredJwtException
import io.jsonwebtoken.Jwts
import io.jsonwebtoken.security.Keys
import mara.server.auth.security.PrincipalDetailsService
import mara.server.config.redis.RefreshToken
import mara.server.config.redis.RefreshTokenRepository
import mara.server.domain.user.User
import mara.server.domain.user.UserRepository
import mara.server.util.logger
import org.springframework.beans.factory.annotation.Value
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.security.core.Authentication
import org.springframework.stereotype.Component
import org.springframework.transaction.annotation.Transactional
import java.security.Key
import java.util.Base64
import java.util.Date

@Component
class JwtProvider(
@Value("\${jwt.secret-key}") private val secretKey: String,
@Value("\${jwt.access-duration-mils}") private val accessDurationMils: Long,
private val principalDetailsService: PrincipalDetailsService,
private val userRepository: UserRepository,
private val refreshTokenRepository: RefreshTokenRepository
) {
val key: Key = Keys.hmacShaKeyFor(secretKey.toByteArray())
val log = logger()
private val objectMapper = ObjectMapper()
private val ok: String = "ok"
private val reissue: String = "reissue"
private val fail: String = "fail"

fun generateToken(user: User): String {
val now = Date(System.currentTimeMillis())
Expand All @@ -39,16 +52,32 @@ class JwtProvider(
.compact()
}

fun validate(token: String): Boolean {
return try {
@Transactional
fun recreateAccessToken(oldAccessToken: String): String {
val subject = decodeJwtPayloadSubject(oldAccessToken)
val user = userRepository.findById(subject.toLong()).get()
return generateToken(user)
}

fun validRefreshToken(refreshToken: String): RefreshToken {
val token = refreshTokenRepository.findByRefreshToken(refreshToken)
?: throw NullPointerException("만료된 RefreshToken 입니다.")
return token
}

fun validate(token: String): String {
try {
Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
true
return ok
} catch (ex: ExpiredJwtException) {
log.warn("JWT 토큰 만료 [{}] {}", ex.javaClass.simpleName, ex.message)
return reissue
} catch (e: Exception) {
log.warn("JWT 오류 발생 [{}] {}", e.javaClass.simpleName, e.message)
false
return fail
}
}

Expand All @@ -66,4 +95,10 @@ class JwtProvider(
userDetails.authorities,
)
}

private fun decodeJwtPayloadSubject(oldAccessToken: String) =
objectMapper.readValue(
Base64.getUrlDecoder().decode(oldAccessToken.split('.')[1]).decodeToString(),
Map::class.java
)["sub"].toString()
}
25 changes: 25 additions & 0 deletions src/main/kotlin/mara/server/config/redis/RedisConfig.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package mara.server.config.redis

import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.data.redis.connection.RedisConnectionFactory
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory
import org.springframework.data.redis.core.RedisTemplate
import org.springframework.data.redis.repository.configuration.EnableRedisRepositories

@Configuration
@EnableRedisRepositories
class RedisConfig() {

@Bean
fun redisConnectionFactory(): RedisConnectionFactory {
return LettuceConnectionFactory()
}

@Bean
fun redisTemplate(): RedisTemplate<ByteArray, ByteArray> {
val redisTemplate = RedisTemplate<ByteArray, ByteArray>()
redisTemplate.connectionFactory = redisConnectionFactory()
return redisTemplate
}
}
19 changes: 19 additions & 0 deletions src/main/kotlin/mara/server/config/redis/RefreshToken.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package mara.server.config.redis

import org.springframework.data.annotation.Id
import org.springframework.data.redis.core.RedisHash
import org.springframework.data.redis.core.TimeToLive
import org.springframework.data.redis.core.index.Indexed
import java.util.concurrent.TimeUnit

@RedisHash(value = "refreshToken")
data class RefreshToken(
@Id
@Indexed
val refreshToken: String,

val userId: Long,

@TimeToLive(unit = TimeUnit.MINUTES)
val expiration: Int,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package mara.server.config.redis

import org.springframework.data.repository.CrudRepository

interface RefreshTokenRepository : CrudRepository<RefreshToken, String> {
fun findByRefreshToken(refreshToken: String?): RefreshToken?
}
42 changes: 23 additions & 19 deletions src/main/kotlin/mara/server/config/swagger/SwaggerConfig.kt
Original file line number Diff line number Diff line change
@@ -1,35 +1,39 @@
package mara.server.config.swagger

import io.swagger.v3.oas.models.Components
import io.swagger.v3.oas.annotations.OpenAPIDefinition
import io.swagger.v3.oas.annotations.enums.SecuritySchemeIn
import io.swagger.v3.oas.annotations.enums.SecuritySchemeType
import io.swagger.v3.oas.annotations.security.SecurityScheme
import io.swagger.v3.oas.annotations.security.SecuritySchemes
import io.swagger.v3.oas.models.OpenAPI
import io.swagger.v3.oas.models.info.Info
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

@Configuration
@OpenAPIDefinition
@SecuritySchemes(
SecurityScheme(
name = "jwtAuth",
type = SecuritySchemeType.HTTP,
scheme = "bearer",
bearerFormat = "JWT",
description = "JWT (JSON Web Token) 인증을 위한 헤더"
),
SecurityScheme(
name = "Refresh-Token",
type = SecuritySchemeType.APIKEY,
`in` = SecuritySchemeIn.HEADER,
description = "Refresh Token을 포함한 헤더"
)
)
class SwaggerConfig {
@Bean
fun openAPI(): OpenAPI {
val jwtSchemeName = "jwtAuth"
// API 요청 헤더에 인증 정보 포함
val securityRequirement = SecurityRequirement().addList(jwtSchemeName)
// SecuritySchemes 등록
val components = Components()
.addSecuritySchemes(
jwtSchemeName,
SecurityScheme()
.name(jwtSchemeName)
.type(SecurityScheme.Type.HTTP) // HTTP 방식
.scheme("bearer")
.bearerFormat("JWT")
)

return OpenAPI()
.components(Components())
.addSecurityItem(securityRequirement)
.components(components)
.addSecurityItem(SecurityRequirement().addList("jwtAuth"))
.addSecurityItem(SecurityRequirement().addList("Refresh-Token"))
.info(apiInfo())
}

Expand Down
4 changes: 2 additions & 2 deletions src/main/kotlin/mara/server/domain/user/AuthDto.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ package mara.server.domain.user
sealed class AuthDto

data class JwtDto(
val accessToken: String?,
val refreshToken: String?,
val accessToken: String,
val refreshToken: String,
) : AuthDto()

data class KakaoAuthInfo(
Expand Down
1 change: 1 addition & 0 deletions src/main/kotlin/mara/server/domain/user/UserDto.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ data class UserRequest(

data class CheckDuplicateResponse(val isDuplicated: Boolean)

data class RefreshAccessTokenRequest(val refreshToken: String)
class UserResponse(
val nickName: String?,
val kakaoId: Long?,
Expand Down
19 changes: 15 additions & 4 deletions src/main/kotlin/mara/server/domain/user/UserService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,11 @@ import mara.server.auth.google.GoogleApiClient
import mara.server.auth.jwt.JwtProvider
import mara.server.auth.kakao.KakaoApiClient
import mara.server.auth.security.getCurrentLoginUserId
import mara.server.config.redis.RefreshToken
import mara.server.config.redis.RefreshTokenRepository
import mara.server.util.StringUtil
import mara.server.util.logger
import org.springframework.beans.factory.annotation.Value
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
Expand All @@ -18,8 +21,11 @@ class UserService(
private val userRepository: UserRepository,
private val jwtProvider: JwtProvider,
private val passwordEncoder: BCryptPasswordEncoder,
private val refreshTokenRepository: RefreshTokenRepository,
private val kakaoApiClient: KakaoApiClient,
private val googleApiClient: GoogleApiClient,
@Value("\${jwt.refresh-duration-mins}") private val refreshDurationMins: Int,

) {

val log = logger()
Expand All @@ -39,9 +45,8 @@ class UserService(

SecurityContextHolder.getContext().authentication =
UsernamePasswordAuthenticationToken(authId, newUser.password)
val refreshToken = UUID.randomUUID().toString()

// JWT 발급
val refreshToken = createRefreshToken(newUser)
return JwtDto(jwtProvider.generateToken(newUser), refreshToken)
}
private fun createUser(userRequest: UserRequest): User {
Expand Down Expand Up @@ -75,7 +80,7 @@ class UserService(
SecurityContextHolder.getContext().authentication =
UsernamePasswordAuthenticationToken(authId, password)

val refreshToken = UUID.randomUUID().toString()
val refreshToken = createRefreshToken(user)
return JwtDto(jwtProvider.generateToken(user), refreshToken)
}

Expand All @@ -99,12 +104,18 @@ class UserService(
SecurityContextHolder.getContext().authentication =
UsernamePasswordAuthenticationToken(authId, authId)

val refreshToken = UUID.randomUUID().toString()
val refreshToken = createRefreshToken(user)

return JwtDto(jwtProvider.generateToken(user), refreshToken)
}

return GoogleAuthInfo(
googleEmail = authId,
)
}

fun createRefreshToken(user: User): String {
val refreshToken = refreshTokenRepository.save(RefreshToken(UUID.randomUUID().toString(), user.userId, refreshDurationMins))
return refreshToken.refreshToken
}
}
3 changes: 2 additions & 1 deletion src/main/resources/application-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ cloud:

jwt:
secret-key: ${jwt-secret-key}
access-duration-mils: 2147483647
access-duration-mils: 1800000
refresh-duration-mins: 20160

logging:
level:
Expand Down

0 comments on commit 8ff9e59

Please sign in to comment.