@TOC
一、需要了解的事项
- http和WebSocket的安全链和安全配置是完全独立的。
- SpringAuthenticationProvider根本不参与 Websocket 身份验证。
- 将要给出的示例中,身份验证不会发生在 HTTP 协商端点上,因为 JavaScript STOMP(websocket)库不会随 HTTP 请求一起发送必要的身份验证标头。
- 一旦在 CONNECT 请求上设置,用户( simpUser) 将被存储在 websocket 会话中,并且以后的消息将不再需要进行身份验证。
二、依赖
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 配置
3.1 、简单的消息代理
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends WebSocketMessageBrokerConfigurer {
@Override
public void configureMessageBroker(final MessageBrokerRegistry config) {
config.enableSimpleBroker("/queue/topic");
config.setApplicationDestinationPrefixes("/app");
}
@Override
public void registerStompEndpoints(final StompEndpointRegistry registry) {
registry.addEndpoint("stomp");
setAllowedOrigins("*")
}
}
3.2 、Spring安全配置
由于 Stomp 协议依赖于第一个 HTTP 请求,因此需要授权对 stomp 握手端点的 HTTP 调用。
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(final HttpSecurity http) throws Exception
http.httpBasic().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeRequests().antMatchers("/stomp").permitAll()
.anyRequest().denyAll();
}
}
然后创建一个负责验证用户身份的服务。
@Component
public class WebSocketAuthenticatorService {
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.");
}
if (fetchUserFromDb(username, password) == null) {
throw new BadCredentialsException("Bad credentials for user " + username);
}
return new UsernamePasswordAuthenticationToken(
username,
null,
Collections.singleton((GrantedAuthority) () -> "USER") // 必须给至少一个角色
);
}
}
接着需要创建一个拦截器,它将设置“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,则身份验证将失败,因为没有授予权限的构造函数自动设置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) {
// 这里不用给任何东西
}
@Override
public void configureClientInboundChannel(final ChannelRegistration registration) {
registration.setInterceptors(authChannelInterceptorAdapter);
}
}
请注意:这@Order是至关重要的,它允许我们的拦截器首先在安全链中注册。
@Configuration
public class WebSocketAuthorizationSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {
@Override
protected void configureInbound(final MessageSecurityMetadataSourceRegistry messages) {
// 添加自己的映射
messages.anyMessage().authenticated();
}
// 这里请自己按需求修改
@Override
protected boolean sameOriginDisabled() {
return true;
}
}
之后编写客户端进行连接,我们就可以这样指定客户端进行消息的发送。
@MessageMapping("/greeting")
public void greetingReturn(@Payload Object ojd){
simpMessagingTemplate.convertAndSendToUser(username,"/topic/greeting",ojd);
}