[#30] feat: 인증 여부 체크 API 도입 및 Security 관련 리팩토링
- 인증 여부 체크 API 개발(GET /auth/check) - 인증, 인가 Exception 처리에 대한 Security 적용 - 기타 리팩토링
This commit is contained in:
@@ -1,5 +1,10 @@
|
||||
package io.beaniejoy.dongnecafe.common.config
|
||||
|
||||
import io.beaniejoy.dongnecafe.security.JwtTokenUtils
|
||||
import io.beaniejoy.dongnecafe.security.config.JwtAuthenticationConfigurer
|
||||
import io.beaniejoy.dongnecafe.security.handler.CustomAccessDeniedHandler
|
||||
import io.beaniejoy.dongnecafe.security.handler.CustomAuthenticationEntryPoint
|
||||
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
|
||||
@@ -14,6 +19,15 @@ import org.springframework.security.web.SecurityFilterChain
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
class SecurityConfig {
|
||||
@Autowired
|
||||
lateinit var jwtTokenUtils: JwtTokenUtils
|
||||
|
||||
@Autowired
|
||||
lateinit var customAccessDeniedHandler: CustomAccessDeniedHandler
|
||||
|
||||
@Autowired
|
||||
lateinit var customAuthenticationEntryPoint: CustomAuthenticationEntryPoint
|
||||
|
||||
@Bean
|
||||
fun filterChain(http: HttpSecurity): SecurityFilterChain {
|
||||
return http
|
||||
@@ -28,10 +42,22 @@ class SecurityConfig {
|
||||
.sessionManagement()
|
||||
.sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 토큰 방식(세션 불필요)
|
||||
|
||||
.and()
|
||||
.also { jwtAuthenticationConfigurer(it) }
|
||||
.exceptionHandling()
|
||||
.authenticationEntryPoint(customAuthenticationEntryPoint) // 인증 예외 entryPoint 적용
|
||||
.accessDeniedHandler(customAccessDeniedHandler) // 인가 예외 handler 적용
|
||||
|
||||
.and()
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun jwtAuthenticationConfigurer(http: HttpSecurity) {
|
||||
http
|
||||
.apply(JwtAuthenticationConfigurer())
|
||||
.jwtTokenUtils(jwtTokenUtils)
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun webSecurityCustomizer(): WebSecurityCustomizer {
|
||||
return WebSecurityCustomizer { web ->
|
||||
|
||||
@@ -5,6 +5,9 @@ import io.beaniejoy.dongnecafe.security.JwtTokenUtils
|
||||
import io.beaniejoy.dongnecafe.domain.member.model.request.SignInRequest
|
||||
import io.beaniejoy.dongnecafe.model.TokenResponse
|
||||
import io.beaniejoy.dongnecafe.service.AuthService
|
||||
import mu.KLogging
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.PostMapping
|
||||
import org.springframework.web.bind.annotation.RequestBody
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
@@ -16,6 +19,8 @@ class AuthController(
|
||||
private val authService: AuthService,
|
||||
private val jwtTokenUtils: JwtTokenUtils
|
||||
) {
|
||||
companion object: KLogging()
|
||||
|
||||
@PostMapping("/authenticate")
|
||||
fun signIn(@RequestBody signInRequest: SignInRequest): ApplicationResponse<TokenResponse> {
|
||||
val authentication = authService.signIn(
|
||||
@@ -29,4 +34,13 @@ class AuthController(
|
||||
.success("success authenticate")
|
||||
.data(TokenResponse(accessToken))
|
||||
}
|
||||
|
||||
@GetMapping("/check")
|
||||
fun checkAuthenticated(@AuthenticationPrincipal principal: String?): ApplicationResponse<String> {
|
||||
logger.info { "[Authentication Principal] $principal" }
|
||||
|
||||
return ApplicationResponse
|
||||
.success("authenticated")
|
||||
.data(principal)
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,9 @@
|
||||
package io.beaniejoy.dongnecafe.security
|
||||
|
||||
import io.beaniejoy.dongnecafe.common.error.constant.ErrorCode
|
||||
import io.beaniejoy.dongnecafe.common.error.exception.BusinessException
|
||||
import io.beaniejoy.dongnecafe.security.SecurityUser
|
||||
import mu.KLogging
|
||||
import org.springframework.security.authentication.AuthenticationProvider
|
||||
import org.springframework.security.authentication.BadCredentialsException
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
|
||||
import org.springframework.security.core.Authentication
|
||||
import org.springframework.security.core.userdetails.UserDetailsService
|
||||
@@ -30,7 +29,7 @@ class ApiAuthenticationProvider(
|
||||
|
||||
val user = userDetailsService.loadUserByUsername(email) as SecurityUser
|
||||
if (!passwordEncoder.matches(password, user.password)) {
|
||||
throw BusinessException(ErrorCode.AUTH_PASSWORD_NOT_VALID)
|
||||
throw BadCredentialsException(ErrorCode.AUTH_PASSWORD_NOT_VALID.name)
|
||||
}
|
||||
|
||||
logger.info { "User password ${user.password}" }
|
||||
|
||||
@@ -4,10 +4,10 @@ import io.beaniejoy.dongnecafe.common.error.constant.ErrorCode
|
||||
import io.beaniejoy.dongnecafe.common.error.exception.BusinessException
|
||||
import io.beaniejoy.dongnecafe.domain.member.entity.Member
|
||||
import io.beaniejoy.dongnecafe.domain.member.repository.MemberRepository
|
||||
import io.beaniejoy.dongnecafe.security.SecurityUser
|
||||
import mu.KLogging
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority
|
||||
import org.springframework.security.core.userdetails.UserDetailsService
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException
|
||||
import org.springframework.stereotype.Component
|
||||
import org.springframework.transaction.annotation.Transactional
|
||||
|
||||
@@ -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 BusinessException(ErrorCode.AUTH_MEMBER_NOT_FOUND)
|
||||
} ?: throw UsernameNotFoundException(ErrorCode.AUTH_MEMBER_NOT_FOUND.name)
|
||||
}
|
||||
|
||||
private fun createSecurityUser(member: Member): SecurityUser {
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package io.beaniejoy.dongnecafe.security.config
|
||||
|
||||
import io.beaniejoy.dongnecafe.security.JwtTokenUtils
|
||||
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 lateinit var jwtTokenUtils: JwtTokenUtils
|
||||
|
||||
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,52 @@
|
||||
package io.beaniejoy.dongnecafe.security.filter
|
||||
|
||||
import io.beaniejoy.dongnecafe.security.JwtTokenUtils
|
||||
import io.beaniejoy.dongnecafe.security.constant.SecurityConstant.BEARER
|
||||
import io.beaniejoy.dongnecafe.security.constant.SecurityConstant.WHITESPACE
|
||||
import mu.KotlinLogging
|
||||
import org.springframework.http.HttpHeaders
|
||||
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 {}
|
||||
|
||||
/**
|
||||
* 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(HttpHeaders.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()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package io.beaniejoy.dongnecafe.security.handler
|
||||
|
||||
import mu.KLogging
|
||||
import org.springframework.security.access.AccessDeniedException
|
||||
import org.springframework.security.web.access.AccessDeniedHandler
|
||||
import org.springframework.stereotype.Component
|
||||
import org.springframework.web.servlet.HandlerExceptionResolver
|
||||
import javax.servlet.http.HttpServletRequest
|
||||
import javax.servlet.http.HttpServletResponse
|
||||
|
||||
@Component
|
||||
class CustomAccessDeniedHandler(
|
||||
private val handlerExceptionResolver: HandlerExceptionResolver
|
||||
) : AccessDeniedHandler {
|
||||
companion object : KLogging()
|
||||
|
||||
override fun handle(
|
||||
request: HttpServletRequest,
|
||||
response: HttpServletResponse,
|
||||
accessDeniedException: AccessDeniedException
|
||||
) {
|
||||
logger.info { "Access Denied!!!!!" }
|
||||
handlerExceptionResolver.resolveException(request, response, null, accessDeniedException)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package io.beaniejoy.dongnecafe.security.handler
|
||||
|
||||
import mu.KLogging
|
||||
import org.springframework.security.core.AuthenticationException
|
||||
import org.springframework.security.web.AuthenticationEntryPoint
|
||||
import org.springframework.stereotype.Component
|
||||
import org.springframework.web.servlet.HandlerExceptionResolver
|
||||
import javax.servlet.http.HttpServletRequest
|
||||
import javax.servlet.http.HttpServletResponse
|
||||
|
||||
@Component
|
||||
class CustomAuthenticationEntryPoint(
|
||||
private val handlerExceptionResolver: HandlerExceptionResolver
|
||||
): AuthenticationEntryPoint {
|
||||
companion object: KLogging()
|
||||
|
||||
override fun commence(
|
||||
request: HttpServletRequest,
|
||||
response: HttpServletResponse,
|
||||
authException: AuthenticationException
|
||||
) {
|
||||
logger.info { "Unauthorized Error!!" }
|
||||
handlerExceptionResolver.resolveException(request, response, null, authException)
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import io.beaniejoy.dongnecafe.common.error.exception.BusinessException
|
||||
import io.beaniejoy.dongnecafe.common.response.ApplicationResponse
|
||||
import mu.KLogging
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.security.core.AuthenticationException
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler
|
||||
import org.springframework.web.bind.annotation.ResponseStatus
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice
|
||||
@@ -21,12 +22,13 @@ class BasicControllerAdvice {
|
||||
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
@ExceptionHandler(Exception::class)
|
||||
fun handleException(e: Exception): ApplicationResponse<Nothing> {
|
||||
logger.error { "[COMMON][${e.javaClass.simpleName}] $e" }
|
||||
logger.error { "[COMMON][${e::class.simpleName}] $e" }
|
||||
return ApplicationResponse.fail(errorCode = ErrorCode.COMMON_SERVER_ERROR).build()
|
||||
}
|
||||
|
||||
/**
|
||||
* 비즈니스 로직 상 에러 처리(예상 가능한 예외 처리)
|
||||
* 비즈니스 로직 상 에러 처리(예상 가능한 예외 처리 - 200 ok 처리)
|
||||
* - TODO: 우선 인증, 인가 관련 에러도 여기에 포함 - 분리할지 고민
|
||||
* @param e BusinessException
|
||||
*/
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
@@ -35,4 +37,22 @@ class BasicControllerAdvice {
|
||||
logger.error { "[${BusinessException::class.simpleName}] <ErrorCode>: ${e.errorCode.name}, <ErrorMessage>: ${e.message}" }
|
||||
return ApplicationResponse.fail(errorCode = e.errorCode).build()
|
||||
}
|
||||
|
||||
/**
|
||||
* 인증, 인가 관련 에러 처리
|
||||
* - TODO: 이부분을 따로 구성해야할 필요성 아직 모르겠음
|
||||
* - 현재로써는 BusinessException handler 내용과 똑같다고 볼 수 있지만 성격상 분리
|
||||
*/
|
||||
@ResponseStatus(HttpStatus.OK)
|
||||
@ExceptionHandler(*arrayOf(AuthenticationException::class, AccessDeniedException::class))
|
||||
fun handleAuthException(e: Exception): ApplicationResponse<Nothing> {
|
||||
val errorCode = when (e) {
|
||||
is AuthenticationException -> ErrorCode.AUTH_UNAUTHORIZED
|
||||
is AccessDeniedException -> ErrorCode.AUTH_ACCESS_DENIED
|
||||
else -> ErrorCode.DEFAULT
|
||||
}
|
||||
|
||||
logger.error { "[${e::class.simpleName}] <ErrorCode>: ${errorCode.name}, <ErrorMessage>: ${e.message}" }
|
||||
return ApplicationResponse.fail(errorCode = errorCode).build()
|
||||
}
|
||||
}
|
||||
@@ -5,15 +5,18 @@ import io.beaniejoy.dongnecafe.common.error.constant.SubCategory.*
|
||||
|
||||
enum class ErrorCode(
|
||||
val domain: Domain,
|
||||
val subCategory: SubCategory
|
||||
val subCategory: SubCategory?
|
||||
) {
|
||||
// COMMON
|
||||
COMMON_SERVER_ERROR(COMMON, SERVER_ERROR),
|
||||
DEFAULT(COMMON, null),
|
||||
|
||||
// AUTH(security 관련)
|
||||
AUTH_MEMBER_NOT_FOUND(AUTH, INVALID_AUTHENTICATE_REQUEST),
|
||||
AUTH_PASSWORD_NOT_VALID(AUTH, INVALID_AUTHENTICATE_REQUEST),
|
||||
AUTH_MEMBER_DEACTIVATED(AUTH, DEACTIVATED),
|
||||
AUTH_ACCESS_DENIED(AUTH, ACCESS_DENIED),
|
||||
AUTH_UNAUTHORIZED(AUTH, UNAUTHORIZED),
|
||||
|
||||
// MEMBER
|
||||
MEMBER_EXISTED(MEMBER, EXISTED),
|
||||
@@ -29,7 +32,6 @@ enum class ErrorCode(
|
||||
MENU_OPTION_NOT_FOUND(MENU_OPTION, NOT_FOUND),
|
||||
|
||||
// OPTION_DETAIL
|
||||
OPTION_DETAIL_NOT_FOUND(OPTION_DETAIL, NOT_FOUND)
|
||||
|
||||
OPTION_DETAIL_NOT_FOUND(OPTION_DETAIL, NOT_FOUND),
|
||||
;
|
||||
}
|
||||
@@ -5,5 +5,8 @@ enum class SubCategory {
|
||||
INVALID_AUTHENTICATE_REQUEST,
|
||||
NOT_FOUND,
|
||||
EXISTED,
|
||||
DEACTIVATED
|
||||
DEACTIVATED,
|
||||
|
||||
ACCESS_DENIED,
|
||||
UNAUTHORIZED
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package io.beaniejoy.dongnecafe.security.constant
|
||||
|
||||
object SecurityConstant {
|
||||
const val BEARER = "Bearer"
|
||||
const val WHITESPACE = " "
|
||||
}
|
||||
@@ -26,7 +26,7 @@ class SecurityConfig {
|
||||
|
||||
// FIXME 임시 permitAll 설정
|
||||
.authorizeRequests()
|
||||
.anyRequest().permitAll()
|
||||
.anyRequest().authenticated()
|
||||
|
||||
.and()
|
||||
.sessionManagement()
|
||||
|
||||
@@ -8,12 +8,12 @@ import org.springframework.security.web.authentication.UsernamePasswordAuthentic
|
||||
|
||||
class JwtAuthenticationConfigurer :
|
||||
SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity>() {
|
||||
private var jwtTokenUtils: JwtTokenUtils? = null
|
||||
private lateinit var jwtTokenUtils: JwtTokenUtils
|
||||
|
||||
override fun configure(http: HttpSecurity) {
|
||||
http
|
||||
.addFilterBefore(
|
||||
JwtAuthenticationFilter(this.jwtTokenUtils!!),
|
||||
JwtAuthenticationFilter(this.jwtTokenUtils),
|
||||
UsernamePasswordAuthenticationFilter::class.java
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
package io.beaniejoy.dongnecafe.security.filter
|
||||
|
||||
import io.beaniejoy.dongnecafe.security.JwtTokenUtils
|
||||
import io.beaniejoy.dongnecafe.security.constant.SecurityConstant.BEARER
|
||||
import io.beaniejoy.dongnecafe.security.constant.SecurityConstant.WHITESPACE
|
||||
import mu.KotlinLogging
|
||||
import org.springframework.http.HttpHeaders
|
||||
import org.springframework.security.core.context.SecurityContextHolder
|
||||
import org.springframework.web.filter.GenericFilterBean
|
||||
import javax.servlet.FilterChain
|
||||
@@ -14,12 +17,6 @@ class JwtAuthenticationFilter(
|
||||
) : 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 인가 처리
|
||||
*/
|
||||
@@ -38,7 +35,7 @@ class JwtAuthenticationFilter(
|
||||
}
|
||||
|
||||
private fun getAccessToken(request: HttpServletRequest): String? {
|
||||
val bearer = request.getHeader(AUTHORIZATION)
|
||||
val bearer = request.getHeader(HttpHeaders.AUTHORIZATION)
|
||||
?: return null
|
||||
|
||||
val splitBearer = bearer.split(WHITESPACE)
|
||||
|
||||
Reference in New Issue
Block a user