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]: Logout API 배포 #65

Merged
merged 2 commits into from
Aug 10, 2024
Merged
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
5 changes: 0 additions & 5 deletions core/core-api/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,6 @@ dependencies {
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("org.springframework.security:spring-security-oauth2-client")
implementation("org.springframework.boot:spring-boot-starter-validation")

// JWT
implementation("io.jsonwebtoken:jjwt-api:0.12.6")
implementation("io.jsonwebtoken:jjwt-impl:0.12.6")
implementation("io.jsonwebtoken:jjwt-jackson:0.12.6")
}

tasks.named<BootJar>("bootJar") {
Expand Down
42 changes: 40 additions & 2 deletions core/core-api/src/docs/asciidoc/auth.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
이 문서는 유저 인증/인가 관련 API에 대한 설명을 제공합니다.
OAuth2를 사용하여 인증을 수행합니다.

== 카카오 로그인 API
== 로그인/로그아웃

=== 카카오 로그인 (POST)

Expand All @@ -34,4 +34,42 @@ include::{snippets}/kakaoLoginPost/http-response.adoc[]

==== 응답 필드

include::{snippets}/kakaoLoginPost/response-body.adoc[]
include::{snippets}/kakaoLoginPost/response-body.adoc[]

=== 로그아웃 (DELETE)

==== 개요

로그아웃을 수행합니다.

==== Curl 요청

include::{snippets}/logout/curl-request.adoc[]

==== HTTP 응답

include::{snippets}/logout/http-response.adoc[]

==== 응답 필드

include::{snippets}/logout/response-body.adoc[]

== Token 갱신

=== AccessToken 갱신 (POST)

==== 개요

AccessToken을 갱신합니다.

==== Curl 요청

include::{snippets}/refreshToken/curl-request.adoc[]

==== HTTP 응답

include::{snippets}/refreshToken/http-response.adoc[]

==== 응답 필드

include::{snippets}/refreshToken/response-body.adoc[]
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package com.myongjiway.core.api.controller
import com.myongjiway.core.api.support.error.CoreApiException
import com.myongjiway.core.api.support.error.ErrorType
import com.myongjiway.core.api.support.response.ApiResponse
import com.myongjiway.error.CoreException
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.boot.logging.LogLevel
Expand All @@ -27,6 +28,12 @@ class ApiControllerAdvice {
@ExceptionHandler(Exception::class)
fun handleException(e: Exception): ResponseEntity<ApiResponse<Any>> {
log.error("Exception : {}", e.message, e)
return ResponseEntity(ApiResponse.error(ErrorType.DEFAULT_ERROR), ErrorType.DEFAULT_ERROR.status)
return ResponseEntity(ApiResponse.error(ErrorType.DEFAULT_ERROR, e.message), ErrorType.DEFAULT_ERROR.status)
}

@ExceptionHandler(CoreException::class)
fun handleCoreException(e: CoreException): ResponseEntity<ApiResponse<Any>> {
log.error("CoreException : {}", e.message, e)
return ResponseEntity(ApiResponse.error(e.coreErrorType, e.data), ErrorType.COMMON_ERROR.status)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.myongjiway.core.api.controller.v1.request

import jakarta.validation.constraints.NotBlank

data class RefreshRequest(
@field:NotBlank
val refreshToken: String,
)
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package com.myongjiway.core.api.support.error

enum class ErrorCode {
E500,

C400,
AUTH002,
AUTH003,
AUTH004,
Expand All @@ -11,7 +11,6 @@ enum class ErrorCode {
AUTH007,
AUTH009,
AUTH010,

U009,
U010,
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.myongjiway.core.api.support.error

import com.myongjiway.error.CoreErrorType

data class ErrorMessage private constructor(
val code: String,
val message: String,
Expand All @@ -10,4 +12,10 @@ data class ErrorMessage private constructor(
message = errorType.message,
data = data,
)

constructor(coreErrorType: CoreErrorType, data: Any? = null) : this(
code = coreErrorType.code.name,
message = coreErrorType.message,
data = data,
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,11 @@ import org.springframework.boot.logging.LogLevel
import org.springframework.http.HttpStatus

enum class ErrorType(val status: HttpStatus, val code: ErrorCode, val message: String, val logLevel: LogLevel) {
DEFAULT_ERROR(
HttpStatus.INTERNAL_SERVER_ERROR,
E500,
"An unexpected error has occurred.",
LogLevel.ERROR,
),
/**
* 기본 에러코드
*/
DEFAULT_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, E500, "An unexpected error has occurred.", LogLevel.ERROR),
COMMON_ERROR(HttpStatus.BAD_REQUEST, C400, "요청이 잘못되었습니다.", LogLevel.ERROR),

/**
* 인증 관련 에러코드
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,20 @@ package com.myongjiway.core.api.support.response

import com.myongjiway.core.api.support.error.ErrorMessage
import com.myongjiway.core.api.support.error.ErrorType
import com.myongjiway.error.CoreErrorType

data class ApiResponse<T> private constructor(
val result: ResultType,
val data: T? = null,
val error: ErrorMessage? = null,
) {
companion object {
fun success(): ApiResponse<Any> {
return ApiResponse(ResultType.SUCCESS, null, null)
}
fun success(): ApiResponse<Any> = ApiResponse(ResultType.SUCCESS, null, null)

fun <S> success(data: S): ApiResponse<S> {
return ApiResponse(ResultType.SUCCESS, data, null)
}
fun <S> success(data: S): ApiResponse<S> = ApiResponse(ResultType.SUCCESS, data, null)

fun <S> error(error: ErrorType, errorData: Any? = null): ApiResponse<S> {
return ApiResponse(ResultType.ERROR, null, ErrorMessage(error, errorData))
}
fun <S> error(error: ErrorType, errorData: Any? = null): ApiResponse<S> = ApiResponse(ResultType.ERROR, null, ErrorMessage(error, errorData))

fun <S> error(error: CoreErrorType, errorData: Any? = null): ApiResponse<S> = ApiResponse(ResultType.ERROR, null, ErrorMessage(error, errorData))
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
package com.myongjiway.core.auth.controller

import com.myongjiway.core.api.controller.v1.request.RefreshRequest
import com.myongjiway.core.api.support.response.ApiResponse
import com.myongjiway.core.auth.controller.v1.request.KakaoLoginRequest
import com.myongjiway.core.auth.security.domain.AuthService
import com.myongjiway.token.RefreshData
import com.myongjiway.token.TokenService
import jakarta.validation.Valid
import org.springframework.web.bind.annotation.DeleteMapping
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
Expand All @@ -13,6 +17,7 @@ import org.springframework.web.bind.annotation.RestController
@RestController
class AuthController(
private val authService: AuthService,
private val tokenService: TokenService,
) {
@PostMapping("/kakao-login")
fun kakaoLogin(
Expand All @@ -21,4 +26,20 @@ class AuthController(
val result = authService.kakaoLogin(request.toKakaoLoginData())
return ApiResponse.success(result)
}

@PostMapping("/refresh")
fun refresh(
@Valid @RequestBody request: RefreshRequest,
): ApiResponse<Any> {
val result = tokenService.refresh(RefreshData(request.refreshToken))
return ApiResponse.success(result)
}

@DeleteMapping("/logout")
fun logout(
@Valid @RequestBody request: RefreshRequest,
): ApiResponse<Any> {
tokenService.delete(request.refreshToken)
return ApiResponse.success()
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.myongjiway.core.auth.security.config

import com.myongjiway.core.auth.security.domain.JwtProvider
import com.myongjiway.core.auth.security.domain.JwtValidator
import com.myongjiway.core.auth.security.jwt.JwtAuthenticationFilter
import org.springframework.context.annotation.Configuration
import org.springframework.security.config.annotation.SecurityConfigurerAdapter
Expand All @@ -10,11 +10,11 @@ import org.springframework.security.web.authentication.UsernamePasswordAuthentic

@Configuration
class JwtSecurityConfig(
private val jwtProvider: JwtProvider,
private val jwtValidator: JwtValidator,
) : SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity>() {

override fun configure(http: HttpSecurity) {
val customFilter = JwtAuthenticationFilter(jwtProvider)
val customFilter = JwtAuthenticationFilter(jwtValidator)
http
.addFilterBefore(customFilter, UsernamePasswordAuthenticationFilter::class.java)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
package com.myongjiway.core.auth.security.domain

import com.myongjiway.token.RefreshData
import com.myongjiway.token.TokenAppender
import com.myongjiway.token.TokenGenerator
import com.myongjiway.token.TokenResult
import com.myongjiway.user.ProviderType
import com.myongjiway.user.Role
import com.myongjiway.user.UserAppender
import org.springframework.stereotype.Service

@Service
class AuthService(
private val jwtProvider: JwtProvider,
private val userAppender: UserAppender,
private val tokenAppender: TokenAppender,
private val tokenGenerator: TokenGenerator,
) {
fun kakaoLogin(toKakaoLoginData: KakaoLoginData): TokenResult {
val userId = userAppender.upsert(
Expand All @@ -21,10 +24,14 @@ class AuthService(
Role.USER,
)

val accessToken = jwtProvider.generateAccessTokenByUserId(userId.toString())
val refreshToken = jwtProvider.generateRefreshTokenByUserId(userId.toString())
val accessToken = tokenGenerator.generateAccessTokenByUserId(userId.toString())
val refreshToken = tokenGenerator.generateRefreshTokenByUserId(userId.toString())

tokenAppender.upsert(userId, refreshToken.token, refreshToken.expiration)
return TokenResult(accessToken.token, refreshToken.token)
}

fun refresh(refreshData: RefreshData): TokenResult {
TODO("Not yet implemented")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,10 @@ package com.myongjiway.core.auth.security.domain

import com.myongjiway.core.api.support.error.CoreApiException
import com.myongjiway.core.api.support.error.ErrorType
import com.myongjiway.core.auth.security.config.JwtProperty
import com.myongjiway.token.Token
import com.myongjiway.token.TokenType
import com.myongjiway.token.TokenType.*
import com.myongjiway.token.JwtProperty
import com.myongjiway.user.UserRepository
import io.jsonwebtoken.ExpiredJwtException
import io.jsonwebtoken.Jwts
import io.jsonwebtoken.Jwts.SIG.*
import io.jsonwebtoken.MalformedJwtException
import io.jsonwebtoken.UnsupportedJwtException
import io.jsonwebtoken.security.Keys
Expand All @@ -25,37 +21,10 @@ import java.lang.IllegalArgumentException
import java.util.Date

@Component
class JwtProvider(
class JwtValidator(
private val jwtProperty: JwtProperty,
private val userRepository: UserRepository,
) {
fun generateAccessTokenByUserId(userId: String): Token {
val (expiration, secret) = getExpirationAndSecret(ACCESS)
val expirationDate = Date(System.currentTimeMillis() + expiration)
return ACCESS.generate(
token = Jwts.builder()
.subject(userId)
.expiration(expirationDate)
.signWith(Keys.hmacShaKeyFor(secret.toByteArray()), HS512)
.compact(),
expiration = expirationDate,
userId = userId,
)
}

fun generateRefreshTokenByUserId(userId: String): Token {
val (expiration, secret) = getExpirationAndSecret(REFRESH)
val expirationDate = Date(System.currentTimeMillis() + expiration)
return REFRESH.generate(
token = Jwts.builder()
.expiration(expirationDate)
.signWith(Keys.hmacShaKeyFor(secret.toByteArray()), HS512)
.compact(),
expiration = expirationDate,
userId = userId,
)
}

fun validateAccessTokenFromRequest(servletRequest: ServletRequest, token: String?): Boolean {
try {
val claims =
Expand Down Expand Up @@ -93,12 +62,7 @@ class JwtProvider(
return UsernamePasswordAuthenticationToken(user, "", listOf(GrantedAuthority { user?.role?.value }))
}

private fun getExpirationAndSecret(tokenType: TokenType) = when (tokenType) {
ACCESS -> jwtProperty.accessToken.expiration to jwtProperty.accessToken.secret
REFRESH -> jwtProperty.refreshToken.expiration to jwtProperty.refreshToken.secret
}

companion object {
private val logger = LoggerFactory.getLogger(JwtProvider::class.java)
private val logger = LoggerFactory.getLogger("AuthenticationLog")
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.myongjiway.core.auth.security.jwt

import com.myongjiway.core.auth.security.domain.JwtProvider
import com.myongjiway.core.auth.security.domain.JwtValidator
import jakarta.servlet.FilterChain
import jakarta.servlet.http.HttpServletRequest
import jakarta.servlet.http.HttpServletResponse
Expand All @@ -13,7 +13,7 @@ import org.springframework.web.filter.OncePerRequestFilter

@Component
class JwtAuthenticationFilter(
private val jwtProvider: JwtProvider,
private val jwtValidator: JwtValidator,
) : OncePerRequestFilter() {
override fun doFilterInternal(
servletRequest: HttpServletRequest,
Expand All @@ -23,8 +23,8 @@ class JwtAuthenticationFilter(
val httpServletRequest = servletRequest as HttpServletRequest
val jwt = getJwt()
val requestURI = httpServletRequest.requestURI
if (!jwt.isNullOrBlank() && jwtProvider.validateAccessTokenFromRequest(servletRequest, jwt)) {
val authentication = jwtProvider.getAuthentication(jwt)
if (!jwt.isNullOrBlank() && jwtValidator.validateAccessTokenFromRequest(servletRequest, jwt)) {
val authentication = jwtValidator.getAuthentication(jwt)
SecurityContextHolder.getContext().authentication = authentication
Companion.logger.info("Security Context에 '${authentication.name}' 인증 정보를 저장했습니다. uri: $requestURI")
}
Expand Down
Loading
Loading