Skip to content
This repository has been archived by the owner on Aug 13, 2022. It is now read-only.

[#8] refresh token 생성기능 구현 #13

Open
wants to merge 4 commits into
base: ft/6
Choose a base branch
from
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
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ dependencies {
tasks.withType<KotlinCompile> {
kotlinOptions {
freeCompilerArgs = listOf("-Xjsr305=strict")
jvmTarget = "11"
jvmTarget = "1.8"
}
}

Expand Down
Empty file modified gradlew
100644 → 100755
Empty file.
2 changes: 1 addition & 1 deletion src/main/kotlin/com/kotlin/delivery/DeliveryApplication.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.boot.context.properties.ConfigurationPropertiesScan
import org.springframework.boot.runApplication

@ConfigurationPropertiesScan("com.kotlin.delivery.common.property")
@ConfigurationPropertiesScan("com.kotlin.delivery.common.properties")
@SpringBootApplication
class DeliveryApplication

Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,28 @@
package com.kotlin.delivery.auth.controller

import com.kotlin.delivery.auth.dto.TokenSet
import com.kotlin.delivery.auth.service.AuthService
import com.kotlin.delivery.auth.service.JwtTokenService
import org.springframework.http.HttpStatus
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.ResponseStatus
import org.springframework.web.bind.annotation.RestController

@RestController
@RequestMapping("/members/common")
class AuthController(private val authService: AuthService) {
class AuthController(private val authService: AuthService, private val tokenService: JwtTokenService) {

@ResponseStatus(HttpStatus.CREATED)
@PostMapping("/sms-auth")
fun sendSMSAuth(@RequestParam mobile: String) {
authService.sendAuth(mobile)
}
fun sendSMSAuth(@RequestParam mobile: String) = "Sent"

@PostMapping("/sms-auth/verification")
fun verifySMSAuth(@RequestParam mobile: String, @RequestParam auth: String) {
authService.verifyAuth(mobile, auth)
}
fun verifySMSAuth(@RequestParam mobile: String, @RequestParam auth: String) = "Verified"

@ResponseStatus(HttpStatus.CREATED)
@PostMapping("/token-reissue")
fun reissueToken(@RequestBody tokenSet: TokenSet) = tokenService.reissueToken(tokenSet)
}
12 changes: 8 additions & 4 deletions src/main/kotlin/com/kotlin/delivery/auth/dao/AuthRepository.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,20 @@ import org.springframework.data.redis.core.RedisTemplate
import org.springframework.stereotype.Repository
import java.time.Duration

const val VALID_AUTH_TIME = 3L

@Repository
class AuthRepository(

@Qualifier("authRedisTemplate")
private val redisTemplate: RedisTemplate<String, String>
) {
fun insertAuth(mobile: String, auth: String) =
redisTemplate.opsForValue().set(mobile, auth, Duration.ofMinutes(3L))
fun insert(key: String, value: String) = insert(key, value, VALID_AUTH_TIME)

fun insert(key: String, value: String, expiration: Long) =
redisTemplate.opsForValue().set(key, value, Duration.ofMinutes(expiration))

fun searchAuth(mobile: String) = redisTemplate.opsForValue().get(mobile)
fun select(key: String): String? = redisTemplate.opsForValue().get(key)

fun deleteAuth(mobile: String) = redisTemplate.delete(mobile)
fun delete(key: String) = redisTemplate.delete(key)
}
10 changes: 10 additions & 0 deletions src/main/kotlin/com/kotlin/delivery/auth/dto/TokenSet.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.kotlin.delivery.auth.dto

data class TokenSet(

val type: String = "Bearer",

val accessToken: String,

val refreshToken: String
)
83 changes: 67 additions & 16 deletions src/main/kotlin/com/kotlin/delivery/auth/service/JwtTokenService.kt
Original file line number Diff line number Diff line change
@@ -1,12 +1,17 @@
package com.kotlin.delivery.auth.service

import com.kotlin.delivery.common.property.JwtProperty
import com.kotlin.delivery.auth.dao.AuthRepository
import com.kotlin.delivery.auth.dto.TokenSet
import com.kotlin.delivery.common.exception.AuthNotFoundException
import com.kotlin.delivery.common.properties.JwtProperties
import io.jsonwebtoken.Claims
import io.jsonwebtoken.ExpiredJwtException
import io.jsonwebtoken.Jws
import io.jsonwebtoken.Jwts
import io.jsonwebtoken.MalformedJwtException
import io.jsonwebtoken.SignatureAlgorithm
import io.jsonwebtoken.UnsupportedJwtException
import org.springframework.security.authentication.BadCredentialsException
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.security.core.Authentication
import org.springframework.security.core.authority.SimpleGrantedAuthority
Expand All @@ -21,29 +26,61 @@ import java.util.Date
@Service
class JwtTokenService(

private val prop: JwtProperty,
private val prop: JwtProperties,

private val authRepository: AuthRepository
) {
fun createToken(authentication: Authentication): String {
fun createTokenSet(authentication: Authentication): TokenSet {
val authorities = authentication.authorities.joinToString()
val accessToken = generateAccessToken(authentication, authorities)
val refreshToken = generateRefreshToken()

return Jwts.builder()
.setSubject(authentication.name)
.claim("auth", authorities)
.setExpiration(getExpiration())
.signWith(prop.key, SignatureAlgorithm.HS512)
.compact()
return TokenSet(accessToken = accessToken, refreshToken = refreshToken)
}

fun createAuthenticationToken(parsedToken: Claims): Authentication {
val authorities = parsedToken["auth"].toString().split(",").map { SimpleGrantedAuthority(it) }
val principal = User(parsedToken.subject, "", authorities)
fun createAuthenticationToken(tokenParsed: Jws<Claims>): Authentication {
val tokenBody = tokenParsed.body

val authorities = tokenBody["auth"].toString().split(",")
.map { SimpleGrantedAuthority(it) }
val principal = User(tokenBody.subject, "", authorities)

return UsernamePasswordAuthenticationToken(principal, "", authorities)
}

fun parseToken(token: String): Claims =
fun reissueToken(tokenSet: TokenSet): TokenSet {
// TODO: 여기 에러 핸들링 필요할 듯 하다. ControllerAdvice 만들어줄 때 함께 처리하자.
parseToken(tokenSet.refreshToken)

val authentication = createAuthenticationToken(parseToken(tokenSet.accessToken))
val refreshTokenSaved = authRepository.select(authentication.name)
?: throw AuthNotFoundException("이미 로그아웃 한 사용자입니다.")

if (tokenSet.refreshToken != refreshTokenSaved) {
throw BadCredentialsException("토큰에 담긴 사용자 정보가 일치하지 않습니다.")
}

return createTokenSet(authentication).also {
authRepository.insert(authentication.name, it.refreshToken)
}
}

fun saveRefreshToken(email: String, refreshToken: String) {
authRepository.insert(email, refreshToken, prop.refreshDuration)
}

/**
* possible exception description
*
* 1. SignatureException: signature in a Jwt token not verified
* 2. MalformedJwtException: no valid composition for a given Jwt token
* 3. ExpiredJwtException: an expired Jwt token comes in
* 4. UnsupportedJwtException: a Jwt token that has unexpected formats
* 5. IllegalArgumentException: when `null` or `empty string` or `white space` has been provided
*/
fun parseToken(token: String): Jws<Claims> =
try {
Jwts.parserBuilder().setSigningKey(prop.key).build().parseClaimsJws(token).body
Jwts.parserBuilder().setSigningKey(prop.key).build().parseClaimsJws(token)
} catch (e: RuntimeException) {
when (e) {
is SignatureException -> { throw SignatureException("서명이 잘못된 토큰입니다.", e) }
Expand All @@ -54,7 +91,21 @@ class JwtTokenService(
}
}

private fun getExpiration() = Date.from(
LocalDateTime.now().plusMinutes(prop.duration).atZone(ZoneId.systemDefault()).toInstant()
private fun generateAccessToken(authentication: Authentication, authorities: String) =
Jwts.builder()
.setSubject(authentication.name)
.claim("auth", authorities)
.setExpiration(expireIn(prop.accessDuration))
.signWith(prop.key, SignatureAlgorithm.HS512)
.compact()

private fun generateRefreshToken() =
Jwts.builder()
.setExpiration(expireIn(prop.refreshDuration))
.signWith(prop.key, SignatureAlgorithm.HS256)
.compact()

private fun expireIn(duration: Long) = Date.from(
LocalDateTime.now().plusMinutes(duration).atZone(ZoneId.systemDefault()).toInstant()
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.kotlin.delivery.auth.service

import com.kotlin.delivery.common.exception.AuthNotFoundException
import org.springframework.stereotype.Service

@Service
class MockSmsAuthService : AuthService {

val mockAuth = "123456"

override fun sendAuth(to: String) {
return
}

override fun verifyAuth(to: String, inputAuth: String) {
if (inputAuth != mockAuth) {
throw AuthNotFoundException("인증정보가 일치하지 않습니다. 인증번호를 다시 확인해주세요.")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,42 @@ package com.kotlin.delivery.auth.service

import com.kotlin.delivery.auth.dao.AuthRepository
import com.kotlin.delivery.common.exception.AuthNotFoundException
import com.kotlin.delivery.common.property.SMSAuthProperty
import com.kotlin.delivery.common.properties.SMSAuthProperties
import net.nurigo.java_sdk.api.Message
import org.apache.commons.lang.RandomStringUtils
import org.springframework.context.annotation.Profile
import org.springframework.stereotype.Service

@Profile("prod")
@Service
class SMSAuthService(
class SmsAuthService(

private val authRepository: AuthRepository,

private val prop: SMSAuthProperty
private val prop: SMSAuthProperties
) : AuthService {

override fun sendAuth(to: String) {
val auth = RandomStringUtils.randomNumeric(6)
val sms = Message(prop.apiKey, prop.secret)
sms.send(createSMS(to, auth))

authRepository.insertAuth(to, auth)
authRepository.insert(to, auth)
}

override fun verifyAuth(to: String, inputAuth: String) {
val savedAuth = authRepository.searchAuth(to)
if (savedAuth != inputAuth) throw AuthNotFoundException("인증정보가 일치하지 않습니다. 인증번호를 다시 확인해주세요.")
authRepository.deleteAuth(to)
}
override fun verifyAuth(to: String, inputAuth: String) =
authRepository.select(to).run {
val message = "인증정보가 일치하지 않습니다. 인증번호를 다시 확인해주세요."
requireNotNull(this) {
message
}

if (this != inputAuth) {
throw AuthNotFoundException(message)
}
}.also {
authRepository.delete(to)
}

private fun createSMS(mobile: String, auth: String) = hashMapOf(
"from" to prop.sender,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
package com.kotlin.delivery.common.config

import com.kotlin.delivery.common.property.RedisProperty
import com.kotlin.delivery.common.properties.RedisProperties
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory
import org.springframework.data.redis.core.StringRedisTemplate

@Configuration
class RedisConfig(private val prop: RedisProperty) {
class RedisConfig(private val prop: RedisProperties) {

@Bean
fun redisConnectionFactory() = LettuceConnectionFactory(prop.host, prop.port)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.kotlin.delivery.common.property
package com.kotlin.delivery.common.properties

import io.jsonwebtoken.io.Decoders
import io.jsonwebtoken.security.Keys
Expand All @@ -8,7 +8,14 @@ import java.security.Key

@ConstructorBinding
@ConfigurationProperties(prefix = "auth.jwt")
class JwtProperty(secret: String, val duration: Long) {
class JwtProperties(

secret: String,

val accessDuration: Long,

val refreshDuration: Long
) {

val key: Key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secret))
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package com.kotlin.delivery.common.property
package com.kotlin.delivery.common.properties

import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.boot.context.properties.ConstructorBinding

@ConstructorBinding
@ConfigurationProperties(prefix = "spring.redis")
class RedisProperty(val host: String, val port: Int)
class RedisProperties(val host: String, val port: Int)
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
package com.kotlin.delivery.common.property
package com.kotlin.delivery.common.properties

import org.springframework.boot.context.properties.ConfigurationProperties
import org.springframework.boot.context.properties.ConstructorBinding

@ConstructorBinding
@ConfigurationProperties(prefix = "sms.auth-api")
open class SMSAuthProperty(
open class SMSAuthProperties(

val apiKey: String,

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.kotlin.delivery.member.service

import com.kotlin.delivery.auth.dto.TokenSet
import com.kotlin.delivery.auth.service.JwtTokenService
import com.kotlin.delivery.member.dto.LoginRequest
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder
Expand All @@ -13,8 +14,11 @@ class JwtTokenLoginService(
private val tokenService: JwtTokenService
) : LoginService {

override fun login(req: LoginRequest): String {
override fun login(req: LoginRequest): TokenSet {
val authentication = authManagerBuilder.`object`.authenticate(req.toAuthToken())
return tokenService.createToken(authentication)

return tokenService.createTokenSet(authentication).also {
tokenService.saveRefreshToken(authentication.name, it.refreshToken)
}
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
package com.kotlin.delivery.member.service

import com.kotlin.delivery.auth.dto.TokenSet
import com.kotlin.delivery.member.dto.LoginRequest

interface LoginService {

fun login(req: LoginRequest): String
fun login(req: LoginRequest): TokenSet
}
12 changes: 3 additions & 9 deletions src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,8 @@ spring:
host: localhost
port: 6379

sms:
auth-api:
apikey: (enter your api key)
secret: (enter your secret key)
sender: (enter a representative sender number)
type: (enter sms type to send)

auth:
jwt:
secret: (enter your secret key)
duration: 86400
secret: c3ByaW5nLWJvb3Qtc2VjdXJpdHktand0LXR1dG9yaWFsLWppd29vbi1zcHJpbmctYm9vdC1zZWN1cml0eS1qd3QtdHV0b3JpYWwK
accessduration: 30
refreshduration: 10080
Loading