# websocket
**Repository Path**: xuelingkang/websocket
## Basic Information
- **Project Name**: websocket
- **Description**: 基于spring-boot 2.x扩展WebSocket,支持细粒度控制
- **Primary Language**: Java
- **License**: GPL-3.0
- **Default Branch**: master
- **Homepage**: https://demo.xzixi.com
- **GVP Project**: No
## Statistics
- **Stars**: 8
- **Forks**: 2
- **Created**: 2019-07-23
- **Last Updated**: 2022-01-25
## Categories & Tags
**Categories**: web-dev-toolkits
**Tags**: None
## README
# 基于spring-boot 2.x扩展WebSocket,支持细粒度控制
> 主要用于动态权限控制
## 介绍
基于spring-boot_2.1.6.RELEASE,对spring框架的WebSocket扩展,支持细粒度控制
博客主页
## spring security文档

spring security只支持在应用启动时加载WebSocket权限信息,修改权限必须要重启应用才能生效,不能按照用户的权限动态授权
spring提供的HandshakeInterceptor接口可以自定义拦截器,但是只能在握手是进行一次拦截,无法细粒度控制权限
## StompSubProtocolHandler源码
```java
package org.springframework.web.socket.messaging;
public class StompSubProtocolHandler implements SubProtocolHandler, ApplicationEventPublisherAware {
	/**
	 * 接收客户端消息方法
	 * Handle incoming WebSocket messages from clients.
	 */
	public void handleMessageFromClient(WebSocketSession session,
			WebSocketMessage> webSocketMessage, MessageChannel outputChannel) {
		List> messages;
		try {
			ByteBuffer byteBuffer;
			if (webSocketMessage instanceof TextMessage) {
				byteBuffer = ByteBuffer.wrap(((TextMessage) webSocketMessage).asBytes());
			}
			else if (webSocketMessage instanceof BinaryMessage) {
				byteBuffer = ((BinaryMessage) webSocketMessage).getPayload();
			}
			else {
				return;
			}
			BufferingStompDecoder decoder = this.decoders.get(session.getId());
			if (decoder == null) {
				throw new IllegalStateException("No decoder for session id '" + session.getId() + "'");
			}
			messages = decoder.decode(byteBuffer);
			if (messages.isEmpty()) {
				if (logger.isTraceEnabled()) {
					logger.trace("Incomplete STOMP frame content received in session " +
							session + ", bufferSize=" + decoder.getBufferSize() +
							", bufferSizeLimit=" + decoder.getBufferSizeLimit() + ".");
				}
				return;
			}
		}
		catch (Throwable ex) {
			if (logger.isErrorEnabled()) {
				logger.error("Failed to parse " + webSocketMessage +
						" in session " + session.getId() + ". Sending STOMP ERROR to client.", ex);
			}
			handleError(session, ex, null);
			return;
		}
		for (Message message : messages) {
			try {
				StompHeaderAccessor headerAccessor =
						MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
				Assert.state(headerAccessor != null, "No StompHeaderAccessor");
				headerAccessor.setSessionId(session.getId());
				headerAccessor.setSessionAttributes(session.getAttributes());
				headerAccessor.setUser(getUser(session));
				headerAccessor.setHeader(SimpMessageHeaderAccessor.HEART_BEAT_HEADER, headerAccessor.getHeartbeat());
				if (!detectImmutableMessageInterceptor(outputChannel)) {
					headerAccessor.setImmutable();
				}
				if (logger.isTraceEnabled()) {
					logger.trace("From client: " + headerAccessor.getShortLogMessage(message.getPayload()));
				}
				StompCommand command = headerAccessor.getCommand();
				boolean isConnect = StompCommand.CONNECT.equals(command) || StompCommand.STOMP.equals(command);
				if (isConnect) {
					this.stats.incrementConnectCount();
				}
				else if (StompCommand.DISCONNECT.equals(command)) {
					this.stats.incrementDisconnectCount();
				}
				try {
					SimpAttributesContextHolder.setAttributesFromMessage(message);
					boolean sent = outputChannel.send(message);
					if (sent) {
						if (isConnect) {
							Principal user = headerAccessor.getUser();
							if (user != null && user != session.getPrincipal()) {
								this.stompAuthentications.put(session.getId(), user);
							}
						}
						if (this.eventPublisher != null) {
							Principal user = getUser(session);
							if (isConnect) {
								publishEvent(this.eventPublisher, new SessionConnectEvent(this, message, user));
							}
							else if (StompCommand.SUBSCRIBE.equals(command)) {
								publishEvent(this.eventPublisher, new SessionSubscribeEvent(this, message, user));
							}
							else if (StompCommand.UNSUBSCRIBE.equals(command)) {
								publishEvent(this.eventPublisher, new SessionUnsubscribeEvent(this, message, user));
							}
						}
					}
				}
				finally {
					SimpAttributesContextHolder.resetAttributes();
				}
			}
			catch (Throwable ex) {
				if (logger.isErrorEnabled()) {
					logger.error("Failed to send client message to application via MessageChannel" +
							" in session " + session.getId() + ". Sending STOMP ERROR to client.", ex);
				}
				handleError(session, ex, message);
			}
		}
	}
	/**
	 * 向客户端发送消息的方法
	 * Handle STOMP messages going back out to WebSocket clients.
	 */
	@Override
	@SuppressWarnings("unchecked")
	public void handleMessageToClient(WebSocketSession session, Message> message) {
		if (!(message.getPayload() instanceof byte[])) {
			if (logger.isErrorEnabled()) {
				logger.error("Expected byte[] payload. Ignoring " + message + ".");
			}
			return;
		}
		StompHeaderAccessor accessor = getStompHeaderAccessor(message);
		StompCommand command = accessor.getCommand();
		if (StompCommand.MESSAGE.equals(command)) {
			if (accessor.getSubscriptionId() == null && logger.isWarnEnabled()) {
				logger.warn("No STOMP \"subscription\" header in " + message);
			}
			String origDestination = accessor.getFirstNativeHeader(SimpMessageHeaderAccessor.ORIGINAL_DESTINATION);
			if (origDestination != null) {
				accessor = toMutableAccessor(accessor, message);
				accessor.removeNativeHeader(SimpMessageHeaderAccessor.ORIGINAL_DESTINATION);
				accessor.setDestination(origDestination);
			}
		}
		else if (StompCommand.CONNECTED.equals(command)) {
			this.stats.incrementConnectedCount();
			accessor = afterStompSessionConnected(message, accessor, session);
			if (this.eventPublisher != null) {
				try {
					SimpAttributes simpAttributes = new SimpAttributes(session.getId(), session.getAttributes());
					SimpAttributesContextHolder.setAttributes(simpAttributes);
					Principal user = getUser(session);
					publishEvent(this.eventPublisher, new SessionConnectedEvent(this, (Message) message, user));
				}
				finally {
					SimpAttributesContextHolder.resetAttributes();
				}
			}
		}
		byte[] payload = (byte[]) message.getPayload();
		if (StompCommand.ERROR.equals(command) && getErrorHandler() != null) {
			Message errorMessage = getErrorHandler().handleErrorMessageToClient((Message) message);
			if (errorMessage != null) {
				accessor = MessageHeaderAccessor.getAccessor(errorMessage, StompHeaderAccessor.class);
				Assert.state(accessor != null, "No StompHeaderAccessor");
				payload = errorMessage.getPayload();
			}
		}
		sendToClient(session, accessor, payload);
	}
}
```
阅读源码可以发现spring原生的消息处理类不支持自定义拦截器
## interceptable-websocket
做这个项目是为了细粒度的动态控制WebSocket的权限,项目对StompSubProtocolHandler类和其他相关的类做了扩展,增加了对自定义拦截器的支持
具体实现在extension包,拦截器实现在interceptor包
### 使用方法
#### 直接使用
项目已经发布到maven中央仓库,直接在pom.xml中引用即可
```xml
    
        com.xzixi
        interceptable-websocket
        2.0
    
```
#### 修改后使用
1. 下载项目
打开git bash窗口,执行命令`git clone git@gitee.com:xuelingkang/websocket.git`
2. 编译并安装到本地maven仓库
进入工程目录,打开cmd窗口,执行命令`mvn clean install`
3. 在自己的项目中引用
```xml
    
        com.xzixi
        interceptable-websocket
        2.0
    
```
配置类:
```java
package com.xzixi.websocket.interceptablewebsocketdemo.config;
@Configuration
@EnableInterceptableWebSocketMessageBroker // 增加注解
public class InterceptableSecurityWebSocketConfig extends AbstractInterceptableSecurityWebSocketMessageBrokerConfigurer {
    @Override
    public void registerStompEndpoints(InterceptableWebMvcStompEndpointRegistry registry) {
        // 注册拦截器
        registry.addFromClientInterceptor(accessDecisionFromClientInterceptor()) // 消息授权决策
                .addFromClientInterceptor(sessionIdUnRegistryInterceptor()) // sessionId记录
                .addToClientInterceptor(sessionIdRegistryInterceptor()); // sessionId移除
    }
}
```
具体使用方法请参考案例工程:interceptable-websocket-demo
## 更新
- [更新日志](./UPDATELOG.md)
## 欢迎issue和star!