
시작하기에 앞서 이 내용들은 제가 이해한 내용들을 정리한 것입니다.
틀릴 수있으며, 틀린 내용은 댓글 부탁드립니다.
필터 동작은 다음과 같이 진행되면 아래 구현 객체들을 구현해주어야한다.

@EnableWebSecurity
class SecurityConfig{
@Bean
fun filterChain(http: HttpSecurity): SecurityFilterChain {
http { // kotlin DSL
httpBasic { disable() }
csrf { disable() }
cors { }
authorizeRequests {
authorize("검증할 url",authenticated)
authorize("/**",permitAll)
}
oauth2Login {
loginPage = "/loginPage"
defaultSuccessUrl("/",true)
userInfoEndpoint {
userService = principalOauthUserService
}
authenticationSuccessHandler = oAuthSuccessHandler //OAuth 로그인 성공시 동작
}
addFilterBefore<UsernamePasswordAuthenticationFilter>(jwtAuthenticationFilter)
}
return http.build()
}
OAuth로 로그인한 경우 처리됨.
@Service
class PrincipalOauthUserService(
private val userService: UserService,
) : DefaultOAuth2UserService() {
@Throws(OAuth2AuthenticationException::class)
@Transactional(value = "transactionManager")
override fun loadUser(userRequest: OAuth2UserRequest): OAuth2User? {
val oAuth2User = super.loadUser(userRequest)
var oAuth2UserInfo: OAuth2UserInfo? = null
when (userRequest.clientRegistration.registrationId) {
// 인증 기관에 따른 처리 google, naver , kakao 등등
}
// oAuth2User.attribute에서 oAuth에서 받은 정보들을 확인할 수 있음
// 이 정보로 DB에 저장하거나 등등 마음대로 구현
return PrincipalDetails(userEntity, oAuth2User.attributes) // UserDetails와 OAuth2User 인터페이스를 상속받은 객체.
}
}
Form로그인(아이디 비밀번호)시 적용됨.
@Service
@RequiredArgsConstructor
class PrincipalService(
private val userService: UserService
) : UserDetailsService {
@Throws(UsernameNotFoundException::class)
override fun loadUserByUsername(username: String): UserDetails? {
val findUser: User = userService.findUserByName(username)
if (findUser != null) {
return PrincipalDetails(findUser) // UserDetails와 OAuth2User 인터페이스를 상속받은 객체.
}
return null
}
}
로그인 성공시 동작시킬 함수 구현
@Component
class OAuthSuccessHandler(
) : AuthenticationSuccessHandler {
override fun onAuthenticationSuccess(
request: HttpServletRequest,
response: HttpServletResponse,
authentication: Authentication
) {
val principal = authentication.principal as PrincipalDetails // 이 객체를 통해 oAuth나 form 둘다 가능
val email = principal.getEmail() // 이메일로 검증하는 예시
if(email == null){
response.sendRedirect("에러")
}
else{
redisService.saveJwt(jwt.token,user)
response.status = HttpServletResponse.SC_OK
response.contentType = "application/json;charset=UTF-8"
response.sendRedirect("로그인 성공했으니 여기로 리다이렉트")
}
}
}
class PrincipalDetails : UserDetails, OAuth2User{
private var user : User
private lateinit var attributes :MutableMap<String,Any>
public constructor(user: User) {
this.user = user
}
constructor(user: User, attributes: Map<String, Any>){
this.user = user
this.attributes = attributes as MutableMap<String, Any>
}
// 두 인터페이스의 모든 함수를 오버라이드 해야함.
}
아래는 jwt 검증하려고 만든 필터 예시
class JwtAuthenticationFilter(
) : GenericFilterBean() {
override fun doFilter(request: ServletRequest?, response: ServletResponse?, chain: FilterChain?) {
val token = resolveToken(request as HttpServletRequest)// 토큰 유효성 검사후 통과 => token반환, 불통과 => null 반환
if (token != null && jwtTokenProvider.validateToken(token)) {
val authentication = jwtTokenProvider.getAuthentication(token)
SecurityContextHolder.getContext().authentication = authentication //세션에 Authentication을 저장하므로써 인증된 유저임을 알릴 수있음.
println("doFilterChain:$authentication")
}
chain?.doFilter(request, response)
}
private fun resolveToken(request : HttpServletRequest) : String? {
val bearerToken = request.getHeader("Authorization")
// 토큰 유효성 검사후 통과 => token반환, 불통과 => null 반환
}
}
Webflux환경에서의 Security는
filterChain부터 모든게 다른 인터페이스로 작성되어있지만 동작은 비슷하다. 다만 어노테이션을 MVC환경에서 Webflux로 모두 바꿔주어야만 정상적으로 동작하니
이 점을 꼭 확인해야한다.

@EnableWebFluxSecurity
@EnableReactiveMethodSecurity
위 두 어노테이션을 통해서 Webflux환경 Security관련 bean들이 자동 주입되도록 하여야한다.
특히, filterChain에서 쓰이는 ServerHttpSecurity를 주입해준다.
또한, MVC환경과 다르게filterChain이SecurityWebFilterChain를 반환해야한다.
😅 그리고 가장중요하게ServerOAuth2AuthorizationRequestResolver를 구현해주어서 넣어주지 않으면clientRegistrationRepository가 없다는 오류가 발생한다..
(MVC에서는 자동으로 해줬는데... 아직 안되는 것같다는 내생각..)
@Configuration
@EnableReactiveMethodSecurity
@EnableWebFluxSecurity
class SecurityConfig(
val principalOauthUserService: PrincipalOauthUserServiceReactive,
val oAuthSuccessHandler: OAuthSuccessHandlerReactive,
val jwtAuthenticationFilter: JwtAuthenticationFilterReactive,
val clientRegistrationRepository: ReactiveClientRegistrationRepository
) {
@Bean
fun filterChain(http: ServerHttpSecurity): SecurityWebFilterChain {
http.httpBasic {
it.disable()
}
.csrf {
it.disable()
}
.authorizeExchange{
it.pathMatchers("검증할 url").authenticated()
it.pathMatchers("/**").permitAll()
}
.exceptionHandling{
it.authenticationEntryPoint(RedirectServerAuthenticationEntryPoint("/loginPage")) // 로그인 안되어 있을 때 보낼 곳 설정
}
.oauth2Login {
it.authenticationSuccessHandler(oAuthSuccessHandler)
it.authorizationRequestResolver(authorizationRequestResolver())
}
.addFilterBefore(jwtAuthenticationFilter, SecurityWebFiltersOrder.AUTHENTICATION)
return http.build()
}
private fun authorizationRequestResolver(): ServerOAuth2AuthorizationRequestResolver {
val authorizationRequestMatcher: ServerWebExchangeMatcher = PathPatternParserServerWebExchangeMatcher(
"/oauth2/authorization/{registrationId}"
)
return DefaultServerOAuth2AuthorizationRequestResolver(
clientRegistrationRepository, authorizationRequestMatcher
)
}
}
@Service
class PrincipalOauthUserServiceReactive(
private val userService: UserR2DBCService,
) : ReactiveOAuth2UserService<OAuth2UserRequest,OAuth2User> {
@Throws(OAuth2AuthenticationException::class)
@Transactional(value = "transactionManager")
override fun loadUser(userRequest: OAuth2UserRequest): Mono<OAuth2User> {
val delegate = DefaultReactiveOAuth2UserService()
//userRequest.clientRegistration.registrationId로 google, naver,kakao 등 정보
return delegate.loadUser(userRequest)
.map{OAuthUser->
//기타 사용자 저장같은 동작 구현
oAuth2UserInfo => USER로 매핑시켜야됨
PrincipalDetailse(user,attribute)
}
}
@Service
@RequiredArgsConstructor
class PrincipalServiceReactive: ReactiveUserDetailsService {
@Throws(UsernameNotFoundException::class)
override fun findByUsername(username: String): Mono<UserDetails> {
return userService.findByUserId(username)
.map{
return@map PrincipalDetailsReactive(it)
}
}
}
@Component(value = "authenticationSuccessHandler")
class OAuthSuccessHandlerReactive: ServerAuthenticationSuccessHandler {
override fun onAuthenticationSuccess(
webFilterExchange: WebFilterExchange,
authentication: Authentication
): Mono<Void> {
val principal = authentication.principal as PrincipalDetailsReactive
val userName = principal.name
val jwt = jwtTokenProvider.createToken(userName)
return userService.findByUserId(userName)
.doOnNext {user ->
//로그인 성공했을 때 할 동작 구현
}
.flatMap {
//토큰과 함께 리다이렉션
val uri = UriComponentsBuilder.newInstance().path("/").queryParam("token",jwt.token).build().toUri()
val exchange = webFilterExchange.exchange
DefaultServerRedirectStrategy().sendRedirect(exchange,uri)
}
}
}
아래는 jwt 검증하려고 만든 필터 예시
@Component
class JwtAuthenticationFilterReactive(
private val jwtTokenProvider: JwtTokenProvider
) : WebFilter {
override fun filter(exchange: ServerWebExchange, chain: WebFilterChain): Mono<Void> {
val token = resolveToken(exchange.request)// 토큰 유효성 검사후 통과 => token반환, 불통과 => null 반환
if (token != null && jwtTokenProvider.validateToken(token)) {
val authentication = jwtTokenProvider.getAuthentication(token)
SecurityContextHolder.getContext().authentication = authentication
}
return chain.filter(exchange)
}
private fun resolveToken(request : ServerHttpRequest) : String? {
val bearerToken = request.headers["Authorization"]?.get(0)
// 토큰 유효성 검사후 통과 => token반환, 불통과 => null 반환
}
}
https://docs.spring.io/spring-security/reference/reactive/configuration/webflux.html
https://velog.io/@dlawnsdh/Webflux%EC%97%90-OAuth2-JWT-%EC%A0%81%EC%9A%A9%ED%95%98%EA%B8%B0
https://velog.io/@rnqhstlr2297/Spring-Security-OAuth2-%EC%86%8C%EC%85%9C%EB%A1%9C%EA%B7%B8%EC%9D%B8