Spring中的Websocket身份validation和授权
我一直在努力用Spring-Security正确实现Stomp(websocket) 身份validation和授权 。 对于后代,我会回答我自己的问题,提供指导。
问题
Spring WebSocket文档(用于身份validation)看起来不清楚ATM(恕我直言)。 我无法理解如何正确处理身份validation和授权 。
我想要的是
- 使用登录名/密码validation用户。
- 防止匿名用户通过WebSocket进行连接。
- 添加授权层(用户,管理员,…)。
-
Principal
可以在控制器中使用。
我不想要的
- 在HTTP协商端点上进行身份validation(因为大多数JavaScript库不会与HTTP协商调用一起发送身份validation标头)。
如上所述,文档(ATM)尚不清楚,直到Spring提供了一些明确的文档,这里有一个样板,可以帮助您节省两天时间,试图了解安全链正在做什么。
Rob-Leggett做了一个非常好的尝试,但是他正在分配一些Springs课程 , 你应该尽可能地避免这种情况 。
要知道的事情:
- http和WebSocket的安全链和安全配置是完全独立的。
- Spring
AuthentionProvider
在Websocket身份validation中完全不参与。 - 一旦设置为CONNECT请求,将存储用户 (
simpUser
),并且不再需要进一步的身份validation。
Maven deps
org.springframework.boot spring-boot-starter-websocket org.springframework spring-messaging org.springframework.boot spring-boot-starter-security org.springframework.security spring-security-messaging
WebSocket配置
下面的配置注册了一个简单的消息代理(注意它与认证和授权无关)。
@Configuration @EnableWebSocketMessageBroker public class WebSocketConfig extends WebSocketMessageBrokerConfigurer { @Override public void configureMessageBroker(final MessageBrokerRegistry config) { // These are endpoints the client can subscribes to. config.enableSimpleBroker("/queue/topic"); // Message received with one of those below destinationPrefixes will be automatically router to controllers @MessageMapping config.setApplicationDestinationPrefixes("/app"); } @Override public void registerStompEndpoints(final StompEndpointRegistry registry) { // Handshake endpoint registry.addEndpoint("stomp"); // If you want to you can chain setAllowedOrigins("*") } }
Spring安全配置
由于Stomp协议依赖于第一个HTTP请求,因此我们需要授权对我们的stomp握手端点进行HTTP调用。
@Configuration public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(final HttpSecurity http) throws Exception { // This is not for websocket authorization, and this should most likely not be altered. http .httpBasic().disable() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() .authorizeRequests().antMatchers("/stomp").permitAll() .anyRequest().denyAll(); } }
然后我们将创建一个负责validation用户的服务。
@Component public class WebSocketAuthenticatorService { // This method MSUT return a UsernamePasswordAuthenticationToken, another component in the security chain is testing it with 'instanceof' public UsernamePasswordAuthenticationToken getAuthenticatedOrFail(final String username, final String password) throws AuthenticationException { if (username == null || username.trim().length()) { throw new AuthenticationCredentialsNotFoundException("Username was null or empty."); } if (password == null || password.trim().length()) { throw new AuthenticationCredentialsNotFoundException("Password was null or empty."); } // Add your own logic for retrieving user in fetchUserFromDb() if (fetchUserFromDb(username, password) == null) { throw new BadCredentialsException("Bad credentials for user " + username); } // null credentials, we do not pass the password along return new UsernamePasswordAuthenticationToken( username, null, Collections.singleton((GrantedAuthority) () -> "USER") // MUST provide at least one role ); } }
注意: UsernamePasswordAuthenticationToken
必须有GrantedAuthorities,如果你使用另一个构造函数,Spring将自动设置isAuthenticated = false
。
几乎在那里,现在我们需要创建一个Interceptor,它将设置simpUser
头或在CONNECT消息上抛出AuthenticationException
。
@Component public class AuthChannelInterceptorAdapter extends ChannelInterceptor { private static final String USERNAME_HEADER = "login"; private static final String PASSWORD_HEADER = "passcode"; private final WebSocketAuthenticatorService webSocketAuthenticatorService; @Inject public AuthChannelInterceptorAdapter(final WebSocketAuthenticatorService webSocketAuthenticatorService) { this.webSocketAuthenticatorService = webSocketAuthenticatorService; } @Override public Message> preSend(final Message> message, final MessageChannel channel) throws AuthenticationException { final StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class); if (StompCommand.CONNECT == accessor.getCommand()) { final String username = accessor.getFirstNativeHeader(USERNAME_HEADER); final String password = accessor.getFirstNativeHeader(PASSWORD_HEADER); final UsernamePasswordAuthenticationToken user = webSocketAuthenticatorService.getAuthenticatedOrFail(username, password); accessor.setUser(user); } return message; } }
注意: preSend()
必须返回一个UsernamePasswordAuthenticationToken
,Spring安全链中的另一个元素测试这个。 请注意:如果您的UsernamePasswordAuthenticationToken
是在未传递GrantedAuthority
情况下构建的,则身份validation将失败,因为没有授予权限的构造函数自动设置authenticated = false
这是一个重要的详细信息,未在spring-security中记录 。
最后再创建两个类来分别处理授权和认证。
@Configuration @Order(Ordered.HIGHEST_PRECEDENCE + 99) public class WebSocketAuthenticationSecurityConfig extends WebSocketMessageBrokerConfigurer { @Inject private AuthChannelInterceptorAdapter authChannelInterceptorAdapter; @Override public void registerStompEndpoints(final StompEndpointRegistry registry) { // Endpoints are already registered on WebSocketConfig, no need to add more. } @Override public void configureClientInboundChannel(final ChannelRegistration registration) { registration.setInterceptors(authChannelInterceptorAdapter); } }
注意: @Order
是CRUCIAL不要忘记它,它允许我们的拦截器首先在安全链上注册。
@Configuration public class WebSocketAuthorizationSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer { @Override protected void configureInbound(final MessageSecurityMetadataSourceRegistry messages) { // You can customize your authorization mapping here. messages.anyMessage().authenticated(); } // TODO: For test purpose (and simplicity) i disabled CSRF, but you should re-enable this and provide a CRSF endpoint. @Override protected boolean sameOriginDisabled() { return true; } }
祝你好运 !
对于java客户端,请使用此测试示例:
StompHeaders connectHeaders = new StompHeaders(); connectHeaders.add("login", "test1"); connectHeaders.add("passcode", "test"); stompClient.connect(WS_HOST_PORT, new WebSocketHttpHeaders(), connectHeaders, new MySessionHandler);
- 如何在spring-security中注销所有登录用户?
- Spring需要一个’AuthenticationManager’类型的bean
- 使用Java连接到MongoDB服务器实例期间的身份validation
- 如何在GWT中实现登录屏幕?
- 对会话使用sessionRegistry没有会话数限制?
- 在Jetty服务器中,如何获取需要客户端身份validation时使用的客户端证书?
- 如何使用DaoAuthenticationProvider以编程方式使用Spring Security对用户进行身份validation
- JAX WS客户端无法进行身份validation
- 无需身份validation即可在javax.mail中发送邮件