Skip to content

Commit

Permalink
Merge pull request #78 from TinU-Official/TINU-83/social-login-feat
Browse files Browse the repository at this point in the history
Tinu 83/social login feat
  • Loading branch information
honey720 authored Feb 1, 2025
2 parents 58b391e + 83152b0 commit 683a804
Show file tree
Hide file tree
Showing 36 changed files with 1,062 additions and 2 deletions.
30 changes: 30 additions & 0 deletions .github/workflows/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,36 @@ jobs:
aws.s3.bucket=${{ secrets.S3_BUCKET }}
aws.s3.file.max-size=10485760
cloudfront.domain=${{ secrets.CDN_DOMAIN }}
spring.security.oauth2.client.registration.kakao.client-id=${{ secrets.KAKAO_CLIENT_ID}}
spring.security.oauth2.client.registration.kakao.client-secret=${{ secrets.KAKAO_CLIENT_SECRET }}
spring.security.oauth2.client.registration.kakao.scope=${{ secrets.KAKAO_SCOPE }}
spring.security.oauth2.client.registration.kakao.authorization-grant-type=${{ secrets.KAKAO_AUTHORIZATION_GRANT_TYPE }}
spring.security.oauth2.client.registration.kakao.redirect-uri=${{ secrets.KAKAO_REDIRECT_URI }}
spring.security.oauth2.client.registration.kakao.client-name=${{ secrets.KAKAO_CLIENT_NAME }}
spring.security.oauth2.client.registration.kakao.client-authentication-method=${{ secrets.KAKAO_AUTHENTICATION_METHOD }}
spring.security.oauth2.client.registration.naver.client-id=${{ secrets.NAVER_CLIENT_ID }}
spring.security.oauth2.client.registration.naver.client-secret=${{ secrets.NAVER_CLIENT_SECRET }}
spring.security.oauth2.client.registration.naver.scope=${{ secrets.NAVER_SCOPE }}
spring.security.oauth2.client.registration.naver.client-name=${{ secrets.NAVER_CLIENT_NAME }}
spring.security.oauth2.client.registration.naver.authorization-grant-type=${{ secrets.NAVER_AUTHORIZATION_GRANT_TYPE }}
spring.security.oauth2.client.registration.naver.redirect-uri=${{ secrets.NAVER_REDIRECT_URI }}
spring.security.oauth2.client.provider.kakao.authorization-uri=${{ secrets.KAKAO_AUTHORIZATION_URI }}
spring.security.oauth2.client.provider.kakao.token-uri=${{ secrets.KAKAO_TOKEN_URI }}
spring.security.oauth2.client.provider.kakao.user-info-uri=${{ secrets.KAKAO_USER_INFO_URI }}
spring.security.oauth2.client.provider.kakao.user-name-attribute=${{ secrets.KAKAO_USER_NAME_ATTRIBUTE }}
spring.security.oauth2.client.provider.naver.authorization-uri=${{ secrets.NAVER_AUTHORIZATION_URI }}
spring.security.oauth2.client.provider.naver.token-uri=${{ secrets.NAVER_TOKEN_URI }}
spring.security.oauth2.client.provider.naver.user-info-uri=${{ secrets.NAVER_USER_INFO_URI }}
spring.security.oauth2.client.provider.naver.user-name-attribute=${{ secrets.NAVER_USER_NAME_ATTRIBUTE }}
jwt.secret=${{ secrets.JWT_SECRET }}
jwt.redirect=${{ secrets.JWT_REDIRECT_URI }}
jwt.access-token.expiration-time=${{ secrets.JWT_ACCESS_TOKEN_EXPIRATION_TIME }}
jwt.refresh-token.expiration-time=${{ secrets.JWT_REFRESH_TOKEN_EXPIRATION_TIME }}
cookie.max-age=${{ secrets.COOKIE_MAX_AGE }}
cookie.token.access-token=${{ secrets.COOKIE_ACCESS_TOKEN }}
cookie.token.refresh-token=${{ secrets.COOKIE_REFRESH_TOKEN }}
web.allowed-path=${{ secrets.ALLOWED_PATH }}
spring.web.resources.add-mappings=false
EOF
# docker image 빌드
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,6 @@ out/

