generated from pricelees/issue-pr-template
feat: 새로운 인증 API 구현
This commit is contained in:
parent
66ae7d7beb
commit
3c71562317
100
src/main/kotlin/roomescape/auth/business/AuthServiceV2.kt
Normal file
100
src/main/kotlin/roomescape/auth/business/AuthServiceV2.kt
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
package roomescape.auth.business
|
||||||
|
|
||||||
|
import io.github.oshai.kotlinlogging.KLogger
|
||||||
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
|
import org.springframework.stereotype.Service
|
||||||
|
import org.springframework.transaction.annotation.Transactional
|
||||||
|
import roomescape.admin.business.AdminService
|
||||||
|
import roomescape.auth.exception.AuthErrorCode
|
||||||
|
import roomescape.auth.exception.AuthException
|
||||||
|
import roomescape.auth.infrastructure.jwt.JwtUtils
|
||||||
|
import roomescape.auth.web.LoginContext
|
||||||
|
import roomescape.auth.web.LoginRequestV2
|
||||||
|
import roomescape.auth.web.LoginSuccessResponse
|
||||||
|
import roomescape.common.dto.CurrentUserContext
|
||||||
|
import roomescape.common.dto.LoginCredentials
|
||||||
|
import roomescape.common.dto.PrincipalType
|
||||||
|
import roomescape.member.business.UserService
|
||||||
|
|
||||||
|
private val log: KLogger = KotlinLogging.logger {}
|
||||||
|
|
||||||
|
const val CLAIM_PERMISSION_KEY = "permission"
|
||||||
|
const val CLAIM_TYPE_KEY = "principal_type"
|
||||||
|
|
||||||
|
@Service
|
||||||
|
class AuthServiceV2(
|
||||||
|
private val adminService: AdminService,
|
||||||
|
private val userService: UserService,
|
||||||
|
private val loginHistoryService: LoginHistoryService,
|
||||||
|
private val jwtUtils: JwtUtils,
|
||||||
|
) {
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
fun login(
|
||||||
|
request: LoginRequestV2,
|
||||||
|
context: LoginContext
|
||||||
|
): LoginSuccessResponse {
|
||||||
|
log.info { "[AuthService.login] 로그인 시작: account=${request.account}, type=${request.principalType}" }
|
||||||
|
|
||||||
|
val extraClaims: MutableMap<String, Any> = mutableMapOf()
|
||||||
|
|
||||||
|
val credentials: LoginCredentials = when (request.principalType) {
|
||||||
|
PrincipalType.ADMIN -> {
|
||||||
|
adminService.findCredentialsByAccount(request.account).also {
|
||||||
|
extraClaims.put(CLAIM_PERMISSION_KEY, it.permissionLevel)
|
||||||
|
extraClaims.put(CLAIM_TYPE_KEY, PrincipalType.ADMIN)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PrincipalType.USER -> {
|
||||||
|
userService.findCredentialsByAccount(request.account).also {
|
||||||
|
extraClaims.put(CLAIM_TYPE_KEY, PrincipalType.USER)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (credentials.password != request.password) {
|
||||||
|
log.info { "[AuthService.login] 비밀번호 불일치로 인한 로그인 실패: account = ${request.account}" }
|
||||||
|
throw AuthException(AuthErrorCode.LOGIN_FAILED)
|
||||||
|
}
|
||||||
|
|
||||||
|
val accessToken = jwtUtils.createToken(subject = credentials.id.toString(), claims = extraClaims)
|
||||||
|
return LoginSuccessResponse(accessToken)
|
||||||
|
.also {
|
||||||
|
log.info { "[AuthService.login] 관리자 로그인 완료: account = ${request.account}, id=${credentials.id}" }
|
||||||
|
loginHistoryService.createSuccessHistory(credentials.id, PrincipalType.ADMIN, context)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
log.warn { "[AuthService.login] 관리자 로그인 실패: account = ${request.account}, message=${e.message}" }
|
||||||
|
loginHistoryService.createFailureHistory(credentials.id, PrincipalType.ADMIN, context)
|
||||||
|
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
fun checkLogin(context: CurrentUserContext): CurrentUserContext {
|
||||||
|
return findContextById(context.id, context.type).also {
|
||||||
|
if (it != context) {
|
||||||
|
throw AuthException(AuthErrorCode.MEMBER_NOT_FOUND)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
fun findContextById(id: Long, type: PrincipalType): CurrentUserContext {
|
||||||
|
log.info { "[AuthService.checkLogin] 로그인 확인 시작: id=${id}, type=${type}" }
|
||||||
|
|
||||||
|
return when (type) {
|
||||||
|
PrincipalType.ADMIN -> {
|
||||||
|
adminService.findContextById(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
PrincipalType.USER -> {
|
||||||
|
userService.findContextById(id)
|
||||||
|
}
|
||||||
|
}.also {
|
||||||
|
log.info { "[AuthService.checkLogin] 로그인 확인 완료: id=${id}, type=${type}" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
52
src/main/kotlin/roomescape/auth/docs/AuthAPIV2.kt
Normal file
52
src/main/kotlin/roomescape/auth/docs/AuthAPIV2.kt
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
package roomescape.auth.docs
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.Operation
|
||||||
|
import io.swagger.v3.oas.annotations.responses.ApiResponse
|
||||||
|
import io.swagger.v3.oas.annotations.responses.ApiResponses
|
||||||
|
import io.swagger.v3.oas.annotations.tags.Tag
|
||||||
|
import jakarta.servlet.http.HttpServletRequest
|
||||||
|
import jakarta.servlet.http.HttpServletResponse
|
||||||
|
import jakarta.validation.Valid
|
||||||
|
import org.springframework.http.ResponseEntity
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody
|
||||||
|
import roomescape.auth.web.LoginRequestV2
|
||||||
|
import roomescape.auth.web.LoginSuccessResponse
|
||||||
|
import roomescape.auth.web.support.CurrentUser
|
||||||
|
import roomescape.auth.web.support.Public
|
||||||
|
import roomescape.common.dto.CurrentUserContext
|
||||||
|
import roomescape.common.dto.response.CommonApiResponse
|
||||||
|
|
||||||
|
@Tag(name = "1. 인증 / 인가 API", description = "로그인, 로그아웃 및 로그인 상태를 확인합니다")
|
||||||
|
interface AuthAPIV2 {
|
||||||
|
|
||||||
|
@Public
|
||||||
|
@Operation(summary = "로그인")
|
||||||
|
@ApiResponses(
|
||||||
|
ApiResponse(responseCode = "200", description = "로그인 성공시 토큰을 반환합니다."),
|
||||||
|
)
|
||||||
|
fun login(
|
||||||
|
@Valid @RequestBody loginRequest: LoginRequestV2,
|
||||||
|
servletRequest: HttpServletRequest
|
||||||
|
): ResponseEntity<CommonApiResponse<LoginSuccessResponse>>
|
||||||
|
|
||||||
|
@Operation(summary = "로그인 상태 확인")
|
||||||
|
@ApiResponses(
|
||||||
|
ApiResponse(
|
||||||
|
responseCode = "200",
|
||||||
|
description = "입력된 ID / 결과(Boolean)을 반환합니다.",
|
||||||
|
useReturnTypeSchema = true
|
||||||
|
),
|
||||||
|
)
|
||||||
|
fun checkLogin(
|
||||||
|
@CurrentUser user: CurrentUserContext
|
||||||
|
): ResponseEntity<CommonApiResponse<CurrentUserContext>>
|
||||||
|
|
||||||
|
@Operation(summary = "로그아웃", tags = ["로그인이 필요한 API"])
|
||||||
|
@ApiResponses(
|
||||||
|
ApiResponse(responseCode = "200"),
|
||||||
|
)
|
||||||
|
fun logout(
|
||||||
|
@CurrentUser user: CurrentUserContext,
|
||||||
|
servletResponse: HttpServletResponse
|
||||||
|
): ResponseEntity<CommonApiResponse<Unit>>
|
||||||
|
}
|
||||||
48
src/main/kotlin/roomescape/auth/web/AuthControllerV2.kt
Normal file
48
src/main/kotlin/roomescape/auth/web/AuthControllerV2.kt
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
package roomescape.auth.web
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest
|
||||||
|
import jakarta.servlet.http.HttpServletResponse
|
||||||
|
import org.springframework.http.ResponseEntity
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping
|
||||||
|
import org.springframework.web.bind.annotation.RestController
|
||||||
|
import roomescape.auth.business.AuthServiceV2
|
||||||
|
import roomescape.auth.docs.AuthAPIV2
|
||||||
|
import roomescape.auth.web.support.CurrentUser
|
||||||
|
import roomescape.common.dto.CurrentUserContext
|
||||||
|
import roomescape.common.dto.response.CommonApiResponse
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/auth")
|
||||||
|
class AuthControllerV2(
|
||||||
|
private val authService: AuthServiceV2,
|
||||||
|
) : AuthAPIV2 {
|
||||||
|
|
||||||
|
@PostMapping("/login")
|
||||||
|
override fun login(
|
||||||
|
loginRequest: LoginRequestV2,
|
||||||
|
servletRequest: HttpServletRequest
|
||||||
|
): ResponseEntity<CommonApiResponse<LoginSuccessResponse>> {
|
||||||
|
val response = authService.login(request = loginRequest, context = servletRequest.toLoginContext())
|
||||||
|
|
||||||
|
return ResponseEntity.ok(CommonApiResponse(response))
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/login/check")
|
||||||
|
override fun checkLogin(
|
||||||
|
@CurrentUser user: CurrentUserContext,
|
||||||
|
): ResponseEntity<CommonApiResponse<CurrentUserContext>> {
|
||||||
|
val response = authService.checkLogin(user)
|
||||||
|
|
||||||
|
return ResponseEntity.ok(CommonApiResponse(response))
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/logout")
|
||||||
|
override fun logout(
|
||||||
|
@CurrentUser user: CurrentUserContext,
|
||||||
|
servletResponse: HttpServletResponse
|
||||||
|
): ResponseEntity<CommonApiResponse<Unit>> {
|
||||||
|
return ResponseEntity.ok().build()
|
||||||
|
}
|
||||||
|
}
|
||||||
24
src/main/kotlin/roomescape/auth/web/AuthDTOV2.kt
Normal file
24
src/main/kotlin/roomescape/auth/web/AuthDTOV2.kt
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
package roomescape.auth.web
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest
|
||||||
|
import roomescape.common.dto.PrincipalType
|
||||||
|
|
||||||
|
data class LoginContext(
|
||||||
|
val ipAddress: String,
|
||||||
|
val userAgent: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun HttpServletRequest.toLoginContext() = LoginContext(
|
||||||
|
ipAddress = this.remoteAddr,
|
||||||
|
userAgent = this.getHeader("User-Agent")
|
||||||
|
)
|
||||||
|
|
||||||
|
data class LoginRequestV2(
|
||||||
|
val account: String,
|
||||||
|
val password: String,
|
||||||
|
val principalType: PrincipalType
|
||||||
|
)
|
||||||
|
|
||||||
|
data class LoginSuccessResponse(
|
||||||
|
val accessToken: String
|
||||||
|
)
|
||||||
Loading…
x
Reference in New Issue
Block a user