Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
13 commits
Select commit Hold shift + click to select a range
7fc59e8
로그인_및_기타_인증_관련_로직_개발 : feat : member 엔티티 학번: student_number 변경 https:…
Chuseok22 Jan 12, 2026
bff02e5
로그인_및_기타_인증_관련_로직_개발 : feat : member 엔티티 학번: student_number 변경 https:…
Chuseok22 Jan 12, 2026
b957750
로그인_및_기타_인증_관련_로직_개발 : feat : member 엔티티 학번: student_number 변경 https:…
Chuseok22 Jan 12, 2026
2b774f8
로그인_및_기타_인증_관련_로직_개발 : feat : 로그인 로직 추가 https://github.com/CampusTabl…
Chuseok22 Jan 12, 2026
baf8e13
로그인_및_기타_인증_관련_로직_개발 : feat : 로그인 컨트롤러 추가 https://github.com/CampusTa…
Chuseok22 Jan 12, 2026
43e886c
로그인_및_기타_인증_관련_로직_개발 : feat : 지역변수 재사용 https://github.com/CampusTable…
Chuseok22 Jan 12, 2026
4c7feb9
로그인_및_기타_인증_관련_로직_개발 : feat : LoginRequest.kt validation 적용 https://g…
Chuseok22 Jan 12, 2026
6d0f759
로그인_및_기타_인증_관련_로직_개발 : feat : JWT 저장 및 삭제를 위한 TokenManager.kt 추가 http…
Chuseok22 Jan 12, 2026
ec8378b
로그인_및_기타_인증_관련_로직_개발 : feat : Reissue & 로그아웃 로직 추가 https://github.com…
Chuseok22 Jan 12, 2026
e21b082
로그인_및_기타_인증_관련_로직_개발 : feat : Reissue & 로그아웃 컨트롤러 추가 https://github.c…
Chuseok22 Jan 12, 2026
37e543c
로그인_및_기타_인증_관련_로직_개발 : feat : 저장된 리프레시 토큰 검증 로직 추가 https://github.com…
Chuseok22 Jan 12, 2026
7af97fa
로그인_및_기타_인증_관련_로직_개발 : feat : spring validate 및 requestBody 어노테이션 추가 …
Chuseok22 Jan 12, 2026
b214dd0
로그인_및_기타_인증_관련_로직_개발 : feat : 리프레시 토큰을 통한 reissue 과정 검증 강화 https://gi…
Chuseok22 Jan 12, 2026
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.chuseok22.ctauth.application.dto.request

import jakarta.validation.constraints.NotBlank

