Spring Security에서 formLogin()을 사용하면 Content-Type이 x-www-form-urlencoded인 방식으로만 데이터를 받을 수 있다.
이런 경우 Rest Api 통신에 사용하는 Json으로 보내는 데이터는 처리되지 않는다.
Json으로 데이터를 받아서 로그인 처리를 하기 위해서는 AbstractAuthenticationProcessingFilter나 UsernamePasswordAuthenticationFilter를 상속 받아 처리할 수 있다.
다음은 AbstractAuthenticationProcessingFilter를 상속 받아 로그인을 구현한 코드이다.
class JsonUsernamePasswordAuthenticationFilter(
private val objectMapper: ObjectMapper
) : AbstractAuthenticationProcessingFilter(DEFAULT_LOGIN_PATH_REQUEST_MATCHER) {
override fun attemptAuthentication(request: HttpServletRequest, response: HttpServletResponse): Authentication {
if (request.contentType == null || request.contentType != CONTENT_TYPE) { //
throw AuthenticationServiceException("Authentication Content-Type not supported: " + request.contentType)
}
val loginDto: LoginDto = objectMapper.readValue( // HTTP 요청 본문을 dto로 변환
StreamUtils.copyToString(request.inputStream, StandardCharsets.UTF_8),
LoginDto::class.java
)
val username: String = loginDto.email
val password: String = loginDto.password
val authRequest = UsernamePasswordAuthenticationToken(username, password)
setDetails(request, authRequest)
return authenticationManager.authenticate(authRequest) // 검증
}
private fun setDetails(request: HttpServletRequest?, authRequest: UsernamePasswordAuthenticationToken) {
authRequest.details = authenticationDetailsSource.buildDetails(request)
}
private data class LoginDto(
var email: String,
var password: String
) {
constructor() : this("", "") // 기본 생성자가 없는 경우, InvalidDefinitionException: Cannot construct instance
}
companion object {
private const val DEFAULT_LOGIN_REQUEST_URL = "/signin"
private const val HTTP_METHOD = "POST"
private const val CONTENT_TYPE = "application/json"
private val DEFAULT_LOGIN_PATH_REQUEST_MATCHER = AntPathRequestMatcher(DEFAULT_LOGIN_REQUEST_URL, HTTP_METHOD)
}
}
SecurityConfig는 다음과 같이 설정한다.
@Configuration
@EnableWebSecurity
class SecurityConfig(
private val objectMapper: ObjectMapper,
private val userDetailsService: UserDetailsService
) {
@Bean
fun bCryptPasswordEncoder(): BCryptPasswordEncoder {
return BCryptPasswordEncoder()
}
@Bean
fun configure(http: HttpSecurity): SecurityFilterChain {
http.csrf { it.disable() }
return http
.addFilterBefore(
jsonUserNamePasswordAuthenticationFiler(),
UsernamePasswordAuthenticationFilter::class.java
)
.authorizeHttpRequests {
it.requestMatchers(HttpMethod.GET).permitAll()
.requestMatchers("/signup", "/signin").permitAll()
.anyRequest().authenticated()
}
.formLogin { it.disable() }
.logout { it.invalidateHttpSession(true) }
.build()
}
@Bean
fun authenticationManager(): AuthenticationManager {
val authProvider = DaoAuthenticationProvider()
authProvider.setUserDetailsService(userDetailsService)
authProvider.setPasswordEncoder(bCryptPasswordEncoder())
return ProviderManager(authProvider)
}
@Bean
fun jsonUserNamePasswordAuthenticationFiler(): JsonUsernamePasswordAuthenticationFilter {
val jsonUserNamePasswordAuthenticationFiler = JsonUsernamePasswordAuthenticationFilter(objectMapper)
jsonUserNamePasswordAuthenticationFiler.setAuthenticationManager(authenticationManager())
return jsonUserNamePasswordAuthenticationFiler
}
}
이외 다른 부분은 formLogin 구현과 동일하다.
다만 formLogin으로 구현했을 때는 SecurityContextHolder.getContext().authentication.principal로 사용자의 정보를 가져올 수 있었지만, Json 데이터로 요청 받는 방식으로 변경 후 사용자 인증이 완료 된 후 SecurityContextHolderStrategy.clearContext()가 수행되어 현재 스레드의 SecurityContext에 저장된 사용자의 정보가 지워진다.
그러므로 사용자의 정보를 유지하기 위해서는 세션 저장소에 사용자의 정보를 저장하거나 토큰을 발급해 인증하는 방식을 사용해야 한다.
참고자료
'TIL(Today I Learned)' 카테고리의 다른 글
TIL - JWT Token 생성 및 검증(Access Token, Refresh Token) (0) | 2024.05.21 |
---|---|
TIL - Java 8 date/time type `java.time.LocalDateTime` not supported (0) | 2024.05.20 |
TIL - Offset-based Pagination & Cursor-based Pagination (0) | 2024.05.16 |
TIL - SQL WITH 재귀 쿼리 (0) | 2024.05.14 |
TIL - 댓글을 가져올 때 순환 참조 발생 (0) | 2024.05.13 |