在使用Spring Security接入WebSocket和STOMP时,需要进行以下配置:
- 配置WebSocket
在WebSocketConfig中,我们需要将Spring Security的ChannelInterceptorAdapter添加到MessageBrokerRegistry中。这样可以在消息发送之前进行身份验证,并允许只有经过身份验证的用户才能发送或接收消息。
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer {
@Autowired
private WebSocketHandshakeInterceptor webSocketHandshakeInterceptor;
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
config.enableSimpleBroker("/topic");
config.setApplicationDestinationPrefixes("/app");
config.setUserDestinationPrefix("/user");
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/ws").setAllowedOrigins("*")
.addInterceptors(webSocketHandshakeInterceptor).withSockJS();
}
}
其中,我们定义了一个自定义的WebSocketHandshakeInterceptor用于对握手请求进行拦截处理。
- 配置Security
首先,需要配置Spring Security以启用WebSockets支持:
@Configuration
@EnableWebSecurity
public class WebSocketSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/ws/**").authenticated()
.anyRequest().permitAll()
.and()
.formLogin();
}
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("user").password("{noop}password").roles("USER");
}
}
在上面的示例中,我们禁用了CSRF保护,并定义了针对/ws/**路径的安全规则。只有经过身份验证的用户才能访问此路径。我们还配置了一个内存中的用户,用户名为”user”,密码为”password”。
- 配置自定义握手拦截器
@Component
public class WebSocketHandshakeInterceptor extends ChannelInterceptorAdapter implements HandshakeInterceptor {
private static final Logger LOG = LoggerFactory.getLogger(WebSocketHandshakeInterceptor.class);
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response,
WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception {
LOG.info("Before Handshake");
if (request instanceof ServletServerHttpRequest) {
ServletServerHttpRequest servletRequest = (ServletServerHttpRequest) request;
HttpSession session = servletRequest.getServletRequest().getSession();
attributes.put("sessionId", session.getId());
}
return true;
}
@Override
public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response,
WebSocketHandler wsHandler, Exception ex) {
LOG.info("After Handshake");
}
@Override
public Message<?> preSend(Message<?> message, MessageChannel channel) {
StompHeaderAccessor accessor =
MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
if (StompCommand.CONNECT.equals(accessor.getCommand())) {
String sessionId = (String) accessor.getSessionAttributes().get("sessionId");
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth == null) {
throw new AccessDeniedException("Access is denied");
} else {
accessor.setUser(new User(auth.getName(), "", auth.getAuthorities()));
accessor.setSessionId(sessionId);
}
}
return message;
}
}
上述示例中,我们定义了一个自定义的WebSocketHandshakeInterceptor,用于对握手请求进行拦截处理。在beforeHandshake()方法中,我们将HttpSession Id存储到WebSocket连接属性中。
然后,在preSend()方法中,我们从StompHeaderAccessor获取会话ID并与当前SecurityContext中的用户关联。如果没有找到身份验证用户,则抛出AccessDeniedException。
- 配置Stomp控制器
@Controller
public class WebSocketController {
@MessageMapping("/hello")
@SendToUser("/topic/greetings")
public Greeting greeting(HelloMessage message, Principal principal) throws Exception {
Thread.sleep(1000);
return new Greeting("Hello, " + message.getName() + "!", principal.getName());
}
}
class HelloMessage {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
class Greeting {
private String content;
private String user;
public Greeting(String content, String user) {
this.content = content;
this.user = user;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public String getUser() {
return user;
}
public void setUser(String user) {
this.user = user;
}
}
在上面的示例中,我们定义了一个@MessageMapping注释的方法来接收客户端发送的消息。然后,使用@SendToUser注释将响应发送到指定用户的订阅路径。
在这个例子中,我们还定义了一个Greeting类,用于存储消息内容和用户信息。
其中,在STOMP协议的CONNECTED帧中包含有关已连接用户的身份验证信息。Principal对象包含与该用户相关联的名称和权限。因此,在Greeting类构造函数中,我们将其作为参数传递并存储到user属性中。
- 用户信息对应
通过上述示例可知,Spring Security授权成功后将会返回一个Principal对象,在WebSocket控制器方法签名处使用@MessageMapping注解接收请求时可以直接获取当前用户信息。
例如:
public Greeting greeting(HelloMessage message, Principal principal) throws Exception {
Thread.sleep(1000);
return new Greeting("Hello, " + message.getName() + "!", principal.getName());
}
在上面的例子中,principal.getName()返回当前用户的用户名。如果您需要更多的用户详细信息,则可以从SecurityContext获取Authentication对象,并从该对象获取任何其他属性或详细信息。




