[#17] feat: 인증 프로세스 내 여러 부가 내용 적용

- api 전용 success/failure handler 적용
- custom filter 적용을 위한 security config 내용 추가 설정
This commit is contained in:
beaniejoy
2022-10-31 01:10:35 +09:00
parent ae0d89d870
commit 78648ec47d
9 changed files with 186 additions and 8 deletions

View File

@@ -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)
}
}
}

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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"
)
)
}
}

View File

@@ -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"
)
)
}
}

View File

@@ -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
)

View File

@@ -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!"
}
}

View File

@@ -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")
}
}
}

View File

@@ -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: