Skip to content

Commit

Permalink
refactor: auth 수정 (#33)
Browse files Browse the repository at this point in the history
* refactor: 클래스명 변경

* refactor: DTO 분리

* refactor: 쿠키 설정 추가

* refactor: 메서드명 변경

* refactor: 인증 플로우 변경

* refactor: DTO 타입 변경

* refactor: 변수명 변경

* feature: 봉달목록에 상품 추가 api (#31)

* feat: CartProduct 도메인 추가

* feat: Entity 존재 여부 확인 확장 함수 추가

* feat: CartProduct 저장 로직 추가

* refactor: ProductController 패키지 이동

* feat: CartProduct 저장 API 추가

* test: CartProduct 저장시 예외 상황 테스트 추가

* refactor: 중복된 dependency 제거

* chore: mockk 의존성 추가

* test: 로그인 셋업 메서드 추가

* test: 구현된 Auth 헤더 사용 하도록 CartProduct API 테스트 수정

* refactor: 중복된 dependency 제거

* refactor: Accessor -> LoginMember 네이밍 수정

* test: 검증부 내용 이름 수정

* feat: 회원 존재 유무 검증 로직 추가

* test: application 테스트내 빈 등록 범위 수정

* test: 회원 검증 추가에 따른 테스트 로직 수정

* feat: 상품 수량 값객체 추가

* feat: 봉달 상품 수량 값객체 적용

* test: 잘못된 봉달 상품 수량에 대한 테스트 추가

* refactor: 이미 봉달 목록에 존재하는 상품에 대한 처리 추가

* fix: value 필드 예약어 사용에 의한 오류 해결

* refactor: 클래스명 변경

* refactor: url 변경

* refactor: AccessToken으로만 인증을 수행하게 변경

* feat: 로그인 연장 API 추가

* test: 로그인 연장 테스트 추가

* refactor: GET 메서드로 변경

* test: 로그인 연장 테스트 추가

* test: �shouldNotBeNull() 사용

Co-authored-by: Taeyeon <90550065+TaeyeonRoyce@users.noreply.github.com>

* chore: import 정리

* refactor: 사용하지않는 ExceptionType 제거

* refactor: AuthException 패키지 이동

* chore: 서브모듈 업데이트

* chore: shouldNotBeNull 메서드 사용

---------

Co-authored-by: Taeyeon <90550065+TaeyeonRoyce@users.noreply.github.com>
  • Loading branch information
hgo641 and TaeyeonRoyce authored Jan 31, 2024
1 parent 2617f49 commit 086cc29
Show file tree
Hide file tree
Showing 24 changed files with 403 additions and 180 deletions.
2 changes: 1 addition & 1 deletion backend-submodule
4 changes: 2 additions & 2 deletions src/main/kotlin/com/petqua/application/auth/AuthDtos.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.petqua.application.auth

data class AuthResponse(
data class AuthTokenInfo(
val accessToken: String,
val refreshToken: String,
)
)
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
package com.petqua.application.auth

import com.petqua.common.domain.findByIdOrThrow
import com.petqua.exception.auth.AuthException
import com.petqua.exception.auth.AuthExceptionType.EXPIRED_REFRESH_TOKEN
import com.petqua.exception.auth.AuthExceptionType.INVALID_REFRESH_TOKEN
import com.petqua.exception.auth.AuthExceptionType.NOT_RENEWABLE_ACCESS_TOKEN
import com.petqua.domain.auth.Authority.MEMBER
import com.petqua.domain.auth.oauth.OauthClientProvider
import com.petqua.domain.auth.oauth.OauthServerType
Expand All @@ -17,7 +22,7 @@ import java.util.Date

@Transactional
@Service
class OauthService(
class AuthService(
private val oauthClientProvider: OauthClientProvider,
private val memberRepository: MemberRepository,
private val authTokenProvider: AuthTokenProvider,
Expand All @@ -29,18 +34,42 @@ class OauthService(
return oauthClient.getAuthCodeRequestUrl()
}

fun login(oauthServerType: OauthServerType, code: String): AuthResponse {
fun login(oauthServerType: OauthServerType, code: String): AuthTokenInfo {
val oauthClient = oauthClientProvider.getOauthClient(oauthServerType)
val oauthUserInfo = oauthClient.requestOauthUserInfo(oauthClient.requestToken(code))
val member = getMemberByOauthInfo(oauthUserInfo.oauthId, oauthServerType)
?: createMember(oauthUserInfo, oauthServerType)
val authToken = createAuthToken(member)
return AuthResponse(
return AuthTokenInfo(
accessToken = authToken.accessToken,
refreshToken = authToken.refreshToken,
)
}

fun extendLogin(accessToken: String, refreshToken: String): AuthTokenInfo {
validateTokenExpiredStatusForExtendLogin(accessToken, refreshToken)
val savedRefreshToken = refreshTokenRepository.findByToken(refreshToken)
?: throw AuthException(INVALID_REFRESH_TOKEN)
if (savedRefreshToken.token != refreshToken) {
throw AuthException(INVALID_REFRESH_TOKEN)
}
val member = memberRepository.findByIdOrThrow(savedRefreshToken.memberId)
val authToken = createAuthToken(member)
return AuthTokenInfo(
accessToken = authToken.accessToken,
refreshToken = authToken.refreshToken,
)
}

private fun validateTokenExpiredStatusForExtendLogin(accessToken: String, refreshToken: String) {
if (!authTokenProvider.isExpiredAccessToken(accessToken)) {
throw AuthException(NOT_RENEWABLE_ACCESS_TOKEN)
}
if (authTokenProvider.isExpiredRefreshToken(refreshToken)) {
throw AuthException(EXPIRED_REFRESH_TOKEN)
}
}

private fun getMemberByOauthInfo(oauthId: String, oauthServerType: OauthServerType): Member? {
return memberRepository.findByOauthIdAndOauthServerNumber(oauthId, oauthServerType.number)
}
Expand Down
4 changes: 2 additions & 2 deletions src/main/kotlin/com/petqua/domain/auth/Authority.kt
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.petqua.domain.auth

import com.petqua.common.exception.auth.AuthException
import com.petqua.common.exception.auth.AuthExceptionType
import com.petqua.exception.auth.AuthException
import com.petqua.exception.auth.AuthExceptionType
import java.util.Locale

enum class Authority {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.petqua.domain.auth.oauth

import com.petqua.common.exception.auth.OauthClientException
import com.petqua.common.exception.auth.OauthClientExceptionType.UNSUPPORTED_OAUTH_SERVER_TYPE
import com.petqua.exception.auth.OauthClientException
import com.petqua.exception.auth.OauthClientExceptionType.UNSUPPORTED_OAUTH_SERVER_TYPE
import org.springframework.stereotype.Component

@Component
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.petqua.domain.auth.oauth

import com.petqua.common.exception.auth.OauthClientException
import com.petqua.common.exception.auth.OauthClientExceptionType.UNSUPPORTED_OAUTH_SERVER_TYPE
import com.petqua.exception.auth.OauthClientException
import com.petqua.exception.auth.OauthClientExceptionType.UNSUPPORTED_OAUTH_SERVER_TYPE
import java.util.Locale.ENGLISH

enum class OauthServerType(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package com.petqua.domain.auth.token

import com.petqua.common.exception.auth.AuthException
import com.petqua.common.exception.auth.AuthExceptionType.INVALID_ACCESS_TOKEN
import com.petqua.exception.auth.AuthException
import com.petqua.exception.auth.AuthExceptionType.INVALID_ACCESS_TOKEN
import com.petqua.domain.auth.Authority

private const val MEMBER_ID = "memberId"
Expand Down
30 changes: 23 additions & 7 deletions src/main/kotlin/com/petqua/domain/auth/token/AuthTokenProvider.kt
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
package com.petqua.domain.auth.token

import com.petqua.common.exception.auth.AuthException
import com.petqua.common.exception.auth.AuthExceptionType.EXPIRED_TOKEN
import com.petqua.common.exception.auth.AuthExceptionType.INVALID_ACCESS_TOKEN
import com.petqua.common.exception.auth.AuthExceptionType.INVALID_TOKEN
import com.petqua.exception.auth.AuthException
import com.petqua.exception.auth.AuthExceptionType.EXPIRED_ACCESS_TOKEN
import com.petqua.exception.auth.AuthExceptionType.INVALID_ACCESS_TOKEN
import com.petqua.exception.auth.AuthExceptionType.INVALID_REFRESH_TOKEN
import com.petqua.domain.member.Member
import io.jsonwebtoken.ExpiredJwtException
import io.jsonwebtoken.JwtException
Expand Down Expand Up @@ -39,15 +39,31 @@ class AuthTokenProvider(
return jwtProvider.isValidToken(token)
}

fun getAccessTokenClaims(token: String): AccessTokenClaims {
fun getAccessTokenClaimsOrThrow(token: String): AccessTokenClaims {
try {
return AccessTokenClaims.from(jwtProvider.getPayload(token))
} catch (e: ExpiredJwtException) {
throw AuthException(EXPIRED_TOKEN)
throw AuthException(EXPIRED_ACCESS_TOKEN)
} catch (e: JwtException) {
throw AuthException(INVALID_TOKEN)
throw AuthException(INVALID_ACCESS_TOKEN)
} catch (e: NullPointerException) {
throw AuthException(INVALID_ACCESS_TOKEN)
}
}

fun isExpiredAccessToken(token: String): Boolean {
try {
return jwtProvider.isExpiredToken(token)
} catch (e: JwtException) {
throw AuthException(INVALID_ACCESS_TOKEN)
}
}

fun isExpiredRefreshToken(token: String): Boolean {
try {
return jwtProvider.isExpiredToken(token)
} catch (e: JwtException) {
throw AuthException(INVALID_REFRESH_TOKEN)
}
}
}
10 changes: 10 additions & 0 deletions src/main/kotlin/com/petqua/domain/auth/token/JwtProvider.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.petqua.domain.auth.token

import io.jsonwebtoken.Claims
import io.jsonwebtoken.ExpiredJwtException
import io.jsonwebtoken.Header.JWT_TYPE
import io.jsonwebtoken.Header.TYPE
import io.jsonwebtoken.Jws
Expand Down Expand Up @@ -55,6 +56,15 @@ class JwtProvider(
return true
}

fun isExpiredToken(token: String): Boolean {
try {
parseToken(token)
} catch (e: ExpiredJwtException) {
return true
}
return false
}

fun getPayload(token: String): Map<String, String> {
val tokenClaims = parseToken(token)
return tokenClaims.body.entries.associate {(key, value) ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ interface RefreshTokenRepository : CrudRepository<RefreshToken, Long> {

fun existsByToken(token: String): Boolean

fun findByMemberId(memberId: Long): RefreshToken?
fun findByToken(token: String): RefreshToken?

fun deleteByMemberId(memberId: Long)
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.petqua.common.exception.auth
package com.petqua.exception.auth

import com.petqua.common.exception.BaseException
import com.petqua.common.exception.BaseExceptionType
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.petqua.common.exception.auth
package com.petqua.exception.auth

import com.petqua.common.exception.BaseExceptionType
import org.springframework.http.HttpStatus
Expand All @@ -9,14 +9,14 @@ enum class AuthExceptionType(
private val errorMessage: String,
) : BaseExceptionType {

INVALID_TOKEN(BAD_REQUEST, "유효하지 않은 토큰입니다."),
EXPIRED_TOKEN(BAD_REQUEST, "만료된 토큰입니다."),
EXPIRED_ACCESS_TOKEN(BAD_REQUEST, "만료된 AccessToken입니다."),
EXPIRED_REFRESH_TOKEN(BAD_REQUEST, "만료된 RefreshToken입니다."),

NOT_RENEWABLE_ACCESS_TOKEN(BAD_REQUEST, "유효한 AccessToken은 갱신할 수 없습니다."),
INVALID_ACCESS_TOKEN(BAD_REQUEST, "올바른 형태의 AccessToken이 아닙니다."),
INVALID_REFRESH_TOKEN(BAD_REQUEST, "올바른 형태의 RefreshToken이 아닙니다."),

UNSUPPORTED_AUTHORITY(BAD_REQUEST, "해당하는 권한이 존재하지 않습니다."),

INVALID_REQUEST(BAD_REQUEST, "올바르지 않은 요청입니다."),
;

override fun httpStatus(): HttpStatus {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.petqua.common.exception.auth
package com.petqua.exception.auth

import com.petqua.common.exception.BaseException
import com.petqua.common.exception.BaseExceptionType
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.petqua.common.exception.auth
package com.petqua.exception.auth

import com.petqua.common.exception.BaseExceptionType
import org.springframework.http.HttpStatus
Expand Down
74 changes: 74 additions & 0 deletions src/main/kotlin/com/petqua/presentation/auth/AuthController.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package com.petqua.presentation.auth

import com.petqua.application.auth.AuthService
import com.petqua.application.auth.AuthTokenInfo
import com.petqua.domain.auth.oauth.OauthServerType
import org.springframework.http.HttpHeaders.AUTHORIZATION
import org.springframework.http.HttpHeaders.SET_COOKIE
import org.springframework.http.HttpStatus.FOUND
import org.springframework.http.ResponseCookie
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.CookieValue
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.RequestHeader
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController

@RequestMapping("/auth")
@RestController
class AuthController(
private val authService: AuthService
) {

@GetMapping("/{oauthServerType}")
fun redirectToAuthCodeRequestUrl(
@PathVariable oauthServerType: OauthServerType,
): ResponseEntity<Void> {
val redirectUri = authService.getAuthCodeRequestUrl(oauthServerType)
return ResponseEntity.status(FOUND)
.location(redirectUri)
.build()
}

@GetMapping("/login/{oauthServerType}")
fun login(
@PathVariable oauthServerType: OauthServerType,
@RequestParam("code") code: String,
): ResponseEntity<AuthResponse> {
val authTokenInfo = authService.login(oauthServerType, code)
val refreshTokenCookie = createRefreshTokenCookie(authTokenInfo)
val authResponse = AuthResponse(
accessToken = authTokenInfo.accessToken
)
return ResponseEntity
.ok()
.header(SET_COOKIE, refreshTokenCookie.toString())
.body(authResponse)
}

@GetMapping("/token")
fun extendLogin(
@RequestHeader(AUTHORIZATION) accessToken: String,
@CookieValue("refresh-token") refreshToken: String,
): ResponseEntity<AuthResponse> {
val authTokenInfo = authService.extendLogin(accessToken, refreshToken)
val refreshTokenCookie = createRefreshTokenCookie(authTokenInfo)
val authResponse = AuthResponse(
accessToken = authTokenInfo.accessToken
)
return ResponseEntity
.ok()
.header(SET_COOKIE, refreshTokenCookie.toString())
.body(authResponse)
}

private fun createRefreshTokenCookie(authTokenInfo: AuthTokenInfo): ResponseCookie {
return ResponseCookie.from("refresh-token", authTokenInfo.refreshToken)
.sameSite("None")
.secure(true)
.httpOnly(true)
.build()
}
}
5 changes: 5 additions & 0 deletions src/main/kotlin/com/petqua/presentation/auth/AuthDtos.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.petqua.presentation.auth

data class AuthResponse(
val accessToken: String,
)
Original file line number Diff line number Diff line change
@@ -1,26 +1,19 @@
package com.petqua.presentation.auth

import com.petqua.common.exception.auth.AuthException
import com.petqua.common.exception.auth.AuthExceptionType
import com.petqua.domain.auth.Auth
import com.petqua.domain.auth.LoginMember
import com.petqua.domain.auth.token.AuthTokenProvider
import com.petqua.domain.auth.token.RefreshTokenRepository
import jakarta.servlet.http.HttpServletRequest
import org.springframework.core.MethodParameter
import org.springframework.http.HttpHeaders
import org.springframework.http.HttpHeaders.AUTHORIZATION
import org.springframework.stereotype.Component
import org.springframework.web.bind.support.WebDataBinderFactory
import org.springframework.web.context.request.NativeWebRequest
import org.springframework.web.method.support.HandlerMethodArgumentResolver
import org.springframework.web.method.support.ModelAndViewContainer

private const val REFRESH_TOKEN_COOKIE = "refresh-token"

@Component
class LoginArgumentResolver(
private val authTokenProvider: AuthTokenProvider,
private val refreshTokenRepository: RefreshTokenRepository
) : HandlerMethodArgumentResolver {

override fun supportsParameter(parameter: MethodParameter): Boolean {
Expand All @@ -34,20 +27,8 @@ class LoginArgumentResolver(
webRequest: NativeWebRequest,
binderFactory: WebDataBinderFactory?
): LoginMember {
val request = webRequest.getNativeRequest(HttpServletRequest::class.java)
?: throw AuthException(AuthExceptionType.INVALID_REQUEST)
val refreshToken = request.cookies?.find { it.name == REFRESH_TOKEN_COOKIE }?.value
val accessToken = webRequest.getHeader(HttpHeaders.AUTHORIZATION) as String
val accessTokenClaims = authTokenProvider.getAccessTokenClaims(accessToken)
if (refreshToken == null) {
return LoginMember.from(accessTokenClaims)
}

val savedRefreshToken = refreshTokenRepository.findByMemberId(accessTokenClaims.memberId)
?: throw AuthException(AuthExceptionType.INVALID_REFRESH_TOKEN)
if (savedRefreshToken.token == refreshToken) {
return LoginMember.from(accessTokenClaims)
}
throw AuthException(AuthExceptionType.INVALID_REFRESH_TOKEN)
val accessToken = webRequest.getHeader(AUTHORIZATION) as String
val accessTokenClaims = authTokenProvider.getAccessTokenClaimsOrThrow(accessToken)
return LoginMember.from(accessTokenClaims)
}
}
Loading

0 comments on commit 086cc29

Please sign in to comment.