Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FEAT] JWT 기반 소셜 로그인 기능 구현 #15

Merged
merged 20 commits into from
Mar 1, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
c70064e
⚡️: fix Swagger, ApiResponseDto 패키지 수정 #14
orijoon98 Feb 16, 2024
5a71449
⚡️: fix PluApiApplication 패키지 수정 #14
orijoon98 Feb 16, 2024
ac90cd2
⚡️: fix Jpa, Querydsl 설정 수정 #14
orijoon98 Feb 16, 2024
0e9002a
🔥: remove 중복 ControllerAdvice 제거 #14
orijoon98 Feb 16, 2024
6c5f558
✨: feat JWT 관련 기능 추가 #14
orijoon98 Feb 16, 2024
ec7e202
✨: feat JWT 토큰 생성 서비스 추가 #14
orijoon98 Feb 16, 2024
342d8dd
✨: feat 카카오 API 연동 #14
orijoon98 Feb 16, 2024
aba0d2f
✨: feat 카카오 회원가입 API 추가 #14
orijoon98 Feb 16, 2024
65e6286
✨: feat 카카오 로그인 API 추가 #14
orijoon98 Feb 16, 2024
945b92f
✨: feat 토큰 갱신 API 추가 #14
orijoon98 Feb 16, 2024
1128c06
✨: feat 회원 인증용 interceptor, resolver 추가 #14
orijoon98 Feb 16, 2024
3d96f66
✨: feat 로그아웃 API 추가 #14
orijoon98 Feb 16, 2024
1fa63ad
✨: feat 애플 로그인 기능 추가 #14
orijoon98 Feb 16, 2024
64e3e8c
⚡️: fix Redis Template 의존성 문제 해결 #14
orijoon98 Feb 17, 2024
e7176bf
⚡️: fix QuerydslConfig 의존성 주입 방식 수정 #14
orijoon98 Mar 1, 2024
8663d1c
♻️: refactor RedisTemplate wrapping 적용, class 명 수정 #14
orijoon98 Mar 1, 2024
59832a3
♻️: refactor TokenService 패키지 수정 및 RedisHandler 사용하도록 수정 #14
orijoon98 Mar 1, 2024
1751374
♻️: refactor 토큰 생성 후 전달 방식 개선 #14
orijoon98 Mar 1, 2024
4b5fe80
♻️: refactor 예외처리 방식 수정 #14
orijoon98 Mar 1, 2024
28c6c84
⚡️: fix 불필요한 어노테이션 제거 #14
orijoon98 Mar 1, 2024
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
12 changes: 10 additions & 2 deletions plu-common/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ plugins {
kotlin("jvm")
}
tasks.jar {
enabled = true
enabled = true
}

tasks.bootJar {
enabled = false
enabled = false
}


Expand All @@ -15,6 +15,14 @@ dependencies {

// web
implementation("org.springframework.boot:spring-boot-starter-web")

// Redis
implementation("org.springframework.boot:spring-boot-starter-data-redis")

//jwt
implementation("io.jsonwebtoken:jjwt-api:0.11.2")
runtimeOnly("io.jsonwebtoken:jjwt-impl:0.11.2")
runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.11.2")
}
repositories {
mavenCentral()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.th.plu.common.constant

object JwtKey {
const val MEMBER_ID = "MEMBER_ID"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.th.plu.common.constant

object RedisKey {
const val REFRESH_TOKEN = "RT:"
}
105 changes: 105 additions & 0 deletions plu-common/src/main/kotlin/com/th/plu/common/util/JwtUtils.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package com.th.plu.common.util

import com.th.plu.common.constant.JwtKey
import com.th.plu.common.constant.RedisKey
import io.jsonwebtoken.*
import io.jsonwebtoken.io.Decoders
import io.jsonwebtoken.io.DecodingException
import io.jsonwebtoken.security.Keys
import jakarta.annotation.PostConstruct
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Value
import org.springframework.data.redis.core.RedisTemplate
import org.springframework.stereotype.Component
import java.security.Key
import java.util.*
import java.util.concurrent.TimeUnit

@Component
class JwtUtils(
private val redisTemplate: RedisTemplate<String, Any>
) {

@Value("\${jwt.secret}")
private var jwtSecret: String? = null

private var secretKey: Key? = null
private val log = LoggerFactory.getLogger(this.javaClass)

companion object {
// private const val ACCESS_TOKEN_EXPIRE_TIME = 10 * 60 * 1000L // 10분
// private const val REFRESH_TOKEN_EXPIRE_TIME = 6 * 30 * 24 * 60 * 60 * 1000L // 180일

private const val ACCESS_TOKEN_EXPIRE_TIME = 365 * 24 * 60 * 60 * 1000L; // 1년
private const val REFRESH_TOKEN_EXPIRE_TIME = 365 * 24 * 60 * 60 * 1000L; // 1년
private const val EXPIRED_TIME = 1L
}

@PostConstruct
fun init() {
val keyBytes: ByteArray = Decoders.BASE64.decode(jwtSecret)
this.secretKey = Keys.hmacShaKeyFor(keyBytes)
}

fun createTokenInfo(memberId: Long): List<String> {
val now = Date().time
val accessTokenExpiresIn = Date(now + ACCESS_TOKEN_EXPIRE_TIME)
val refreshTokenExpiresIn = Date(now + REFRESH_TOKEN_EXPIRE_TIME)

// Access Token 생성
val accessToken: String = Jwts.builder()
.claim(JwtKey.MEMBER_ID, memberId)
.setExpiration(accessTokenExpiresIn)
.signWith(secretKey, SignatureAlgorithm.HS512)
.compact()

// Refresh Token 생성
val refreshToken: String = Jwts.builder()
.setExpiration(refreshTokenExpiresIn)
.signWith(secretKey, SignatureAlgorithm.HS512)
.compact()

redisTemplate.opsForValue()
.set(RedisKey.REFRESH_TOKEN + memberId, refreshToken, REFRESH_TOKEN_EXPIRE_TIME, TimeUnit.MILLISECONDS)

return listOf(accessToken, refreshToken)
}

fun expireRefreshToken(memberId: Long) {
redisTemplate.opsForValue().set(RedisKey.REFRESH_TOKEN + memberId, "", EXPIRED_TIME, TimeUnit.MILLISECONDS)
}
orijoon98 marked this conversation as resolved.
Show resolved Hide resolved

fun validateToken(token: String?): Boolean {
try {
Jwts.parserBuilder().setSigningKey(secretKey).build().parseClaimsJws(token)
return true
} catch (e: SecurityException) {
log.warn("Invalid JWT Token", e)
} catch (e: MalformedJwtException) {
log.warn("Invalid JWT Token", e)
} catch (e: DecodingException) {
log.warn("Invalid JWT Token", e)
} catch (e: ExpiredJwtException) {
log.warn("Expired JWT Token", e)
} catch (e: UnsupportedJwtException) {
log.warn("Unsupported JWT Token", e)
} catch (e: IllegalArgumentException) {
log.warn("JWT claims string is empty.", e)
} catch (e: Exception) {
log.error("Unhandled JWT exception", e)
}
return false
}

fun getMemberIdFromJwt(accessToken: String): Long {
return parseClaims(accessToken).get(JwtKey.MEMBER_ID, Long::class.java)
}

private fun parseClaims(accessToken: String): Claims {
return try {
Jwts.parserBuilder().setSigningKey(secretKey).build().parseClaimsJws(accessToken).body
} catch (e: ExpiredJwtException) {
e.claims
}
}
}