Merge pull request #25 from beaniejoy/feature/24

JWT 활용한 인가 프로세스 적용
This commit is contained in:
Hanbin Lee
2022-11-29 01:17:23 +09:00
committed by GitHub
24 changed files with 189 additions and 41 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

@@ -22,7 +22,7 @@ class UserDetailsServiceImpl(
return memberRepository.findByEmail(email)?.let {
logger.info { "[LOAD MEMBER] email: ${it.email}, role: ${it.roleType}, activated: ${it.activated}" }
createSecurityUser(it)
} ?: throw UsernameNotFoundException(email)
} ?: throw UsernameNotFoundException("${email} is not found")
}
private fun createSecurityUser(member: Member): SecurityUser {

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

@@ -0,0 +1,27 @@
package io.beaniejoy.dongnecafe.error.handler
import io.beaniejoy.dongnecafe.error.ErrorResponse
import mu.KLogging
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.security.core.AuthenticationException
import org.springframework.web.bind.annotation.ExceptionHandler
import org.springframework.web.bind.annotation.RestControllerAdvice
// TODO 통합된 에러 핸들링 필요(ErrorResponse 규격화)
@RestControllerAdvice
class CommonControllerAdvice {
companion object : KLogging()
@ExceptionHandler(AuthenticationException::class)
fun handleAuthenticationException(e: AuthenticationException): ResponseEntity<ErrorResponse> {
logger.error { "AuthenticationException: ${e.message}" }
return ResponseEntity.ok().body(
ErrorResponse(
code = HttpStatus.BAD_REQUEST.value(),
message = "계정 혹은 비밀번호가 일치하지 않습니다."
)
)
}
}

View File

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

View File

@@ -0,0 +1,6 @@
package io.beaniejoy.dongnecafe.error
data class ErrorResponse(
val code: Int,
val message: String?
)

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,8 +1,12 @@
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
import org.springframework.http.HttpMethod
import org.springframework.security.config.annotation.web.builders.HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer
@@ -11,19 +15,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

@@ -1,7 +1,7 @@
package io.beaniejoy.dongnecafe.domain.cafe.service
import io.beaniejoy.dongnecafe.domain.cafe.model.response.CafeMenuDetailedInfo
import io.beaniejoy.dongnecafe.domain.cafe.error.CafeMenuNotFoundException
import io.beaniejoy.dongnecafe.error.exception.CafeMenuNotFoundException
import io.beaniejoy.dongnecafe.domain.cafe.model.request.CafeMenuUpdateRequest
import io.beaniejoy.dongnecafe.domain.cafe.repository.CafeMenuRepository
import org.springframework.data.repository.findByIdOrNull

View File

@@ -4,8 +4,8 @@ import io.beaniejoy.dongnecafe.domain.cafe.model.response.CafeDetailedInfo
import io.beaniejoy.dongnecafe.domain.cafe.model.response.CafeSearchInfo
import io.beaniejoy.dongnecafe.domain.cafe.model.request.CafeMenuRegisterRequest
import io.beaniejoy.dongnecafe.domain.cafe.entity.Cafe
import io.beaniejoy.dongnecafe.domain.cafe.error.CafeExistedException
import io.beaniejoy.dongnecafe.domain.cafe.error.CafeNotFoundException
import io.beaniejoy.dongnecafe.error.exception.CafeExistedException
import io.beaniejoy.dongnecafe.error.exception.CafeNotFoundException
import io.beaniejoy.dongnecafe.domain.cafe.repository.CafeRepository
import mu.KLogging
import org.springframework.data.domain.Page

View File

@@ -1,6 +1,6 @@
package io.beaniejoy.dongnecafe.domain.cafe.service
import io.beaniejoy.dongnecafe.domain.cafe.error.MenuOptionNotFoundException
import io.beaniejoy.dongnecafe.error.exception.MenuOptionNotFoundException
import io.beaniejoy.dongnecafe.domain.cafe.model.request.MenuOptionUpdateRequest
import io.beaniejoy.dongnecafe.domain.cafe.repository.MenuOptionRepository
import org.springframework.data.repository.findByIdOrNull

View File

@@ -1,6 +1,6 @@
package io.beaniejoy.dongnecafe.domain.cafe.service
import io.beaniejoy.dongnecafe.domain.cafe.error.OptionDetailNotFoundException
import io.beaniejoy.dongnecafe.error.exception.OptionDetailNotFoundException
import io.beaniejoy.dongnecafe.domain.cafe.model.request.OptionDetailUpdateRequest
import io.beaniejoy.dongnecafe.domain.cafe.repository.OptionDetailRepository
import org.springframework.data.repository.findByIdOrNull

View File

@@ -1,5 +1,6 @@
package io.beaniejoy.dongnecafe.domain.cafe.error
package io.beaniejoy.dongnecafe.error
import io.beaniejoy.dongnecafe.error.exception.CafeNotFoundException
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.ExceptionHandler
import org.springframework.web.bind.annotation.RestControllerAdvice

View File

@@ -1,3 +1,3 @@
package io.beaniejoy.dongnecafe.domain.cafe.error
package io.beaniejoy.dongnecafe.error.exception
class CafeExistedException(name: String) : RuntimeException("Cafe[$name] is already existed")

View File

@@ -1,4 +1,4 @@
package io.beaniejoy.dongnecafe.domain.cafe.error
package io.beaniejoy.dongnecafe.error.exception
class CafeMenuNotFoundException(menuId: Long, cafeId: Long) :
RuntimeException("Cafe[${cafeId}]의 Menu[${menuId}]는 존재하지 않는 메뉴입니다.")

View File

@@ -1,3 +1,3 @@
package io.beaniejoy.dongnecafe.domain.cafe.error
package io.beaniejoy.dongnecafe.error.exception
class CafeNotFoundException(cafeId: Long) : RuntimeException("Cafe[$cafeId] is not found")

View File

@@ -1,3 +1,3 @@
package io.beaniejoy.dongnecafe.domain.cafe.error
package io.beaniejoy.dongnecafe.error.exception
class MenuOptionNotFoundException(menuOptionId: Long) : RuntimeException("MenuOption[$menuOptionId] is not found")

View File

@@ -1,3 +1,3 @@
package io.beaniejoy.dongnecafe.domain.cafe.error
package io.beaniejoy.dongnecafe.error.exception
class OptionDetailNotFoundException(optionDetailId: Long) : RuntimeException("OptionDetail[$optionDetailId] is not found")

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,55 @@
package io.beaniejoy.dongnecafe.security.filter
import io.beaniejoy.dongnecafe.security.JwtTokenUtils
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}] uri: ${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

@@ -1,7 +1,7 @@
package io.beaniejoy.dongnecafe.domain.cafe.service
import io.beaniejoy.dongnecafe.domain.cafe.entity.CafeMenu
import io.beaniejoy.dongnecafe.domain.cafe.error.CafeMenuNotFoundException
import io.beaniejoy.dongnecafe.error.exception.CafeMenuNotFoundException
import io.beaniejoy.dongnecafe.domain.cafe.repository.CafeMenuRepository
import io.beaniejoy.dongnecafe.domain.cafe.repository.MenuOptionRepository
import io.beaniejoy.dongnecafe.domain.cafe.repository.OptionDetailRepository

View File

@@ -1,8 +1,8 @@
package io.beaniejoy.dongnecafe.domain.cafe.service
import io.beaniejoy.dongnecafe.domain.cafe.entity.Cafe
import io.beaniejoy.dongnecafe.domain.cafe.error.CafeExistedException
import io.beaniejoy.dongnecafe.domain.cafe.error.CafeNotFoundException
import io.beaniejoy.dongnecafe.error.exception.CafeExistedException
import io.beaniejoy.dongnecafe.error.exception.CafeNotFoundException
import io.beaniejoy.dongnecafe.domain.cafe.repository.CafeRepository
import io.beaniejoy.dongnecafe.domain.cafe.utils.CafeTestUtils
import org.junit.jupiter.api.*

View File

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