如上所述,文档(ATM)尚不清楚(IMHO),直到Spring提供一些清晰的文档为止,这是一个样板,可避免您花两天的时间试图了解安全链的功能。
Rob-Leggett做出了非常不错的尝试,但他在Springs上课了,所以我感到不舒服。
要知道的事情:
- __http和WebSocket的 安全链 和 安全配置 是完全独立的。
- Spring
AuthenticationProvider
完全不参与Websocket身份验证。 - 不会在HTTP协商终结点上进行身份验证,因为没有Javascript STOMP(Websocket)将身份验证标头与HTTP请求一起发送。
- 在CONNECT请求上设置后, 用户 (
simpUser
)将存储在websocket会话中,以后的消息将不再需要身份验证。
Maven部门
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId></dependency><dependency> <groupId>org.springframework</groupId> <artifactId>spring-messaging</artifactId></dependency><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId></dependency><dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-messaging</artifactId></dependency>
WebSocket配置
下面的配置注册一个简单的消息代理(请注意,它与身份验证或授权无关)。
@Configuration@EnableWebSocketMessageBrokerpublic 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请求,因此我们需要授权对脚踏握手端点的HTTP调用。
@Configurationpublic 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(); }}然后,我们将创建一个负责验证用户身份的服务。
@Componentpublic class WebSocketAuthenticatorService { // This method MUST return a UsernamePasswordAuthenticationToken instance, the spring security chain is testing it with 'instanceof' later on. So don't use a subclass of it or any other class public UsernamePasswordAuthenticationToken getAuthenticatedOrFail(final String username, final String password) throws AuthenticationException { if (username == null || username.trim().isEmpty()) { throw new AuthenticationCredentialsNotFoundException("Username was null or empty."); } if (password == null || password.trim().isEmpty()) { 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。
几乎在那儿,现在我们需要创建一个拦截器,该拦截器将设置“ simpUser”标头或在CONNECT消息上抛出“
AuthenticationException”。
@Componentpublic class AuthChannelInterceptorAdapter extends ChannelInterceptor { private static final String USERNAME_HEADER = "login"; private static final String PASSWORD_HEADER = "passpre"; 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()必须 返回a
UsernamePasswordAuthenticationToken,Spring安全链中的另一个元素对此进行测试。请注意:如果您的
UsernamePasswordAuthenticationToken构建未通过
GrantedAuthority,则身份验证将失败,因为未经授权的构造函数会自动设置“
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); }}请注意:
@Orderis CRUCIAL 别忘了它,它使我们的拦截器可以在安全链中首先注册。
@Configurationpublic 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; }}


