@@ -22,7 +22,6 @@ class SecurityConfig {
|
||||
.authorizeRequests()
|
||||
.antMatchers("/auth/members/sign-up").permitAll()
|
||||
.antMatchers("/auth/authenticate").permitAll()
|
||||
.antMatchers("/test").hasRole("USER") // 임시 인가 테스트용
|
||||
.anyRequest().authenticated()
|
||||
|
||||
.and()
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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!"
|
||||
}
|
||||
}
|
||||
@@ -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 = "계정 혹은 비밀번호가 일치하지 않습니다."
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -5,4 +5,4 @@ spring:
|
||||
password: beaniejoy # TODO 추후 보안에 대해 생각해보기
|
||||
jwt:
|
||||
secret_key: aG9wZS15b3UtYWx3YXlzLWJlLWhhcHB5LXRoaXMteWVhcgo=
|
||||
expiration_time: 86400
|
||||
validity_time_in_sec: 86400
|
||||
@@ -0,0 +1,6 @@
|
||||
package io.beaniejoy.dongnecafe.error
|
||||
|
||||
data class ErrorResponse(
|
||||
val code: Int,
|
||||
val message: String?
|
||||
)
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 ->
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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")
|
||||
@@ -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}]는 존재하지 않는 메뉴입니다.")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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")
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
@@ -5,4 +5,4 @@ spring:
|
||||
password: beaniejoy # TODO 추후 보안에 대해 생각해보기
|
||||
jwt:
|
||||
secret_key: aG9wZS15b3UtYWx3YXlzLWJlLWhhcHB5LXRoaXMteWVhcgo=
|
||||
expiration_time: 86400
|
||||
validity_time_in_sec: 86400
|
||||
@@ -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
|
||||
|
||||
@@ -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.*
|
||||
|
||||
@@ -14,4 +14,4 @@ logging:
|
||||
|
||||
jwt:
|
||||
secret_key: ZG9uZ25lLWNhZmUtcHJvamVjdC1rZXktZm9yLXRlc3QtY29kZQo
|
||||
expiration_time: 60
|
||||
validity_time_in_sec: 60
|
||||
Reference in New Issue
Block a user