最近遇到了一个需求,根据不同用户的权限能接收不同的消息,在websocket中获取用户信息的时候遇到了困难,发现了一篇写的很清楚的文章,记录一下。后面还有遇到的问题
原地址:https://www.cnblogs.com/zhuxiaojie/p/6238826.html
@ServerEndpoint(value = "/websocket")
@Component
public class MyWebSocket {
//与某个客户端的连接会话,需要通过它来给客户端发送数据
private Session session;
/**
* 连接成功*/
@OnOpen
public void onOpen(Session session) {
this.session = session;
}
/**
* 连接关闭调用的方法
*/
@OnClose
public void onClose() {
}
/**
* 收到消息
*
* @param message
*/
@OnMessage
public void onMessage(String message, Session session) {
System.out.println("来自浏览器的消息:" + message);
//群发消息
for (MyWebSocket item : webSocketSet) {
try {
item.sendMessage(message);
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 发生错误时调用
*/
@OnError
public void onError(Session session, Throwable error) {
System.out.println("发生错误");
error.printStackTrace();
}
public void sendMessage(String message) throws IOException {
this.session.getBasicRemote().sendText(message);//同步
//this.session.getAsyncRemote().sendText(message);//异步
}
}
获取HttpSession是一个很有必要讨论的问题,因为java后台需要知道当前是哪个用户,用以处理该用户的业务逻辑,或者是对该用户进行授权之类的,但是由于websocket的协议与Http协议是不同的,所以造成了无法直接拿到session。但是问题总是要解决的,不然这个websocket协议所用的场景也就没了。
3.1.获取HttpSession的工具类,源码详细分析
package javax.websocket.server;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.websocket.Decoder;
import javax.websocket.Encoder;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface ServerEndpoint {
/**
* URI or URI-template that the annotated class should be mapped to.
* @return The URI or URI-template that the annotated class should be mapped
* to.
*/
String value();
String[] subprotocols() default {};
Class extends Decoder>[] decoders() default {};
Class extends Encoder>[] encoders() default {};
public Class extends ServerEndpointConfig.Configurator> configurator()
default ServerEndpointConfig.Configurator.class;
}
我们看到最后的一个方法,可以看到,它要求返回一个ServerEndpointConfig.Configurator的子类,我们写一个类去继承它。
import javax.servlet.http.HttpSession;
import javax.websocket.HandshakeResponse;
import javax.websocket.server.HandshakeRequest;
import javax.websocket.server.ServerEndpointConfig;
import javax.websocket.server.ServerEndpointConfig.Configurator;
public class HttpSessionConfigurator extends Configurator {
@Override
public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {
//怎么搞?
}
}
当我们覆盖modifyHandshake方法时,可以看到三个参数,其中后面两个参数让我们感觉有点见过的感觉,我们查看一HandshakeRequest的源码
package javax.websocket.server;
import java.net.URI;
import java.security.Principal;
import java.util.List;
import java.util.Map;
/**
* Represents the HTTP request that asked to be upgraded to WebSocket.
*/
public interface HandshakeRequest {
static final String SEC_WEBSOCKET_KEY = "Sec-WebSocket-Key";
static final String SEC_WEBSOCKET_PROTOCOL = "Sec-WebSocket-Protocol";
static final String SEC_WEBSOCKET_VERSION = "Sec-WebSocket-Version";
static final String SEC_WEBSOCKET_EXTENSIONS= "Sec-WebSocket-Extensions";
Map> getHeaders();
Principal getUserPrincipal();
URI getRequestURI();
boolean isUserInRole(String role);
/**
* Get the HTTP Session object associated with this request. Object is used
* to avoid a direct dependency on the Servlet API.
* @return The javax.servlet.http.HttpSession object associated with this
* request, if any.
*/
Object getHttpSession();
Map> getParameterMap();
String getQueryString();
}
我们发现它是一个接口,接口中规范了这样的一个方法
/**
* Get the HTTP Session object associated with this request. Object is used
* to avoid a direct dependency on the Servlet API.
* @return The javax.servlet.http.HttpSession object associated with this
* request, if any.
*/
Object getHttpSession();
上面有相应的注释,说明可以从Servlet API中获取到相应的HttpSession。
当我们发现这个方法的时候,其实已经松了一口气了。
那么我们就可以补全未完成的代码
import javax.servlet.http.HttpSession;
import javax.websocket.HandshakeResponse;
import javax.websocket.server.HandshakeRequest;
import javax.websocket.server.ServerEndpointConfig;
import javax.websocket.server.ServerEndpointConfig.Configurator;
/**
* 从websocket中获取用户session
*
*
*/
public class HttpSessionConfigurator extends Configurator {
@Override
public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {
HttpSession httpSession = (HttpSession) request.getHttpSession();
sec.getUserProperties().put(HttpSession.class.getName(), httpSession);
}
}
其实通过上面的源码分析,你们应该知道了HttpSession的获取。但是下面又多了一行代码
sec.getUserProperties().put(HttpSession.class.getName(), httpSession);
这行代码又是什么意思呢?
我们看一下ServerEnpointConfig的声明
public interface ServerEndpointConfig extends EndpointConfig
我们发现这个接口继承了EndpointConfig的接口,好,我们看一下EndpointConfig的源码:
package javax.websocket;
import java.util.List;
import java.util.Map;
public interface EndpointConfig {
List> getEncoders();
List> getDecoders();
Map getUserProperties();
}
我们发现了这样的一个方法定义
Map getUserProperties();
可以看到,它是一个map,从方法名也可以理解到,它是用户的一些属性的存储,那既然它提供了get方法,那么也就意味着我们可以拿到这个map,并且对这里面的值进行操作,
所以就有了上面的
sec.getUserProperties().put(HttpSession.class.getName(), httpSession);
那么到此,获取HttpSession的源码分析,就完成了。
3.2.设置HttpSession的类
我们之前有说过,由于HTTP协议与websocket协议的不同,导致没法直接从websocket中获取协议,然后在3.1中我们已经写了获取HttpSession的代码,但是如果真的放出去执行,那么会报空指值异常,因为这个HttpSession并没有设置进去。
好,这一步,我们来设置HttpSession。这时候我们需要写一个监听器。
import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
import javax.servlet.http.HttpServletRequest;
import org.springframework.stereotype.Component;
@Component
public class RequestListener implements ServletRequestListener {
public void requestInitialized(ServletRequestEvent sre) {
//将所有request请求都携带上httpSession
((HttpServletRequest) sre.getServletRequest()).getSession();
}
public RequestListener() {
}
public void requestDestroyed(ServletRequestEvent arg0) {
}
}
然后我们需要把这个类注册为监听器,请自行百度。
3.3.在websocket中获取用户的session
然后刚才我们通过源码分析,是知道@ServerEndpoint注解中是有一个参数可以配置我们刚才继承的类。好的,我们现在进行配置。
@ServerEndpoint(value = "/websocket" , configurator = HttpSessionConfigurator.class)
Config对象,并且通过这个对象,拿到之前我们设置进去的map
@OnOpen
public void onOpen(Session session,EndpointConfig config){
HttpSession httpSession= (HttpSession) config.getUserProperties().get(HttpSession.class.getName());
User user = (User)httpSession.getAttribute(SessionName.USER);
if(user != null){
this.session = session;
this.httpSession = httpSession;
}else{
//用户未登陆
try {
session.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
这下我们就从java的webscoket中拿到了用户的session。
#注意
按上面流程操作的时候出现了HttpSession为空,获取不到用户信息的情况
在前端连接WebSocket的时候,我的代码是这样的:
var url = "ws://127.0.0.1/XXXX/XXXX";
然而浏览器地址栏是这样的:
http://localhost:8080/
网上解释说如果不使用同一个host,则会创建不同的连接请求。在没有HttpSession激活状态的时候,使用getSession()方法会新建一个HttpSession,也就是说实际上这个监听器是在没有激活HttpSession的情况下不断新建会话。
于是我们可以这样拼接一下:
var host = window.location.host;
var url = "ws://"+host+"/XXXX/XXXX";
这样就可以正常的获取到用户的session了