Spring Webflux Websocket 安全 - 基本身份验证
Spring Webflux Websocket Security - Basic Authentication
问题:我没有得到 Spring Websockets 的安全性以在 Webflux 项目中工作。
注意:我使用的是 Kotlin 而不是 Java。
依赖项:
Spring 启动 2.0.0
Spring 安全 5.0.3
Spring WebFlux 5.0.4
重要更新: 我提出了一个 Spring 问题错误(3 月 30 日)here and one of the Spring security maintainers said its NOT SUPPORTED but they can add it for Spring Security 5.1.0 M2。
LINK: Add WebFlux WebSocket Support #5188
Webflux 安全配置
@EnableWebFluxSecurity
class SecurityConfig
{
@Bean
fun configure(http: ServerHttpSecurity): SecurityWebFilterChain
{
return http.authorizeExchange()
.pathMatchers("/").permitAll()
.anyExchange().authenticated()
.and().httpBasic()
.and().formLogin().disable().csrf().disable()
.build()
}
@Bean
fun userDetailsService(): MapReactiveUserDetailsService
{
val user = User.withDefaultPasswordEncoder()
.username("user")
.password("pass")
.roles("USER")
.build()
return MapReactiveUserDetailsService(user)
}
}
Webflux Websocket 配置
@Configuration
class ReactiveWebSocketConfiguration
{
@Bean
fun webSocketMapping(handler: WebSocketHandler): HandlerMapping
{
val map = mapOf(Pair("/event", handler))
val mapping = SimpleUrlHandlerMapping()
mapping.order = -1
mapping.urlMap = map
return mapping
}
@Bean
fun handlerAdapter() = WebSocketHandlerAdapter()
@Bean
fun websocketHandler() = WebSocketHandler { session ->
// Should print authenticated principal BUT does show NULL
println("${session.handshakeInfo.principal.block()}")
// Just for testing we send hello world to the client
session.send(Mono.just(session.textMessage("hello world")))
}
}
客户代码
// Lets create a websocket and pass Basic Auth to it
new WebSocket("ws://user:pass@localhost:8000/event");
// ...
观察结果
在 websocket 处理程序中,主体显示 null
客户端无需验证即可连接。如果我在没有 Basic Auth 的情况下执行 WebSocket("ws://localhost:8000/event")
它仍然有效!因此 Spring 安全性不验证任何内容。
我错过了什么?
我做错了什么?
我建议您实施 own authentication mechanism
而不是利用 Spring 安全性。
当 WebSocket
连接即将建立时,它使用 handshake
机制伴随 UPGRADE
请求。基于此,我们的想法是使用我们自己的处理程序来处理请求并在那里执行身份验证。
幸运的是,Spring 引导有 RequestUpgradeStrategy
for such purpose. On top of that, based on the application server what you use, Spring provides a default implementation of those strategies. As I use Netty
bellow the class would be ReactorNettyRequestUpgradeStrategy。
这是建议的原型:
/**
* Based on {@link ReactorNettyRequestUpgradeStrategy}
*/
@Slf4j
@Component
public class BasicAuthRequestUpgradeStrategy implements RequestUpgradeStrategy {
private int maxFramePayloadLength = NettyWebSocketSessionSupport.DEFAULT_FRAME_MAX_SIZE;
private final AuthenticationService service;
public BasicAuthRequestUpgradeStrategy(AuthenticationService service) {
this.service = service;
}
@Override
public Mono<Void> upgrade(ServerWebExchange exchange, //
WebSocketHandler handler, //
@Nullable String subProtocol, //
Supplier<HandshakeInfo> handshakeInfoFactory) {
ServerHttpResponse response = exchange.getResponse();
HttpServerResponse reactorResponse = getNativeResponse(response);
HandshakeInfo handshakeInfo = handshakeInfoFactory.get();
NettyDataBufferFactory bufferFactory = (NettyDataBufferFactory) response.bufferFactory();
String originHeader = handshakeInfo.getHeaders()
.getOrigin();// you will get ws://user:pass@localhost:8080
return service.authenticate(originHeader)//returns Mono<Boolean>
.filter(Boolean::booleanValue)// filter the result
.doOnNext(a -> log.info("AUTHORIZED"))
.flatMap(a -> reactorResponse.sendWebsocket(subProtocol, this.maxFramePayloadLength, (in, out) -> {
ReactorNettyWebSocketSession session = //
new ReactorNettyWebSocketSession(in, out, handshakeInfo, bufferFactory, this.maxFramePayloadLength);
return handler.handle(session);
}))
.switchIfEmpty(Mono.just("UNATHORIZED")
.doOnNext(log::info)
.then());
}
private static HttpServerResponse getNativeResponse(ServerHttpResponse response) {
if (response instanceof AbstractServerHttpResponse) {
return ((AbstractServerHttpResponse) response).getNativeResponse();
} else if (response instanceof ServerHttpResponseDecorator) {
return getNativeResponse(((ServerHttpResponseDecorator) response).getDelegate());
} else {
throw new IllegalArgumentException("Couldn't find native response in " + response.getClass()
.getName());
}
}
}
此外,如果你在项目中对Spring安全没有关键的逻辑依赖,比如复杂的ACL逻辑,那么我建议你摆脱它,甚至根本不使用它。
原因是我认为 Spring 安全是反应式方法的违反者,因为它,我想说,是 MVC 遗留的思维方式。它使您的应用程序与大量额外配置和“非表面”调整纠缠在一起,并迫使工程师维护这些配置,使它们变得越来越复杂。在大多数情况下,事情可以非常顺利地实施,而根本不会触及 Spring 安全性。只需创建一个组件并以适当的方式使用它。
希望对您有所帮助。
问题:我没有得到 Spring Websockets 的安全性以在 Webflux 项目中工作。
注意:我使用的是 Kotlin 而不是 Java。
依赖项:
Spring 启动 2.0.0
Spring 安全 5.0.3
Spring WebFlux 5.0.4
重要更新: 我提出了一个 Spring 问题错误(3 月 30 日)here and one of the Spring security maintainers said its NOT SUPPORTED but they can add it for Spring Security 5.1.0 M2。
LINK: Add WebFlux WebSocket Support #5188
Webflux 安全配置
@EnableWebFluxSecurity
class SecurityConfig
{
@Bean
fun configure(http: ServerHttpSecurity): SecurityWebFilterChain
{
return http.authorizeExchange()
.pathMatchers("/").permitAll()
.anyExchange().authenticated()
.and().httpBasic()
.and().formLogin().disable().csrf().disable()
.build()
}
@Bean
fun userDetailsService(): MapReactiveUserDetailsService
{
val user = User.withDefaultPasswordEncoder()
.username("user")
.password("pass")
.roles("USER")
.build()
return MapReactiveUserDetailsService(user)
}
}
Webflux Websocket 配置
@Configuration
class ReactiveWebSocketConfiguration
{
@Bean
fun webSocketMapping(handler: WebSocketHandler): HandlerMapping
{
val map = mapOf(Pair("/event", handler))
val mapping = SimpleUrlHandlerMapping()
mapping.order = -1
mapping.urlMap = map
return mapping
}
@Bean
fun handlerAdapter() = WebSocketHandlerAdapter()
@Bean
fun websocketHandler() = WebSocketHandler { session ->
// Should print authenticated principal BUT does show NULL
println("${session.handshakeInfo.principal.block()}")
// Just for testing we send hello world to the client
session.send(Mono.just(session.textMessage("hello world")))
}
}
客户代码
// Lets create a websocket and pass Basic Auth to it
new WebSocket("ws://user:pass@localhost:8000/event");
// ...
观察结果
在 websocket 处理程序中,主体显示 null
客户端无需验证即可连接。如果我在没有 Basic Auth 的情况下执行
WebSocket("ws://localhost:8000/event")
它仍然有效!因此 Spring 安全性不验证任何内容。
我错过了什么? 我做错了什么?
我建议您实施 own authentication mechanism
而不是利用 Spring 安全性。
当 WebSocket
连接即将建立时,它使用 handshake
机制伴随 UPGRADE
请求。基于此,我们的想法是使用我们自己的处理程序来处理请求并在那里执行身份验证。
幸运的是,Spring 引导有 RequestUpgradeStrategy
for such purpose. On top of that, based on the application server what you use, Spring provides a default implementation of those strategies. As I use Netty
bellow the class would be ReactorNettyRequestUpgradeStrategy。
这是建议的原型:
/**
* Based on {@link ReactorNettyRequestUpgradeStrategy}
*/
@Slf4j
@Component
public class BasicAuthRequestUpgradeStrategy implements RequestUpgradeStrategy {
private int maxFramePayloadLength = NettyWebSocketSessionSupport.DEFAULT_FRAME_MAX_SIZE;
private final AuthenticationService service;
public BasicAuthRequestUpgradeStrategy(AuthenticationService service) {
this.service = service;
}
@Override
public Mono<Void> upgrade(ServerWebExchange exchange, //
WebSocketHandler handler, //
@Nullable String subProtocol, //
Supplier<HandshakeInfo> handshakeInfoFactory) {
ServerHttpResponse response = exchange.getResponse();
HttpServerResponse reactorResponse = getNativeResponse(response);
HandshakeInfo handshakeInfo = handshakeInfoFactory.get();
NettyDataBufferFactory bufferFactory = (NettyDataBufferFactory) response.bufferFactory();
String originHeader = handshakeInfo.getHeaders()
.getOrigin();// you will get ws://user:pass@localhost:8080
return service.authenticate(originHeader)//returns Mono<Boolean>
.filter(Boolean::booleanValue)// filter the result
.doOnNext(a -> log.info("AUTHORIZED"))
.flatMap(a -> reactorResponse.sendWebsocket(subProtocol, this.maxFramePayloadLength, (in, out) -> {
ReactorNettyWebSocketSession session = //
new ReactorNettyWebSocketSession(in, out, handshakeInfo, bufferFactory, this.maxFramePayloadLength);
return handler.handle(session);
}))
.switchIfEmpty(Mono.just("UNATHORIZED")
.doOnNext(log::info)
.then());
}
private static HttpServerResponse getNativeResponse(ServerHttpResponse response) {
if (response instanceof AbstractServerHttpResponse) {
return ((AbstractServerHttpResponse) response).getNativeResponse();
} else if (response instanceof ServerHttpResponseDecorator) {
return getNativeResponse(((ServerHttpResponseDecorator) response).getDelegate());
} else {
throw new IllegalArgumentException("Couldn't find native response in " + response.getClass()
.getName());
}
}
}
此外,如果你在项目中对Spring安全没有关键的逻辑依赖,比如复杂的ACL逻辑,那么我建议你摆脱它,甚至根本不使用它。
原因是我认为 Spring 安全是反应式方法的违反者,因为它,我想说,是 MVC 遗留的思维方式。它使您的应用程序与大量额外配置和“非表面”调整纠缠在一起,并迫使工程师维护这些配置,使它们变得越来越复杂。在大多数情况下,事情可以非常顺利地实施,而根本不会触及 Spring 安全性。只需创建一个组件并以适当的方式使用它。
希望对您有所帮助。