지난번에 예외발생 DTO를 생성까지 작성했다.
[Kotlin] 코틀린 + 스프링부트로 단숨에 완성하는 초강력 JWT 인증 시스템!💡 보안과 성능을 모두 잡아라!🔥
1. validation 추가
컨트롤러를 다시 보면 @RequestBody 옆에 @validated가 있었다. 이는 필요한 값이 안왔을 경우 예외를 발생시킨다.
@PostMapping("/signup")
fun signup(
request: HttpServletRequest,
@RequestBody @Validated(SignUpValidation::class) reqAuthDto: ReqAuthDto,
result: BindingResult
): ResponseEntity<ResAuthDto> {
if (result.hasErrors()) {
// 필드 오류 메시지만 가져오기
val errorMessage = result.fieldErrors
.joinToString(", ") { it.defaultMessage ?: "Unknown error" }
// 하나의 메시지만 반환
log.error(errorMessage)
throw InvalidCredentialsException(errorMessage)
}
// ip 가져오기
val ipAddress = ipAddressUtil.getClientIp(request)
reqAuthDto.ipAddress = ipAddress
val user = userService.signup(reqAuthDto)
val resAuthDto = ResAuthDto(email = user.email, message = "회원가입 성공!", token = "")
return ResponseEntity.ok(resAuthDto)
}
}
여기서는 reqAuthDto에 걸려있는데,
data class ReqAuthDto (
@field:NotEmpty(message = "이메일을 입력해주세요.")
val email: String,
@field:NotEmpty(message = "비밀번호를 입력해주세요.")
val password: String,
@field:NotEmpty(message = "이름을 입력해주세요.", groups = [SignUpValidation::class])
val name: String?,
var ipAddress: String?
)
회원가입 시 이메일, 비밀번호, 이름을 필수로 받는다. 하지만 로그인 할 때도 ReqAuthDto를 사용하는데 이때는 이름은 필요가 없어서 SignUpValidation을 만들었다.
interface SignUpValidation : Default
2. 토큰 필터 만들기
가입 후 로그인까지 했다면, 요청이 있을 때 마다 검사를 해줘야한다. 그래야 회원인지, 로그인을 했는지를 파악이 가능하기 때문에.
@Component
class JwtAuthenticationFilter(
private val jwtUtil: JwtUtil,
): OncePerRequestFilter() {
private val whiteList = listOf(
"/auth/**",
"/"
)
private fun isWhiteListed(request: HttpServletRequest): Boolean {
val requestURI = request.requestURI
// 화이트리스트 URL 패턴 확인
return whiteList.any { pattern ->
AntPathMatcher().match(pattern, requestURI)
}
}
override fun doFilterInternal(
request: HttpServletRequest,
response: HttpServletResponse,
filterChain: FilterChain
) {
if (!isWhiteListed(request)) {
val token = request.getHeader("Authorization")?.removePrefix("Bearer ")
if (!token.isNullOrEmpty() && jwtUtil.validateToken(token)) {
println("토큰 검증!")
} else {
// 액세스 토큰이 만료된 경우, 리프레시 토큰을 사용하여 액세스 토큰을 재발급
val refreshToken = request.getHeader("Refresh-Token")
if (!refreshToken.isNullOrEmpty() && jwtUtil.validateRefreshToken(refreshToken)) {
println("액세스토큰 재발급!")
val newToken = jwtUtil.generateToken(refreshToken)
response.setHeader("Authorization", "Bearer $newToken")
} else {
println("토큰 만료!")
response.status = HttpServletResponse.SC_UNAUTHORIZED
response.writer.write("Refresh token is required")
return
}
}
}
filterChain.doFilter(request, response)
}
}
코드를 보면 whiteList를 만들어 필터를 타지 않도록 했다. jwtUtil에 토큰을 만들어주고, 검사하는 로직을 넣어두었다.
@Component
class JwtUtil(
@Value("\${jwt.secret-key}") private val secretKey: String,
@Value("\${jwt.expiration}") private val expiration: Long,
@Value("\${jwt.refresh-secret-key}") private val refreshSecretKey: String,
@Value("\${jwt.refresh-expiration}") private val refreshExpiration: Long
) {
val key: SecretKey = Keys.hmacShaKeyFor(secretKey.toByteArray(StandardCharsets.UTF_8))
val refreshKey: SecretKey = Keys.hmacShaKeyFor(refreshSecretKey.toByteArray(StandardCharsets.UTF_8))
// 한국 시간대에 맞는 Date 객체를 생성하는 함수
fun generateToken(email: String): String {
val claims = mapOf("email" to email)
return Jwts.builder()
.claims(claims)
.issuedAt(Date())
.expiration(Date(System.currentTimeMillis() + expiration * 1000))
.signWith(key)
.compact()
}
fun generateRefreshToken(email: String): String {
val claims = mapOf("email" to email)
return Jwts.builder()
.claims(claims)
.issuedAt(Date())
.expiration(Date(System.currentTimeMillis() + refreshExpiration * 1000))
.signWith(refreshKey)
.compact()
}
fun validateToken(token: String): Boolean {
return try {
val claims = getClaims(token, key)
!claims.expiration.before(Date())
} catch (e: Exception) {
false
}
}
fun validateRefreshToken(token: String): Boolean {
return try {
val claims = getClaims(token, refreshKey)
!claims.expiration.before(Date())
} catch (e: Exception) {
false
}
}
fun getClaims(token: String, key: SecretKey): Claims {
return Jwts.parser()
.verifyWith(key)
.build()
.parseSignedClaims(token)
.payload
}
}
key는 application.yml에 넣어두었다.
3. 로그 작성
로그작성은 애플리케이션을 운영하는데 필수요소이다. 로그설정도 해보면,
application.yml에
logging:
level:
root: INFO
com.example.kotlinboilerplate: DEBUG
pattern:
console: '%d{yyyy-MM-dd HH:mm:ss} | %5p | %c{1} | %m%n'
file: '%d{yyyy-MM-dd HH:mm:ss} | %5p | %c{1} | %m%n'
file:
name: logs/app.log
logback:
rollingpolicy:
max-file-size: 10MB
max-history: 30
작성하고 로그가 필요한 컨트롤러나 서비스 등에
private val log = LoggerFactory.getLogger(this.javaClass)
log.error(errorMessage)
이런식으로 해두면, 커맨드창에 로그가 남겨지고
console: '%d{yyyy-MM-dd HH:mm:ss} | %5p | %c{1} | %m%n'
file: '%d{yyyy-MM-dd HH:mm:ss} | %5p | %c{1} | %m%n'
파일에도 같은 양식으로 로그가 저장된다.
나눠서 작성했더니 누락된 것이 있을 수도 있는데,
따라해보고 문제가 있을 경우 알려주시면 수정하겠습니다 ㅎㅎ
'개발 > Java & Kotlin' 카테고리의 다른 글
[Kotlin] 코틀린 + 스프링부트로 단숨에 완성하는 초강력 JWT 인증 시스템!💡 보안과 성능을 모두 잡아라!🔥 (54) | 2025.01.16 |
---|---|
[Spring] 동기, 비동기 차이 (70) | 2024.12.31 |
[Spring] Annotation 알아보기 (83) | 2024.12.25 |
[Java] 동일성과 동등성 알아보기 (85) | 2024.12.22 |
[Java] equals와 hashCode 알아보기 (82) | 2024.12.19 |