How @PreAuthorize is working in an Reactive Application or how to live without ThreadLocal?
You're right, ThreadLocal
is no longer an option because the processing of a request is not tied to a particular thread.
Currently, Spring Security is storing the authentication information as a ServerWebExchange
attribute, so tied to the current request/response pair. But you still need that information when you don't have direct access to the current exchange, like @PreAuthorize
.
The authentication information is stored in the Reactive pipeline itself (so accessible from your Mono
or Flux
), which is a very interesting Reactor feature - managing a context tied to a particular Subscriber
(in a web application, the HTTP client is pulling data from the server and acts as such).
I'm not aware of an equivalent of SecurityContextHolder
, or some shortcut method to get the Authentication information from the context.
See more about Reactor Context feature in the reference documentation. You can also see an example of that being used in Spring Security here.
The ReactiveSecurityContextHolder
provides the authentication in a reactive way, and is analogous to SecurityContextHolder
.
Its getContext()
method provides a Mono<SecurityContext>
, just like SecurityContextHolder.getContext()
provides a SecurityContext
.
ReactiveSecurityContextHolder
.getContext()
.map(context ->
context.getAuthentication()
I implemented a JwtAuthenticationConverter (kotlin):
@Component
class JwtAuthenticationConverter : Function<ServerWebExchange,
Mono<Authentication>> {
@Autowired
lateinit var jwtTokenUtil: JwtTokenUtil
@Autowired
lateinit var userDetailsService: ReactiveUserDetailsService
private val log = LogFactory.getLog(this::class.java)
override fun apply(exchange: ServerWebExchange): Mono<Authentication> {
val request = exchange.request
val token = getJwtFromRequest(request)
if ( token != null )
try {
return userDetailsService.findByUsername(jwtTokenUtil.getUsernameFromToken(token))
.map { UsernamePasswordAuthenticationToken(it, null, it.authorities) }
} catch ( e: Exception ) {
exchange.response.statusCode = HttpStatus.UNAUTHORIZED
exchange.response.headers["internal-message"] = e.message
log.error(e)
}
return Mono.empty()
}
private fun getJwtFromRequest(request: ServerHttpRequest): String? {
val bearerToken = request.headers[SecurityConstants.TOKEN_HEADER]?.first {
it.startsWith(SecurityConstants.TOKEN_PREFIX, true)}
return if (bearerToken.isNullOrBlank()) null else bearerToken?.substring(7, bearerToken.length)
}
And then I set a SecurityConfig like this:
val authFilter = AuthenticationWebFilter(ReactiveAuthenticationManager {
authentication: Authentication -> Mono.just(authentication)
})
authFilter.setAuthenticationConverter(jwtAuthenticationConverter)
http.addFilterAt( authFilter, SecurityWebFiltersOrder.AUTHENTICATION)
You can use this approach to customize your AuthenticationConverter as I did to jwt based authentication to set the desired authentication object.