코틀린 스프링 5번째 시간입니다.
로그인을 통해 JWT를 발행하는 부분 한번 같이 해보겠습니다.
이 기록 겸 강의는 초보자 기준으로 작성이 되었고, 새로 시작하게 된 코틀린 스프링 유저에게 많은 도움이 될 수 있었다면 좋겠다는 마음으로 시작합니다.
틀린 부분이 있다면 알려주시면 감사하겠습니다.
시작하기 앞서 JWT에 대해 설명드려 보겠습니다.
- JWT(JSON Web Token): 당사자 간에 정보를 안전하게 전송하기 위한 콤팩트하고 독립적인 방법을 정의하는 개방형 표준(RFC 7519)입니다.
토큰의 구조는 점으로 구분된 세부분으로 구성됩니다.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
HEADER(알고리즘과 토큰유형) | PayLoad(데이터) | Signature(서명) |
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 | eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ | SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c |
데이터의 값은 쉽게 열어 볼 수 있기 때문에 PayLoad에는 너무 많은 정보, 유출되어선 안 되는 정보를 넣으면 안 됩니다.
쉽게 JWT는 사원증 같은 거라고 보시면 될 것 같아요.
사원증을 가진 사람은 회사에 들어갈 수 있지만 없는 사람은 못 들어가죠, 또 다른 예로 누군가가 사원증을 훔칠 경우 들어갈 수도 있겠죠?
그런 사항에 대해서도 신경을 써주는 게 좋습니다.
그럼 만들기 전에 dependencies를 추가하겠습니다
Maven Repository 에서 추가할 항목은 4가지입니다.
- Spring Boot Starter Security
- JJWT :: API
- JJWT :: Impl
- JJWT :: Jackson
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("io.jsonwebtoken:jjwt-api:0.12.6")
runtimeOnly("io.jsonwebtoken:jjwt-impl:0.12.6")
runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.12.6")
Spring Boot Starter Security는 버전을 지워줍니다.
그리고 application.yml 하단에 secretKey 추가해줍니다.
jwt:
secret: {32글자 이상}
암호화할 때 사용되는 부분입니다. 아무에게도 알려줘서는 안 됩니다.
또한 유추하기 쉽게 한다면 쉽게 해킹을 당할 수 있으니
추천사항은 해킹하는 사람인척 키보드 아무렇게나 누르는 걸 하면 괜찮지 않을까 생각해 봅니다.
core > authority 패키지를 생성합니다.
토큰의 정보를 담을 Info를 만들어 보겠습니다.
TokenInfo클래스 생성해 줍니다.
data class TokenInfo(
val grantType: String,
val accessToken: String,
)
지금은 accessToken 하나지만 추후에 추가해주겠습니다.
JwTokenProvider.kt를 만들어보겠습니다.
여기서는 token을 발행하고 정보를 추출하거나, 유효한 토큰인지 검증합니다.
const val EXPIRATION_MILLISECONDS: Long = 1000 * 60 * 30
@Component
class JwtTokenProvider {
@Value("\${jwt.access_secret}")
lateinit var secretKey: String
private val key by lazy {
Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretKey))
}
/**
* token 생성
*/
fun createToken(authentication: Authentication): TokenInfo {
val authorities: String = authentication
.authorities
.joinToString(",", transform = GrantedAuthority::getAuthority)
val now = Date()
val accessExpiration = Date(now.time + EXPIRATION_MILLISECONDS)
// Access Token
val accessToken = Jwts
.builder()
.subject(authentication.name)
.claim("auth", authorities)
.issuedAt(now)
.expiration(accessExpiration)
.signWith(key, Jwts.SIG.HS256)
.compact()
return TokenInfo("Bearer", accessToken)
}
/**
*token 정보 추출
*/
fun getAuthentication(token: String): Authentication {
val claims: Claims = getClaims(token)
val auth = claims["auth"] ?: throw RuntimeException(" 잘못된 토큰 입니다.")
// 권한 정보 추출
val authorities: Collection<GrantedAuthority> = (auth as String)
.split(",")
.map { SimpleGrantedAuthority(it) }
val principal: UserDetails = User(claims.subject, "", authorities)
return UsernamePasswordAuthenticationToken(principal, "", authorities)
}
/**
* Token 검증
*/
fun validateToken(token: String): Boolean {
try {
getClaims(token)
return true
} catch (e: Exception) {
when (e) {
is SecurityException -> {} // Invalid JWT Token
is MalformedJwtException -> {} // Invalid JWT Token
is ExpiredJwtException -> {} // Expired JWT Token
is UnsupportedJwtException -> {} // Unsupported JWT Token
is IllegalArgumentException -> {} // JWT claims string is empty
else -> {} // else
}
println(e.message)
}
return false
}
private fun getClaims(token: String): Claims =
Jwts.parser()
.verifyWith(key)
.build()
.parseSignedClaims(token)
.payload
}
EXPIRATION_MILLISECONDS를 통해서 만료시간을 지정할 수 있게끔 설정합니다.
현재는 30분으로 설정했지만, 추후에 수정해 주도록겠습니다.
토큰 발행 하는 부분을 좀 더 보겠습니다.
val accessToken = Jwts
- builder() : JWT객체를 생성하기 위한 빌더 초기화입니다.
- subject(authentication.name) : 토큰의 주체를 설정하는 부분으로 여기서는 이름을 지정하고 있습니다.
- claim("auth", authorities) : auth라는 키값에 사용자의 권한 정보를 넣습니다.
- issuedAt(now) : 토큰이 발급된 시간을 뜻하는데 now를 넣어줌으로써 지금으로 설정합니다.
- expiration(accessExpiration) : 만료 시간을 설정합니다. 위에서 현재 시간 + 설정해 둔 시간이 된 값이 들어왔습니다. 현재기준 30분 뒤 만료됨을 알 수 있습니다.
- signWith(key, Jwts.SIG.HS256) : key를 사용하여 HS256 알고리즘으로 서명한다는 내용입니다.
- compact() : 모든 설정을 바탕으로 JWT문자열을 생성합니다.
JWT 인증 필터 구현 하겠습니다.
JwtAuthenticationFilter 클래스를 생성해 줍니다.
class JwtAuthenticationFilter(
private val jwtTokenProvider: JwtTokenProvider
) : GenericFilterBean() {
override fun doFilter(request: ServletRequest?, response: ServletResponse?, chain: FilterChain?){
val token = resolveToken(request as HttpServletRequest)
if (token != null && jwtTokenProvider.validateToken(token)) {
val authentication = jwtTokenProvider.getAuthentication(token)
SecurityContextHolder.getContext().authentication = authentication
}
chain?.doFilter(request, response)
}
private fun resolveToken(request: HttpServletRequest): String? {
val bearerToken = request.getHeader("Authorization")
return if (StringUtils.hasText(bearerToken) &&
bearerToken.startsWith("Bearer")) {
bearerToken.substring(7)
} else {
null
}
}
}
- JwtAuthenticationFilter클래스는 GenericFilterBean을 상속받아서 필터 기능을 구현합니다.
- 생성자에서 JwtTokenProvider를 인스턴스 받아서 JWT토큰의 유효성 검사와 인증 정보를 가져옵니다.
- doFilter 메서드는 필터의 핵심으로, 요청이 들어올 때마다 호출됩니다.
- resolveToken 메서드를 호출하여 요청 헤더에서 JWT토큰을 추출합니다.
- 토큰이 유효하면 JwtTokenProvider를 사용하여 인증 정보를 가져오고, SecurityContextHolder에 설정합니다.
- 다음 필터로 요청을 전달하기 위해 chain.doFilter(request, response)를 호출합니다
- Authorization헤더에서 "Bearer"로 시작하는 문자열을 찾고, 토큰 부분만 반환합니다.
- 유효한 형식이 아닐 경우 null을 반환합니다.
여기까지 진행했을 때, 서버를 가동하고 서버에 접속을 하려고 하면 접속이 안될 겁니다.
비밀번호 입력하라는 내용이 나오게 될 것인데, API를 제작하고 접속하려면 제한이 없어야 하기에 그 부분을 수정해 주겠습니다.
SecurityConfog를 만들어주겠습니다.
@Configuration
@EnableWebSecurity
class SecurityConfig(
private val jwtTokenProvider: JwtTokenProvider
) {
@Bean
fun filterChain(http: HttpSecurity): SecurityFilterChain {
http
.httpBasic { it.disable() }
.csrf { it.disable() }
.sessionManagement {
it.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
}
.authorizeHttpRequests {
it.requestMatchers("/","/member/signup").anonymous()
.anyRequest().permitAll()
}
.addFilterBefore(
JwtAuthenticationFilter(jwtTokenProvider),
UsernamePasswordAuthenticationFilter::class.java
)
return http.build()
}
@Bean
fun passwordEncoder(): PasswordEncoder =
PasswordEncoderFactories.createDelegatingPasswordEncoder()
}
- @Configuration: 설정 클래스로 사용됨을 나타냅니다.
- @EnableWebSecurity: Spring Security기능을 활성화합니다.
- Security클래스는 JwtTokenProvider를 주입받아 보안 설정을 구성합니다.
- filterChain 메서드
- authorizeHttpRequests: 요청에 대한 권한 부여 규칙을 설정합니다. "/member/signup"이란 endPoint에는 익명 사용자(로그인하지 않은 사용자)가 접근할 수 있도록 설정합니다. 추후에 endpoint가 많아질 경우 처리 방법에 대해서도 이야기해볼 생각입니다.
- 추가로 하단에 표를 작성하여 권한 부여에 관한 내용을 같이 보여드리겠습니다.
permitAll() | 권한의 여부에 관계없이 모두 접근 가능 |
denyAll() | 권한의 여부에 관계없이 모두 접근 불가능 |
anonymous() | 인증을 되지 않은 사용자일 경우에 접근 가능 |
rememberMe() | Remember-me기능으로 로그인한 사용자일 경우 |
authenticated() | 인증을 한 사용자인 경우 |
fullyAuthenticated() | 인증되지 않은 사용자가 아니고, Remember-me기능으로 로그인하지 않은 사용자 일 경우 |
hasRole("auth1") | auth1이라는 권한을 가지고 있는 경우(auth1은 원하는 권한으로 변경 가능) |
hasAnyRole("auth1", "auth2", "auth3") | 권한들 중 하나라도 가지고 있는 경우 (갯수 제한은 없습니다.) |
JWT를 만들어봤고 다음은 회원가입시 권한부여 및 로그인구현까지 진행해 보겠습니다.
부족한 글 읽어 주셔서 감사합니다.
피드백은 언제든 환영입니다.
'BackEnd > 코틀린 스프링' 카테고리의 다른 글
코틀린 스프링 스터디 일지 0 (0) | 2025.04.01 |
---|---|
[Kotlin Spring] 프로젝트 05. JWT를 이용해 로그인 구현하기 02: 구현 하기 (2) | 2024.11.04 |
[Kotlin Spring] 프로젝트 Extra00. InteliJ에서 http통신 보내기 (0) | 2024.10.29 |
[Kotlin Spring] 프로젝트 03. 예외 처리 (1) | 2024.10.29 |
[Kotlin Spring] 프로젝트 02. 회원 가입 데이터 값 확인 (0) | 2024.10.28 |