์ด์ด์ ๊ณ์ ๊ตฌํํด๋ณด๊ฒ ์ต๋๋ค.
๊ตฌํ ์์ฒด๊ฐ ๋ชฉ์ ์ด๋ฏ๋ก ์์ธํ ์ค๋ช ์ ์๋ตํฉ๋๋ค.
- JWT ์์ฑํ๊ธฐ
- JWT์ ๊ถํ ์ถ๊ฐํด์ฃผ๊ธฐ
- ์์ฑํ JWT์ ๋ํด ์ธ์ฆ/์ธ๊ฐ ํ๊ธฐ (์งํ)
- JWT ์ฌ๋ฐ๊ธ ํด์ฃผ๊ธฐ
๊ตฌํ
JwtFilter
class JwtFilter(
private val jwtProvider: JwtProvider
) : OncePerRequestFilter() {
override fun doFilterInternal(
request: HttpServletRequest,
response: HttpServletResponse,
filterChain: FilterChain
) {
val jwt = jwtProvider.resolveToken(request)
if (StringUtils.hasText(jwt) && jwtProvider.validateAccessToken(jwt)) {
val authentication = jwtProvider.findAuthentication(jwt)
SecurityContextHolder.getContext().authentication = authentication
}
filterChain.doFilter(request, response)
}
}
JWT ๊ฒ์ฆ์ Filter๋ฅผ ํตํด์ ์งํ๋ฉ๋๋ค.
resolveToken์ header์์ bearer๋ฅผ ์ ์ธํ ์์ ํ ํฐ๋ง์ ๊ฐ์ ธ์ต๋๋ค.
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(oAuth2User: OAuth2User) : JwtDto {
val now = Date().time
val accessTokenExpiresIn: Date = Date(now + ACCESS_TOKEN_EXPIRE_TIME)
val member = memberRepository.findByEmail(oAuth2User.attributes["email"] as String)
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)
.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 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
}
}
}
resolveToken, validateAccessToken, findAuthentication, parseClaims ํจ์๊ฐ ์ถ๊ฐ๋์์ต๋๋ค.
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("/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)
}
}
/guest ์ /admin์ ์ถ๊ฐํด์ฃผ์์ต๋๋ค.
MainController
@RestController
class MainController {
@GetMapping("/success")
fun success(): String {
return "success"
}
@GetMapping("/fail")
fun fail(): String {
return "fail"
}
@GetMapping("/guest")
fun guest(): String {
return "guest"
}
@GetMapping("/admin")
fun admin(): String {
return "admin"
}
}
์คํ
1. ๊ตฌ๊ธ ๊ณ์ ์ผ๋ก ๋ก๊ทธ์ธ์ ํ ํ, jwt์ ๋ฐ๊ธ ๋ฐ์ต๋๋ค.
2. ๋ฐ๊ธ ๋ฐ์ ํ ํฐ์ User๋ก /guest๋ก ์ ๊ทผ์ด ๊ฐ๋ฅํฉ๋๋ค.
3. ์ด๋ฒ์ Admin์ด ์ ๊ทผ ๊ฐ๋ฅํ /admin๋ก ์ ๊ทผ์ ์๋ํด๋ด ๋๋ค.
4. Forbidden์ด ๋จ๋ ๊ฑธ ํ์ธํ ์ ์๊ณ , ์ ๊ทผ์ ํ๊ณ ์ถ์ผ๋ฉด DB์ ์ ์ฅ๋ ROLE_USER๋ฅผ ROLE_ADMIN์ผ๋ก ๋ฐ๊พธ๋ฉด ๋ฉ๋๋ค.