TIL(Today I Learned)

TIL - Spring Security에서 JSON으로 로그인 처리

Happy._. 2024. 5. 17. 22:00

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에 저장된 사용자의 정보가 지워진다.

그러므로 사용자의 정보를 유지하기 위해서는 세션 저장소에 사용자의 정보를 저장하거나 토큰을 발급해 인증하는 방식을 사용해야 한다.

 

 

참고자료

https://ttl-blog.tistory.com/104#%F0%9F%A7%90%20%EC%96%B4%EB%96%BB%EA%B2%8C%20JSON%EC%9C%BC%EB%A1%9C%20%EB%A1%9C%EA%B7%B8%EC%9D%B8%20%ED%95%98%EC%A7%80%3F-1