data class LoginRequest(
@field:NotBlank
val sejongPortalId: String,
@field:NotBlank
val sejongPortalPw: String
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.chuseok22.ctauth.application.dto.request

import jakarta.validation.constraints.NotBlank

data class ReissueRequest(
@field:NotBlank
val refreshToken: String
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.chuseok22.ctauth.application.dto.response

data class LoginResponse(
val studentNumber: String,
val name: String,
val accessToken: String,
val refreshToken: String
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.chuseok22.ctauth.application.dto.response

data class ReissueResponse(
val accessToken: String,
val refreshToken: String
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package com.chuseok22.ctauth.application.service

import com.chuseok22.ctauth.application.dto.request.LoginRequest
import com.chuseok22.ctauth.application.dto.request.ReissueRequest
import com.chuseok22.ctauth.application.dto.response.LoginResponse
import com.chuseok22.ctauth.application.dto.response.ReissueResponse
import com.chuseok22.ctauth.core.token.TokenManager
import com.chuseok22.ctauth.core.token.TokenProvider
import com.chuseok22.ctcommon.application.exception.CustomException
import com.chuseok22.ctcommon.application.exception.ErrorCode
import com.chuseok22.ctmember.infrastructure.entity.Member
import com.chuseok22.ctmember.infrastructure.repository.MemberRepository
import com.chuseok22.sejongportallogin.core.SejongMemberInfo
import com.chuseok22.sejongportallogin.infrastructure.SejongPortalLoginService
import io.github.oshai.kotlinlogging.KotlinLogging
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional

private val log = KotlinLogging.logger { }

@Service
class AuthService(
private val sejongPortalLoginService: SejongPortalLoginService,
private val memberRepository: MemberRepository,
private val tokenProvider: TokenProvider,
private val tokenManager: TokenManager
) {

@Transactional
fun login(request: LoginRequest): LoginResponse {
val sejongMemberInfo = sejongPortalLogin(request.sejongPortalId, request.sejongPortalPw)
val studentNumber = sejongMemberInfo.studentId
val name = sejongMemberInfo.name


val member = memberRepository.findByStudentNumberAndDeletedFalse(studentNumber)
?: run {
log.info { "신규 회원 로그인: 학번=$studentNumber, 이름=$name" }
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

개인정보 로깅 검토 필요

학번(studentNumber)과 이름(name)은 개인정보에 해당할 수 있습니다. info 레벨 로깅은 프로덕션 환경에서도 기록되므로, 개인정보보호법 및 내부 정책에 따라 마스킹 처리하거나 debug 레벨로 변경하는 것을 고려하세요.

🔒 마스킹 처리 예시
-        log.info { "신규 회원 로그인: 학번=$studentNumber, 이름=$name" }
+        log.info { "신규 회원 로그인: 학번=${studentNumber.take(4)}****, 이름=${name.first()}**" }
-    log.info { "로그인 성공: 학번=$studentNumber, 이름=$name" }
+    log.info { "로그인 성공: 학번=${studentNumber.take(4)}****, 이름=${name.first()}**" }

Also applies to: 47-47

🤖 Prompt for AI Agents
In
@CT-auth/src/main/kotlin/com/chuseok22/ctauth/application/service/AuthService.kt
at line 38, The log statement in AuthService that currently does log.info { "신규
회원 로그인: 학번=$studentNumber, 이름=$name" } exposes personal data; change it to
either log.debug(...) or mask the values before logging (e.g., show only last
2-4 chars or replace with asterisks) so that full studentNumber and name are not
written to info-level logs, and apply the same change to the other occurrence on
the file (the similar log at lines ~47). Locate these statements in AuthService
and replace the info-level prints with masked values or a debug-level log to
comply with privacy policy.

val newMember = Member.create(studentNumber, name)
memberRepository.save(newMember)
}

// 토큰 발급
val tokenPair = tokenManager.createTokenPair(member.id.toString())
tokenManager.saveRefreshTokenTtl(member.id.toString(), tokenPair.refreshToken)

log.info { "로그인 성공: 학번=$studentNumber, 이름=$name" }

return LoginResponse(
studentNumber = studentNumber,
name = name,
accessToken = tokenPair.accessToken,
refreshToken = tokenPair.refreshToken
)
}

fun reissue(request: ReissueRequest): ReissueResponse {
log.debug { "토큰 재발급을 진행합니다" }
val memberId = tokenProvider.getMemberId(request.refreshToken)

tokenManager.validateSavedToken(request.refreshToken)

log.debug { "새로운 accessToken, refreshToken 발급" }
val tokenPair = tokenManager.createTokenPair(memberId)
tokenManager.saveRefreshTokenTtl(memberId, tokenPair.refreshToken)

return ReissueResponse(
accessToken = tokenPair.accessToken,
refreshToken = tokenPair.refreshToken
)
}

fun logout(member: Member) {
val memberId = member.id
log.debug { "로그아웃을 진행합니다: 회원=$memberId" }
log.debug { "기존에 저장된 refreshToken 삭제" }
tokenManager.removeRefreshTokenTtl(memberId.toString())
}

private fun sejongPortalLogin(sejongPortalId: String, sejongPortalPw: String): SejongMemberInfo {
try {
log.debug { "세종대학교 포털 로그인을 시도합니다: $sejongPortalId" }
return sejongPortalLoginService.getMemberAuthInfos(sejongPortalId, sejongPortalPw)
} catch (e: Exception) {
log.error(e) { "세종대학교 포털 로그인 중 오류 발생: ${e.message}" }
throw CustomException(ErrorCode.SEJONG_PORTAL_LOGIN_FAILED)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.chuseok22.ctauth.core.token

interface TokenManager {

/**
* accessToken, refreshToken Pair 생성
*/
fun createTokenPair(memberId: String): TokenPair

/**
* refreshToken TTL 저장
*/
fun saveRefreshTokenTtl(memberId: String, refreshToken: String)

/**
* refreshToken TTL 삭제
*/
fun removeRefreshTokenTtl(memberId: String)

/**
* Redis에 저장된 토큰인지 검증
*/
fun validateSavedToken(token: String)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.chuseok22.ctauth.core.token

data class TokenPair(
val accessToken: String,
val refreshToken: String
)
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@ package com.chuseok22.ctauth.core.token

interface TokenStore {

/**
* Redis에 저장된 refreshToken 조회
*/
fun get(key: String): String?

/**
* 리프레시 토큰을 주어진 Key로 저장하고 TTL(ms) 설정
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,8 @@ object SecurityUrls {
// Swagger
"/docs/swagger-ui/**",
"/v3/api-docs/**",

// Health Check
"/actuator/health",
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package com.chuseok22.ctauth.infrastructure.jwt

import com.chuseok22.ctauth.core.token.TokenManager
import com.chuseok22.ctauth.core.token.TokenPair
import com.chuseok22.ctauth.core.token.TokenProvider
import com.chuseok22.ctauth.core.token.TokenStore
import com.chuseok22.ctauth.infrastructure.properties.JwtProperties
import com.chuseok22.ctauth.infrastructure.util.AuthUtil
import com.chuseok22.ctcommon.application.exception.CustomException
import com.chuseok22.ctcommon.application.exception.ErrorCode
import io.github.oshai.kotlinlogging.KotlinLogging
import io.jsonwebtoken.ExpiredJwtException
import org.springframework.stereotype.Component

private val log = KotlinLogging.logger { }

@Component
class JwtManager(
private val tokenProvider: TokenProvider,
private val tokenStore: TokenStore,
private val jwtProperties: JwtProperties
) : TokenManager {

override fun createTokenPair(memberId: String): TokenPair {
return TokenPair(
accessToken = tokenProvider.createAccessToken(memberId),
refreshToken = tokenProvider.createRefreshToken(memberId)
)
}

override fun saveRefreshTokenTtl(memberId: String, refreshToken: String) {
log.debug { "Redis에 refreshToken을 저장합니다" }
val key = getKey(memberId)
tokenStore.save(key, refreshToken, jwtProperties.refreshExpMillis)
}

override fun removeRefreshTokenTtl(memberId: String) {
log.debug { "Redis에 저장된 refreshToken을 삭제합니다: 회원=$memberId" }
val key = getKey(memberId)
tokenStore.remove(key)
}

override fun validateSavedToken(token: String) {

val memberId = try {
tokenProvider.getMemberId(token)
} catch (e: ExpiredJwtException) {
log.warn(e) { "만료된 refreshToken 사용 시도" }
throw e
} catch (e: Exception) {
log.warn(e) { "유효하지 않은 refreshToken 사용 시도" }
throw CustomException(ErrorCode.INVALID_JWT)
}

val key = getKey(memberId)
val savedToken = tokenStore.get(key) ?: run {
log.warn { "Redis에 refreshToken이 존재하지 않습니다: 회원=$memberId" }
throw CustomException(ErrorCode.INVALID_JWT)
}

if (savedToken != token) {
log.warn { "유효하지 않은 refreshToken 사용 시도: $memberId" }
throw CustomException(ErrorCode.INVALID_JWT)
}
}

private fun getKey(memberId: String): String {
return AuthUtil.getRefreshTokenTtlKey(memberId)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,15 @@ class JwtProvider(
category = AuthConstants.ACCESS_TOKEN_CATEGORY,
memberId = memberId,
expMillis = properties.accessExpMillis
).also { log.info { "엑세스 토큰 생성완료: memberId = $memberId" } }
).also { log.debug { "엑세스 토큰 생성완료: memberId = $memberId" } }
}

override fun createRefreshToken(memberId: String): String {
return createToken(
category = AuthConstants.REFRESH_TOKEN_CATEGORY,
memberId = memberId,
expMillis = properties.refreshExpMillis
).also { log.info { "리프레시 토큰 생성완료: memberId = $memberId" } }
).also { log.debug { "리프레시 토큰 생성완료: memberId = $memberId" } }
}

override fun isValidToken(token: String): Boolean {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ import java.util.concurrent.TimeUnit
class JwtStore(
private val redisTemplate: RedisTemplate<String, String>
) : TokenStore {

override fun get(key: String): String? {
return redisTemplate.opsForValue().get(key)
}

override fun save(key: String, refreshToken: String, ttlMillis: Long) {
redisTemplate.opsForValue().set(key, refreshToken, ttlMillis, TimeUnit.MILLISECONDS)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import org.springframework.security.core.userdetails.UserDetails
import java.security.Principal

class CustomUserDetails(
private val member: Member
val member: Member
) : UserDetails, UserPrincipal, Principal {
override fun getAuthorities(): Collection<GrantedAuthority> {
return listOf(SimpleGrantedAuthority(member.role.name))
Expand All @@ -24,7 +24,7 @@ class CustomUserDetails(
}

override fun getUsername(): String {
return member.studentName
return member.studentNumber
}

override fun getRoles(): List<String> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class CustomUserDetailsService(

@Transactional(readOnly = true)
override fun loadUserByUsername(username: String): UserDetails {
val member = memberRepository.findByStudentNameAndDeletedFalse(username)
val member = memberRepository.findByStudentNumberAndDeletedFalse(username)
?: throw CustomException(ErrorCode.MEMBER_NOT_FOUND)
return CustomUserDetails(member)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,7 @@ enum class ErrorCode(

// Member
MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "회원을 찾을 수 없습니다"),

// Auth
SEJONG_PORTAL_LOGIN_FAILED(HttpStatus.BAD_REQUEST, "세종대학교 포털 로그인 실패")
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ open class Member protected constructor() : BaseEntity() {
var id: UUID? = null
protected set

@field:Column(name = "student_name", nullable = false)
lateinit var studentName: String
@field:Column(name = "student_number", nullable = false)
lateinit var studentNumber: String
protected set

@field:Column(name = "name", nullable = false)
Expand All @@ -26,18 +26,18 @@ open class Member protected constructor() : BaseEntity() {
var role: Role = Role.ROLE_USER
protected set

private constructor(studentName: String, name: String, role: Role) : this() {
this.studentName = normalizeStudentName(studentName)
private constructor(studentNumber: String, name: String, role: Role) : this() {
this.studentNumber = normalizeStudentNumber(studentNumber)
this.name = normalizeName(name)
this.role = role
}

companion object {
fun create(studentName: String, name: String): Member {
return Member(studentName, name, Role.ROLE_USER)
fun create(studentNumber: String, name: String): Member {
return Member(studentNumber, name, Role.ROLE_USER)
}

private fun normalizeStudentName(raw: String): String {
private fun normalizeStudentNumber(raw: String): String {
val normalized: String = raw.trim()
require(normalized.isNotBlank()) { "학번은 필수로 입력되어야 합니다 " }
return normalized
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,5 @@ import java.util.*
interface MemberRepository : JpaRepository<Member, UUID> {

fun findByIdAndDeletedFalse(memberId: UUID): Member?
fun findByStudentNameAndDeletedFalse(studentName: String): Member?
fun findByStudentNumberAndDeletedFalse(studentNumber: String): Member?
}
Loading