### personal ###
**/src/main/resources/application.properties
**/src/main/resources/application.yml
**/src/main/resources/secret.yml

13 changes: 13 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,19 @@ dependencies {
testImplementation 'org.jetbrains.kotlin:kotlin-test-junit5'
testImplementation 'org.springframework.kafka:spring-kafka-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'

//security 관련 의존성 추가
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'
testImplementation 'org.springframework.security:spring-security-test'

//jwt 관련 의존성 추가
implementation "io.jsonwebtoken:jjwt-api:0.11.5"
implementation "io.jsonwebtoken:jjwt-impl:0.11.5"
implementation("io.jsonwebtoken:jjwt-jackson:0.11.5")

//OAuth2 관련 의존성 추가
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
}

kotlin {
Expand Down
2 changes: 1 addition & 1 deletion src/main/kotlin/com/tinuproject/tinu/DTO/ResponseDTO.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ import com.fasterxml.jackson.annotation.JsonProperty
data class ResponseDTO(
@JsonProperty("success") val isSuccess: Boolean,
@JsonProperty("state") val stateCode: Int,
@JsonProperty("result") val result: Any
@JsonProperty("result") val result: Any?
)
18 changes: 18 additions & 0 deletions src/main/kotlin/com/tinuproject/tinu/GlobalExceptionHandler.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.tinuproject.tinu

import com.tinuproject.tinu.DTO.ResponseDTO
import com.tinuproject.tinu.domain.exception.base.BaseException
import com.tinuproject.tinu.web.ResponseEntityGenerator
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.ExceptionHandler
import org.springframework.web.bind.annotation.RestControllerAdvice


@RestControllerAdvice
class GlobalExceptionHandler {

@ExceptionHandler(BaseException::class)
fun baseException(e : BaseException) : ResponseEntity<ResponseDTO>{
return ResponseEntityGenerator.onFailure(e)
}
}
19 changes: 19 additions & 0 deletions src/main/kotlin/com/tinuproject/tinu/domain/entity/RefreshToken.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.tinuproject.tinu.domain.entity

import com.tinuproject.tinu.domain.entity.base.BaseEntity
import jakarta.persistence.*
import java.util.*

@Entity
class RefreshToken(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
var id : Long?= null,

@Column(unique = true)
var userId : UUID,

@Column(unique = true)
var token : String
){
}
11 changes: 10 additions & 1 deletion src/main/kotlin/com/tinuproject/tinu/domain/enum/Social.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,14 @@ package com.tinuproject.tinu.domain.enum
enum class Social(var code:Int, var company : String) {
KAKAO(0, "카카오"),
GOOGLE(1, "구글"),
NAVER(2, "네이버")
NAVER(2, "네이버");


companion object{
fun getSocial(provider : String) : Social{
if(provider == "kakao") return KAKAO
else return NAVER
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.tinuproject.tinu.domain.exception.base

import com.tinuproject.tinu.DTO.ResponseDTO

interface BaseCode {
fun getResponse(): ResponseDTO?
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.tinuproject.tinu.domain.exception.base

import com.tinuproject.tinu.DTO.ResponseDTO

interface BaseErrorCode {
fun getResponse(): ResponseDTO?
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.tinuproject.tinu.domain.exception.base

import com.tinuproject.tinu.DTO.ResponseDTO
import java.lang.RuntimeException

open class BaseException(
protected val errorCode : ErrorCode
): RuntimeException(), BaseErrorCode {

override fun getResponse(): ResponseDTO {
var map : MutableMap<String, Any> = mutableMapOf()
map["error-message"] = errorCode.message
if(errorCode.stateCode!=null) map["stateCode"] = errorCode.stateCode
return ResponseDTO(isSuccess = false, stateCode = errorCode.httpStatusCode, result = map)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.tinuproject.tinu.domain.exception.base

enum class ErrorCode(
val httpStatusCode : Int,
val stateCode : String?,
val message :String
) {
TOKEN_MISSING(httpStatusCode = 401, stateCode = "TOKEN_MISSING", message = "토큰이 존재하지 않습니다."),
TOKEN_INVALIDED(httpStatusCode = 401, stateCode = "TOKEN_INVALIDED", message = "토큰이 유효하지 않습니다."),
TOKEN_EXPIRED(httpStatusCode = 401, stateCode = "TOKEN_EXPIRED", message = "토큰이 만료되었습니다.");


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.tinuproject.tinu.domain.exception.token

import com.tinuproject.tinu.domain.exception.base.BaseException
import com.tinuproject.tinu.domain.exception.base.ErrorCode

class ExpiredTokenException(): BaseException(errorCode = ErrorCode.TOKEN_EXPIRED) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.tinuproject.tinu.domain.exception.token

import com.tinuproject.tinu.domain.exception.base.BaseException
import com.tinuproject.tinu.domain.exception.base.ErrorCode

class InvalidedTokenException(

):BaseException(errorCode= ErrorCode.TOKEN_INVALIDED) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.tinuproject.tinu.domain.exception.token

import com.tinuproject.tinu.domain.exception.base.BaseException
import com.tinuproject.tinu.domain.exception.base.ErrorCode

class NotFoundTokenException(
) : BaseException(errorCode = ErrorCode.TOKEN_MISSING) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.tinuproject.tinu.domain.socialmember.repository

import com.tinuproject.tinu.domain.entity.SocialMember
import org.springframework.data.repository.CrudRepository
import org.springframework.stereotype.Repository
import java.util.*

@Repository
interface SocialMemberRepository:CrudRepository<SocialMember, Long> {
fun findByUserId(userId : UUID) : SocialMember?

fun findByProviderId(providerId : String) : SocialMember?
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com.tinuproject.tinu.domain.socialmember.service

interface SocialMemberService {
}
8 changes: 8 additions & 0 deletions src/main/kotlin/com/tinuproject/tinu/domain/token/Tokens.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.tinuproject.tinu.domain.token

class Tokens(
val accessToken : String,
val refreshToken : String
) {

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.tinuproject.tinu.domain.token.refreshtoken.controller

import com.tinuproject.tinu.DTO.ResponseDTO
import com.tinuproject.tinu.domain.exception.token.NotFoundTokenException
import com.tinuproject.tinu.domain.token.Tokens
import com.tinuproject.tinu.domain.token.refreshtoken.service.RefreshTokenService
import com.tinuproject.tinu.web.CookieGenerator
import com.tinuproject.tinu.web.ResponseEntityGenerator
import jakarta.servlet.http.HttpServletResponse
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Value
import org.springframework.http.HttpHeaders
import org.springframework.http.ResponseEntity
import org.springframework.stereotype.Controller
import org.springframework.web.bind.annotation.CookieValue
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping

@Controller
@RequestMapping("api/token")
class RefreshTokenController(
private val refreshTokenService : RefreshTokenService,

@Value("\${cookie.token.access-token}")
private val accessTokenKey : String,

@Value("\${cookie.token.refresh-token}")
private val refreshTokenkey : String
) {
var log : Logger = LoggerFactory.getLogger(this::class.java)


@GetMapping("/generate")
fun generateAccessToken(httpServletResponse: HttpServletResponse, @CookieValue(name = "RefreshToken") refreshToken : String?): ResponseEntity<ResponseDTO> {
log.info("AccessToken 갱신 시도")
if(refreshToken==null){
throw NotFoundTokenException()
}

val tokens : Tokens = refreshTokenService.reissueAccessTokenByRefreshToken(refreshToken)


httpServletResponse.addHeader(HttpHeaders.SET_COOKIE,CookieGenerator.createCookies(accessTokenKey,tokens.accessToken))
httpServletResponse.addHeader(HttpHeaders.SET_COOKIE,CookieGenerator.createCookies(refreshTokenkey, tokens.refreshToken))
var body : MutableMap<String, Any> = mutableMapOf()


val responseEntity = ResponseEntityGenerator.onSuccess(body)
return responseEntity
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.tinuproject.tinu.domain.token.refreshtoken.repository

import com.tinuproject.tinu.domain.entity.RefreshToken
import jakarta.persistence.ManyToOne
import jakarta.transaction.Transactional
import org.springframework.data.jpa.repository.Modifying
import org.springframework.data.repository.CrudRepository
import org.springframework.stereotype.Repository
import java.util.*

interface RefreshTokenRepository:CrudRepository<RefreshToken, Long> {

fun findByToken(token : String) : RefreshToken?

@Transactional
@Modifying
fun deleteByUserId(userId : UUID)


@Transactional
@Modifying
fun deleteByToken(token : String)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.tinuproject.tinu.domain.token.refreshtoken.service

import com.tinuproject.tinu.domain.token.Tokens
import org.springframework.stereotype.Service


interface RefreshTokenService {
fun reissueAccessTokenByRefreshToken(refreshToken : String) : Tokens

fun deleteRefreshToken(refreshToken: String)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package com.tinuproject.tinu.domain.token.refreshtoken.service

import com.tinuproject.tinu.domain.entity.RefreshToken
import com.tinuproject.tinu.domain.exception.token.ExpiredTokenException
import com.tinuproject.tinu.domain.exception.token.InvalidedTokenException
import com.tinuproject.tinu.domain.exception.token.NotFoundTokenException
import com.tinuproject.tinu.domain.token.Tokens
import com.tinuproject.tinu.domain.token.refreshtoken.repository.RefreshTokenRepository
import com.tinuproject.tinu.security.jwt.JwtUtil
import io.jsonwebtoken.ExpiredJwtException
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.beans.factory.annotation.Value
import org.springframework.stereotype.Service
import java.security.SignatureException
import java.util.*

@Service
class RefreshTokenServiceImpl(
@Value("\${jwt.refresh-token.expiration-time}")
private val REFRESH_TOKEN_EXPIRATION_TIME: Long, // 리프레쉬 토큰 유효기간

@Value("\${jwt.access-token.expiration-time}")
private val ACCESS_TOKEN_EXPIRATION_TIME: Long, // 액세스 토큰 유효기간

private val refreshTokenRepository: RefreshTokenRepository,
private val jwtUtil : JwtUtil,
):RefreshTokenService {
var log : Logger = LoggerFactory.getLogger(this::class.java)

override fun reissueAccessTokenByRefreshToken(refreshToken: String): Tokens {
jwtUtil.validateToken(refreshToken)

if(refreshTokenRepository.findByToken(refreshToken)==null){
throw NotFoundTokenException()
}

val userId :UUID = UUID.fromString(jwtUtil.getUserIdFromToken(refreshToken))

//리프레쉬 토큰 삭제
refreshTokenRepository.deleteByUserId(userId)

//리프레쉬 토큰 재발행.
val token = jwtUtil.generateRefreshToken(userId,REFRESH_TOKEN_EXPIRATION_TIME)

val newRefreshToken = RefreshToken(userId = userId, token = token)

//리프레쉬 토큰 저장
refreshTokenRepository.save(newRefreshToken)

//AccesToken 재발행.
val accessToken : String = jwtUtil.generateAccessToken(userId, ACCESS_TOKEN_EXPIRATION_TIME)

return Tokens(accessToken=accessToken, refreshToken = refreshToken)
}

override fun deleteRefreshToken(refreshToken: String) {
jwtUtil.validateToken(refreshToken)

val userId : UUID = UUID.fromString(jwtUtil.getUserIdFromToken(refreshToken))

refreshTokenRepository.deleteByUserId(userId)
}
}
Loading

0 comments on commit 683a804

Please sign in to comment.