[#24] feat: JWT 활용한 인가 프로세스 구현

- 인가 프로세스용 Security Filter 적용
- jwt token 유효성 체크 및 claim 조회를 통한 authentication 반환 메소드 추가
This commit is contained in:
Hanbin Lee
2022-11-21 00:55:12 +09:00
parent 7037214909
commit c5e2115611
9 changed files with 140 additions and 26 deletions

View File

@@ -22,7 +22,6 @@ class SecurityConfig {
.authorizeRequests()
.antMatchers("/auth/members/sign-up").permitAll()
.antMatchers("/auth/authenticate").permitAll()
.antMatchers("/test").hasRole("USER") // 임시 인가 테스트용
.anyRequest().authenticated()
.and()

View File

@@ -1,13 +0,0 @@
package io.beaniejoy.dongnecafe.controller
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController
@RestController
class TestController {
@GetMapping("/test")
fun test(): String {
return "authorize OK!"
}
}

View File

@@ -5,4 +5,4 @@ spring:
password: beaniejoy # TODO 추후 보안에 대해 생각해보기
jwt:
secret_key: aG9wZS15b3UtYWx3YXlzLWJlLWhhcHB5LXRoaXMteWVhcgo=
expiration_time: 86400
validity_time_in_sec: 86400

View File

@@ -1,11 +1,15 @@
package io.beaniejoy.dongnecafe.security
import io.jsonwebtoken.Claims
import io.jsonwebtoken.ExpiredJwtException
import io.jsonwebtoken.Jwts
import io.jsonwebtoken.SignatureAlgorithm
import io.jsonwebtoken.security.Keys
import mu.KLogging
import org.springframework.beans.factory.annotation.Value
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.security.core.Authentication
import org.springframework.security.core.authority.SimpleGrantedAuthority
import org.springframework.stereotype.Component
import java.security.Key
import java.util.*
@@ -14,25 +18,56 @@ import java.util.*
class JwtTokenUtils(
@Value("\${jwt.secret_key}")
private val secretKey: String,
@Value("\${jwt.expiration_time}")
private val expirationTime: Long
@Value("\${jwt.validity_time_in_sec}")
private val validityTimeSec: Long
) {
private val key: Key = Keys.hmacShaKeyFor(secretKey.toByteArray())
private val validityTimeMilliSec: Long = validityTimeSec * 1000
companion object: KLogging()
companion object : KLogging() {
const val AUTHORITIES_KEY = "authorities"
}
fun createToken(authentication: Authentication): String {
logger.info { "test = ${authentication.name}" }
val authenticatedMember = (authentication.principal as SecurityUser).member
val authorities = authentication.authorities.joinToString(",") { it.authority }
val nowTime = Date().time
val expirationDate = Date(nowTime + this.expirationTime)
val expirationDate = Date(nowTime + this.validityTimeMilliSec)
return Jwts.builder()
.setSubject(authenticatedMember.email)
.claim("memberId", authenticatedMember.id)
.claim("email", authenticatedMember.email)
.claim("roles", authentication.authorities.joinToString(",") { it.authority })
.claim(AUTHORITIES_KEY, authorities)
.signWith(key, SignatureAlgorithm.HS256)
.setExpiration(expirationDate)
.compact()
}
fun getAuthentication(accessToken: String): Authentication? {
val claims = getValidTokenBody(accessToken)
?: return null
val authorities = claims[AUTHORITIES_KEY].toString().split(",")
.map { SimpleGrantedAuthority(it) }
return UsernamePasswordAuthenticationToken(claims.subject, accessToken, authorities)
}
// jwt access token 유효성 검증 및 claims 획득
private fun getValidTokenBody(accessToken: String): Claims? {
return try {
Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(accessToken)
.body
} catch (e: ExpiredJwtException) {
logger.error { "JWT access token expired. > Error: ${e.message}" }
null
} catch (e: Exception) {
logger.error { "JWT access token invalid. > Error: ${e.message}" }
null
}
}
}

View File

@@ -1,5 +1,8 @@
package io.beaniejoy.dongnecafe.common.config
import io.beaniejoy.dongnecafe.security.JwtAuthenticationConfigurer
import io.beaniejoy.dongnecafe.security.JwtTokenUtils
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.autoconfigure.security.servlet.PathRequest
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
@@ -11,19 +14,28 @@ import org.springframework.security.web.SecurityFilterChain
@Configuration
@EnableWebSecurity
class SecurityConfig {
@Autowired
lateinit var jwtTokenUtils: JwtTokenUtils
@Bean
fun filterChain(http: HttpSecurity): SecurityFilterChain {
return http
.csrf().disable()
.formLogin().disable()
.authorizeRequests()
.anyRequest().authenticated() // 임시 허용
.anyRequest().authenticated()
.and()
.also { jwtAuthenticationConfigurer(it) }
.build()
}
private fun jwtAuthenticationConfigurer(http: HttpSecurity) {
http
.apply(JwtAuthenticationConfigurer())
.jwtTokenUtils(jwtTokenUtils)
}
@Bean
fun webSecurityCustomizer(): WebSecurityCustomizer {
return WebSecurityCustomizer { web ->

View File

@@ -0,0 +1,25 @@
package io.beaniejoy.dongnecafe.security
import io.beaniejoy.dongnecafe.security.filter.JwtAuthenticationFilter
import org.springframework.security.config.annotation.SecurityConfigurerAdapter
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.web.DefaultSecurityFilterChain
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
class JwtAuthenticationConfigurer :
SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity>() {
private var jwtTokenUtils: JwtTokenUtils? = null
override fun configure(http: HttpSecurity) {
http
.addFilterBefore(
JwtAuthenticationFilter(this.jwtTokenUtils!!),
UsernamePasswordAuthenticationFilter::class.java
)
}
fun jwtTokenUtils(jwtTokenUtils: JwtTokenUtils): JwtAuthenticationConfigurer {
this.jwtTokenUtils = jwtTokenUtils
return this
}
}

View File

@@ -0,0 +1,56 @@
package io.beaniejoy.dongnecafe.security.filter
import io.beaniejoy.dongnecafe.security.JwtTokenUtils
import mu.KLogging
import mu.KotlinLogging
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.web.filter.GenericFilterBean
import javax.servlet.FilterChain
import javax.servlet.ServletRequest
import javax.servlet.ServletResponse
import javax.servlet.http.HttpServletRequest
class JwtAuthenticationFilter(
private val jwtTokenUtils: JwtTokenUtils
) : GenericFilterBean() {
private val log = KotlinLogging.logger {}
companion object {
private const val AUTHORIZATION = "Authorization"
private const val BEARER = "Bearer"
private const val WHITESPACE = " "
}
/**
* JWT access token 인가 처리
*/
override fun doFilter(request: ServletRequest, response: ServletResponse, chain: FilterChain) {
val httpRequest = request as HttpServletRequest
log.info { "[JwtAuthenticationFilter][${request.dispatcherType}][${request.requestURI}]" }
getAccessToken(httpRequest)?.let {
jwtTokenUtils.getAuthentication(it)
}?.also {
SecurityContextHolder.getContext().authentication = it
log.info { "Valid Access Token [${it.name}]" }
}
chain.doFilter(request, response)
}
private fun getAccessToken(request: HttpServletRequest): String? {
val bearer = request.getHeader(AUTHORIZATION)
?: return null
val splitBearer = bearer.split(WHITESPACE)
if (splitBearer.first() != BEARER) {
return null
}
if (splitBearer.size != 2 || splitBearer.last().isBlank()) {
return null
}
return splitBearer.last()
}
}

View File

@@ -5,4 +5,4 @@ spring:
password: beaniejoy # TODO 추후 보안에 대해 생각해보기
jwt:
secret_key: aG9wZS15b3UtYWx3YXlzLWJlLWhhcHB5LXRoaXMteWVhcgo=
expiration_time: 86400
validity_time_in_sec: 86400

View File

@@ -14,4 +14,4 @@ logging:
jwt:
secret_key: ZG9uZ25lLWNhZmUtcHJvamVjdC1rZXktZm9yLXRlc3QtY29kZQo
expiration_time: 60
validity_time_in_sec: 60