์ด์ด์ ๊ณ์ ๊ตฌํํด๋ณด๊ฒ ์ต๋๋ค.
๊ตฌํ ์์ฒด๊ฐ ๋ชฉ์ ์ด๋ฏ๋ก ์์ธํ ์ค๋ช ์ ์๋ตํฉ๋๋ค.
- JWT ์์ฑํ๊ธฐ
- JWT์ ๊ถํ ์ถ๊ฐํด์ฃผ๊ธฐ
- ์์ฑํ JWT์ ๋ํด ์ธ์ฆ/์ธ๊ฐ ํ๊ธฐ
- JWT ์ฌ๋ฐ๊ธ ํด์ฃผ๊ธฐ (์งํ)
๊ตฌํ
AuthController
@RestController
@RequestMapping("/auth")
class AuthController(
private val authService: AuthService
) {
/**
* token ์์ฑํด์ ๋ณด๋ด์ฃผ๊ธฐ
*/
@GetMapping("/login")
fun login(@AuthenticationPrincipal oAuth2User: OAuth2User): ResponseEntity<JwtDto> {
return ResponseEntity.ok(authService.login(oAuth2User))
}
@PostMapping("/reissue")
fun reissue(@RequestBody reissueRequestDto: ReissueRequestDto): ResponseEntity<JwtDto> {
return ResponseEntity.ok(authService.reissue(reissueRequestDto))
}
}
AuthService
@Service
class AuthService(
private val memberRepository: MemberRepository,
private val jwtProvider: JwtProvider
) {
@Transactional
fun login(oAuth2User: OAuth2User) : JwtDto {
//TODO: 1. ํ์์ด ์๋๋ผ๋ฉด ํ์ ๊ฐ์
์ ์์ผ์ค๋ค.
if(!memberRepository.existsByEmail(oAuth2User.attributes["email"] as String)) {
val member = Member(
email = oAuth2User.attributes["email"] as String,
role = AuthType.ROLE_USER
)
memberRepository.save(member)
}
//TODO: 2. token ์ ์์ฑํด์ค๋ค.
return jwtProvider.generateJwtDto(oAuth2User.attributes["email"] as String)
}
@Transactional(readOnly = true)
fun reissue(reissueRequestDto: ReissueRequestDto): JwtDto {
jwtProvider.validateRefreshToken(reissueRequestDto.refreshToken)
val authentication = jwtProvider.findAuthentication(reissueRequestDto.refreshToken)
return jwtProvider.generateJwtDto(authentication.name)
}
}
JwtProvider
@Component
class JwtProvider(
private val memberRepository: MemberRepository
) {
companion object {
private const val AUTHORITIES_KEY = "auth"
private const val BEARER_TYPE = "bearer"
private const val ACCESS_TOKEN_EXPIRE_TIME = (1000 * 60 * 30)
private const val REFRESH_TOKEN_EXPIRE_TIME = (1000 * 60 * 60 * 24 * 7)
}
private val key: Key by lazy {
val secretKey: String = "ZVc3Z0g4bm5TVzRQUDJxUXBIOGRBUGtjRVg2WDl0dzVYVkMyWWs1Qlk3NkZBOXh1UzNoRWUzeTd6cVdEa0x2eQo=" // base64Encoded
Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretKey))
}
fun generateJwtDto(email: String) : JwtDto {
val now = Date().time
val accessTokenExpiresIn: Date = Date(now + ACCESS_TOKEN_EXPIRE_TIME)
val member = memberRepository.findByEmail(email)
val accessToken = Jwts.builder()
.setSubject(member?.email) // payload "sub": "email"
.claim(AUTHORITIES_KEY, member?.role) // payload "auth": "ROLE_USER"
.setExpiration(accessTokenExpiresIn) // payload "exp": 1516239022 (์์)
.signWith(key, SignatureAlgorithm.HS512) // header "alg": "HS512"
.compact()
val refreshToken = Jwts.builder()
.setSubject(member?.email)
.claim(AUTHORITIES_KEY, member?.role)
.setExpiration(Date(now + REFRESH_TOKEN_EXPIRE_TIME))
.signWith(key, SignatureAlgorithm.HS512)
.compact()
return JwtDto(
grantType = BEARER_TYPE,
accessToken = accessToken,
refreshToken = refreshToken,
accessTokenExpiresIn = accessTokenExpiresIn.time
)
}
fun resolveToken(request: HttpServletRequest): String {
val bearerToken = request.getHeader(HttpHeaders.AUTHORIZATION)
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(AccessTokenType.BEARER.value)) {
return bearerToken.substring(7)
}
return ""
}
fun validateAccessToken(token: String): Boolean {
try {
Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token)
return true
} catch (e: SecurityException) {
println("์ฌ๋ฐ๋ฅด์ง ๋ชปํ ํ ํฐ์
๋๋ค.")
} catch (e: MalformedJwtException) {
println("์ฌ๋ฐ๋ฅด์ง ๋ชปํ ํ ํฐ์
๋๋ค.")
} catch (e: ExpiredJwtException) {
println("๋ง๋ฃ๋ ํ ํฐ์
๋๋ค.")
} catch (e: UnsupportedJwtException) {
println("์ง์๋์ง ์๋ ํ ํฐ์
๋๋ค.")
} catch (e: IllegalArgumentException) {
println("์๋ชป๋ ํ ํฐ์
๋๋ค.")
}
return true
}
fun validateRefreshToken(token: String) {
try {
Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token)
} catch (e: SecurityException) {
println("์ฌ๋ฐ๋ฅด์ง ๋ชปํ ํ ํฐ์
๋๋ค.")
} catch (e: MalformedJwtException) {
println("์ฌ๋ฐ๋ฅด์ง ๋ชปํ ํ ํฐ์
๋๋ค.")
} catch (e: ExpiredJwtException) {
println("๋ง๋ฃ๋ ํ ํฐ์
๋๋ค.")
} catch (e: UnsupportedJwtException) {
println("์ง์๋์ง ์๋ ํ ํฐ์
๋๋ค.")
} catch (e: IllegalArgumentException) {
println("์๋ชป๋ ํ ํฐ์
๋๋ค.")
}
}
fun findAuthentication(accessToken: String): Authentication {
val claims = parseClaims(accessToken)
val authorities = mutableListOf(claims[AUTHORITIES_KEY] as String).map { role -> SimpleGrantedAuthority(role) }
val user = User(claims[Claims.SUBJECT] as String, "", authorities)
return UsernamePasswordAuthenticationToken(user, "", authorities)
}
private fun parseClaims(accessToken: String): Claims {
return try {
Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(accessToken).body
} catch (e: ExpiredJwtException) {
e.claims
}
}
}
ReissueRequestDto
data class ReissueRequestDto(
val refreshToken: String
) {
}
SecurityConfig
@Configuration
class SecurityConfig(
private val customUserDetailService: CustomUserDetailService,
private val jwtProvider: JwtProvider
) : WebSecurityConfigurerAdapter() {
override fun configure(http: HttpSecurity) {
http
.csrf {
it.disable()
}
.httpBasic {
it.disable()
}
.sessionManagement {
it.sessionCreationPolicy(SessionCreationPolicy.NEVER)
}
.authorizeRequests {
it.antMatchers("/auth/reissue").permitAll()
it.antMatchers("/guest").hasRole("USER")
it.antMatchers("/admin").hasRole("ADMIN")
}
.oauth2Login {
it.userInfoEndpoint().userService(customUserDetailService)
it.defaultSuccessUrl("/auth/login")
it.failureUrl("/fail")
}
.addFilterBefore(JwtFilter(jwtProvider), UsernamePasswordAuthenticationFilter::class.java)
}
}
์คํ
1. POST /auth/reissue๋ก ์ ๊ทผํฉ๋๋ค. body์๋ refreshToken์ด ๋ด๊น๋๋ค.
์ด๋ ๊ฒ Spring Security๋ฅผ ์ด์ฉํด์ Oauth2, JWT๋ฅผ ๊ตฌํํด๋ณด์์ต๋๋ค.
์ฝ๋๋ Best Practice๋ ์๋๋ฉฐ ์์ธ์ฒ๋ฆฌ, ์คํธ๋ง ์ํฐ ํจํด ๋ฑ์ ํ์ฉํ์ฌ์ ์ถ๊ฐ ๊ฐ๋ฐํ์๊ธธ ๋ฐ๋๋๋ค.
์ ์ฒด ์ฝ๋๋ ๊นํ์์ ํ์ธํ์ค ์ ์์ต๋๋ค.