feat: 새로운 인증 API 구현

This commit is contained in:
이상진 2025-09-11 17:05:34 +09:00
parent 66ae7d7beb
commit 3c71562317
4 changed files with 224 additions and 0 deletions

View 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}" }
}
}
}

View 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>>
}

View 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()
}
}

View 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
)