[#17] feat: 인증 프로세스 내 여러 부가 내용 적용
- api 전용 success/failure handler 적용 - custom filter 적용을 위한 security config 내용 추가 설정
This commit is contained in:
@@ -1,24 +1,35 @@
|
||||
package io.beaniejoy.dongnecafe.common.config
|
||||
|
||||
import io.beaniejoy.dongnecafe.common.security.ApiAuthenticationFilter
|
||||
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.authentication.configuration.AuthenticationConfiguration
|
||||
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
|
||||
import org.springframework.security.core.userdetails.UserDetailsService
|
||||
import org.springframework.security.crypto.factory.PasswordEncoderFactories
|
||||
import org.springframework.security.crypto.password.PasswordEncoder
|
||||
import org.springframework.security.web.SecurityFilterChain
|
||||
import org.springframework.security.web.authentication.AuthenticationFailureHandler
|
||||
import org.springframework.security.web.authentication.AuthenticationSuccessHandler
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
class SecurityConfig {
|
||||
@Bean
|
||||
fun passwordEncoder(): PasswordEncoder {
|
||||
return PasswordEncoderFactories.createDelegatingPasswordEncoder()
|
||||
}
|
||||
@Autowired
|
||||
lateinit var authenticationConfiguration: AuthenticationConfiguration
|
||||
|
||||
@Autowired
|
||||
lateinit var apiAuthenticationSuccessHandler: AuthenticationSuccessHandler
|
||||
|
||||
@Autowired
|
||||
lateinit var apiAuthenticationFailureHandler: AuthenticationFailureHandler
|
||||
|
||||
@Bean
|
||||
fun filterChain(http: HttpSecurity): SecurityFilterChain {
|
||||
@@ -28,6 +39,7 @@ class SecurityConfig {
|
||||
|
||||
.authorizeRequests()
|
||||
.antMatchers("/auth/members/sign-up").permitAll()
|
||||
.antMatchers("/test").hasRole("USER") // 임시 인가 테스트용
|
||||
.anyRequest().authenticated()
|
||||
.and()
|
||||
.addFilterBefore(apiAuthenticationFilter(), UsernamePasswordAuthenticationFilter::class.java)
|
||||
@@ -35,10 +47,27 @@ class SecurityConfig {
|
||||
.build()
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun webSecurityCustomizer(): WebSecurityCustomizer {
|
||||
return WebSecurityCustomizer { web ->
|
||||
web.ignoring().requestMatchers(PathRequest.toStaticResources().atCommonLocations())
|
||||
web.ignoring().antMatchers("/error")
|
||||
}
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun passwordEncoder(): PasswordEncoder {
|
||||
return PasswordEncoderFactories.createDelegatingPasswordEncoder()
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun apiAuthenticationFilter(): ApiAuthenticationFilter {
|
||||
return ApiAuthenticationFilter(
|
||||
AntPathRequestMatcher("/auth/authenticate", HttpMethod.POST.name)
|
||||
)
|
||||
).apply {
|
||||
this.setAuthenticationManager(authenticationConfiguration.authenticationManager)
|
||||
this.setAuthenticationSuccessHandler(apiAuthenticationSuccessHandler)
|
||||
this.setAuthenticationFailureHandler(apiAuthenticationFailureHandler)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ class ApiAuthenticationFilter(requestMatcher: AntPathRequestMatcher) :
|
||||
request: HttpServletRequest,
|
||||
response: HttpServletResponse,
|
||||
): Authentication {
|
||||
log.info { "[API Filter] attempt to authenticate" }
|
||||
if (isPostMethod(request).not()) {
|
||||
val errorMsg = "Authentication is not supported (only support for POST method)"
|
||||
log.error { errorMsg }
|
||||
@@ -30,6 +31,7 @@ class ApiAuthenticationFilter(requestMatcher: AntPathRequestMatcher) :
|
||||
}
|
||||
|
||||
val signInRequest = objectMapper.readValue(request.reader, SignInRequest::class.java)
|
||||
request.setAttribute("email", signInRequest.email)
|
||||
|
||||
val token = signInRequest.let {
|
||||
if (StringUtils.hasText(it.email).not() || StringUtils.hasText(it.password).not()) {
|
||||
@@ -40,7 +42,9 @@ class ApiAuthenticationFilter(requestMatcher: AntPathRequestMatcher) :
|
||||
UsernamePasswordAuthenticationToken(it.email, it.password)
|
||||
}
|
||||
|
||||
return authenticationManager.authenticate(token)
|
||||
val authenticate = authenticationManager.authenticate(token)
|
||||
logger.info("attempt authentication ${authenticate.principal}")
|
||||
return authenticate
|
||||
}
|
||||
|
||||
private fun isPostMethod(request: HttpServletRequest): Boolean {
|
||||
|
||||
@@ -31,8 +31,10 @@ class ApiAuthenticationProvider(
|
||||
throw BadCredentialsException("Input password does not match stored password")
|
||||
}
|
||||
|
||||
logger.info { "User password ${user.password}" }
|
||||
|
||||
// password null로 반환
|
||||
return UsernamePasswordAuthenticationToken(email, null, user.authorities)
|
||||
return UsernamePasswordAuthenticationToken(user, null, user.authorities)
|
||||
}
|
||||
|
||||
override fun supports(authentication: Class<*>): Boolean {
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
package io.beaniejoy.dongnecafe.common.security.handler
|
||||
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import io.beaniejoy.dongnecafe.common.security.model.AuthenticationResult
|
||||
import io.beaniejoy.dongnecafe.domain.member.model.request.SignInRequest
|
||||
import mu.KLogging
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.security.core.AuthenticationException
|
||||
import org.springframework.security.web.authentication.AuthenticationFailureHandler
|
||||
import org.springframework.stereotype.Component
|
||||
import javax.servlet.http.HttpServletRequest
|
||||
import javax.servlet.http.HttpServletResponse
|
||||
|
||||
@Component
|
||||
class ApiAuthenticationFailureHandler : AuthenticationFailureHandler {
|
||||
private val objectMapper = jacksonObjectMapper()
|
||||
|
||||
companion object : KLogging()
|
||||
|
||||
override fun onAuthenticationFailure(
|
||||
request: HttpServletRequest,
|
||||
response: HttpServletResponse,
|
||||
exception: AuthenticationException,
|
||||
) {
|
||||
val email = request.getAttribute("email") as String
|
||||
logger.error { "[AUTH FAILED] $email" }
|
||||
|
||||
response.apply {
|
||||
this.status = HttpStatus.UNAUTHORIZED.value()
|
||||
this.contentType = MediaType.APPLICATION_JSON_VALUE
|
||||
}
|
||||
|
||||
objectMapper.writeValue(
|
||||
response.writer,
|
||||
AuthenticationResult(
|
||||
email = email,
|
||||
msg = "authentication failed"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package io.beaniejoy.dongnecafe.common.security.handler
|
||||
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import io.beaniejoy.dongnecafe.common.security.model.AuthenticationResult
|
||||
import mu.KLogging
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.security.core.Authentication
|
||||
import org.springframework.security.core.userdetails.User
|
||||
import org.springframework.security.web.authentication.AuthenticationSuccessHandler
|
||||
import org.springframework.stereotype.Component
|
||||
import javax.servlet.http.HttpServletRequest
|
||||
import javax.servlet.http.HttpServletResponse
|
||||
|
||||
@Component
|
||||
class ApiAuthenticationSuccessHandler :AuthenticationSuccessHandler {
|
||||
private val objectMapper = jacksonObjectMapper()
|
||||
|
||||
companion object: KLogging()
|
||||
|
||||
override fun onAuthenticationSuccess(
|
||||
request: HttpServletRequest,
|
||||
response: HttpServletResponse,
|
||||
authentication: Authentication,
|
||||
) {
|
||||
val user = authentication.principal as User
|
||||
logger.info { "[AUTH SUCCESS] email: ${user.username}, authorities: ${user.authorities}" }
|
||||
|
||||
response.apply {
|
||||
this.status = HttpStatus.OK.value()
|
||||
this.contentType = MediaType.APPLICATION_JSON_VALUE
|
||||
}
|
||||
|
||||
objectMapper.writeValue(
|
||||
response.writer,
|
||||
AuthenticationResult(
|
||||
email = user.username,
|
||||
authorities = user.authorities,
|
||||
msg = "authentication success"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package io.beaniejoy.dongnecafe.common.security.model
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude
|
||||
import org.springframework.security.core.GrantedAuthority
|
||||
|
||||
data class AuthenticationResult(
|
||||
val email: String,
|
||||
@JsonInclude(JsonInclude.Include.NON_EMPTY)
|
||||
val authorities: Collection<GrantedAuthority> = listOf(),
|
||||
val msg: String
|
||||
)
|
||||
@@ -0,0 +1,13 @@
|
||||
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,34 @@
|
||||
package io.beaniejoy.dongnecafe.common.config
|
||||
|
||||
import org.springframework.boot.autoconfigure.security.servlet.PathRequest
|
||||
import org.springframework.context.annotation.Bean
|
||||
import org.springframework.context.annotation.Configuration
|
||||
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
|
||||
import org.springframework.security.web.SecurityFilterChain
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
class SecurityConfig {
|
||||
@Bean
|
||||
fun filterChain(http: HttpSecurity): SecurityFilterChain {
|
||||
return http
|
||||
.csrf().disable()
|
||||
.formLogin().disable()
|
||||
|
||||
.authorizeRequests()
|
||||
.anyRequest().authenticated() // 임시 허용
|
||||
|
||||
.and()
|
||||
.build()
|
||||
}
|
||||
|
||||
@Bean
|
||||
fun webSecurityCustomizer(): WebSecurityCustomizer {
|
||||
return WebSecurityCustomizer { web ->
|
||||
web.ignoring().requestMatchers(PathRequest.toStaticResources().atCommonLocations())
|
||||
web.ignoring().antMatchers("/error")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -13,7 +13,7 @@ spring:
|
||||
show-sql: false
|
||||
flyway:
|
||||
baseline-on-migrate: true
|
||||
baseline-version: 0
|
||||
baseline-version: "000"
|
||||
locations: classpath:db/migration,classpath:db/seed
|
||||
devtools:
|
||||
livereload:
|
||||
|
||||
Reference in New Issue
Block a user