Springboot http session支持分布式;同时支持 cookie 和 header 传递;websocket 连接 共享 http session

这里有三个问题:

1. http session支持分布式;

2. session 同时支持 cookie 和 header 传递;

3. websocket 连接 共享 http session。

对于第一个问题,很简单:

implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'org.springframework.session:spring-session-data-redis'
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 600)

原理:session 放在公共的地方,所有服务都能共享。

第二个问题,本来也是很简单的事情,想着肯定有现成方案,结果在国内外网上查了一圈,没找到可以直接复制黏贴的。这个也有些 org.springframework.session:spring-session 版本更新导致的因素在里面,我目前用的是 springboot 2.2.5.RELEASE,已经和 spring-session 不那么配套了,现在 springboot 是以 HttpSessionIdResolver 来决定 http session 的存取,而不是以前的 HttpSessionStrategy,但是IoC核心没变,做类似的解决方案就行了,网上没有,那就自己动手,”我还是从前那个少年,没有一丝丝改变“。

Springboot 默认只提供:CookieHttpSessionIdResolver(默认被配置)和 HeaderHttpSessionIdResolver 两个 HttpSessionIdResolver,且一般只配置一个,所谓 HttpSessionIdResolver 就是 HttpSession id 的解析器。现在把这两个合并成一个,然后使用就行了:
 

public class HeaderCookieHttpSessionIdResolver implements HttpSessionIdResolver {

    private static final String WRITTEN_SESSION_ID_ATTR = CookieHttpSessionIdResolver.class.getName().concat(".WRITTEN_SESSION_ID_ATTR");
    private static final String HEADER_X_AUTH_TOKEN = "X-AUTH-TOKEN";

    private CookieSerializer cookieSerializer = new DefaultCookieSerializer();

    /**
     * Sets the {@link CookieSerializer} to be used.
     * 
     * @param cookieSerializer the cookieSerializer to set. Cannot be null.
     */
    public void setCookieSerializer(CookieSerializer cookieSerializer) {
        if (cookieSerializer == null) {
            throw new IllegalArgumentException("cookieSerializer cannot be null");
        }
        this.cookieSerializer = cookieSerializer;
    }

    @Override
    public List resolveSessionIds(HttpServletRequest request) {
        String headerValue = request.getHeader(HEADER_X_AUTH_TOKEN);
        if (!StringUtils.isEmpty(headerValue)) return Collections.singletonList(headerValue);
        return this.cookieSerializer.readCookieValues(request);
    }

    @Override
    public void setSessionId(HttpServletRequest request, HttpServletResponse response, String sessionId) {
        response.setHeader(HEADER_X_AUTH_TOKEN, sessionId);
        if (!sessionId.equals(request.getAttribute(WRITTEN_SESSION_ID_ATTR))) {
            request.setAttribute(WRITTEN_SESSION_ID_ATTR, sessionId);
            this.cookieSerializer.writeCookieValue(new CookieValue(request, response, sessionId));
        }
    }

    @Override
    public void expireSession(HttpServletRequest request, HttpServletResponse response) {
        response.setHeader(HEADER_X_AUTH_TOKEN, "");
        this.cookieSerializer.writeCookieValue(new CookieValue(request, response, ""));
    }

}

这里需要注意的是:我把 header 验证放在了 cookie 之前,因为一般的浏览器或者 http 工具都会默认打开 cookie 存储,所以每一个客户端的请求带的 cookie 是不同的,就会被识别成不同的会话。先验证 header,如果不包含 X-AUTH-TOKEN 字段,那么就认为是从同一个客户端发来的请求,去验证 cookie。

(顺便吐槽一下:springboot 内置类代码也真是各种风格,也有一些不怎么标准嘛。。)

把这个解析器放入 IoC 容器:

// session策略,这里同时提供Header,Cookie方式
    @Bean("httpSessionIdResolver")
    public HeaderCookieHttpSessionIdResolver httpSessionIdResolver() {
        return new HeaderCookieHttpSessionIdResolver();
    }

原理: 重写默认处理,合并处理逻辑。

第三个问题,就是把 http session id 给前端,连 websocket 的时候,带在 url 后面参数或者 header 里都可以,然后认证的时候取出来看看这个 http session 死没死 在不在。

首先,http 登录成功后,返回 http session id:

@Slf4j
@Component
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    @Autowired
    private SupportConfiguration supportConfiguration;

    public MyAuthenticationSuccessHandler() {

    }

    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication)
            throws IOException, ServletException {
        MyResponse response = new MyResponse();
        HttpSession httpSession = httpServletRequest.getSession();
        httpSession.setAttribute("http_principal", authentication);
//        String csrfTokenKey = HttpSessionCsrfTokenRepository.class.getName().concat(".CSRF_TOKEN");
//        String csrfToken = ((CsrfToken) httpSession.getAttribute(csrfTokenKey)).getToken();
        log.info("http session id: " + httpSession.getId());
//        log.info("csrf token: " + csrfToken);
        response.setData(new JSONObject() //
                // .fluentPut("csrf_token", csrfToken) // csrf token
                .fluentPut("auth_token", httpSession.getId()) // auth token = session id
                .fluentPut("cmconnect", supportConfiguration.getCmconnectUrl()) // cmconnect address
                .fluentPut("websocket", supportConfiguration.getWebsocketUrl()) // websocket connection address
        );
        httpServletResponse.setHeader("Content-Type", "application/json;charset=UTF-8");
        httpServletResponse.getWriter().write(response.toString());
    }

}

然后配置 websocket 端点及握手时期过滤器:

/**
	 * 
	 * 兼容web端 SockJS,如果是安卓直接Websocket访问,url后再加 /websocket
	 * 即:ws://x.x.x.x/stomp/websocket
	 */
	@Override
	public void registerStompEndpoints(StompEndpointRegistry stompEndpointRegistry) {
		stompEndpointRegistry // ------------------------------------------------------------------
				.addEndpoint("/stomp") // 将/serviceName/stomp/websocket路径注册为STOMP的端点
				.setAllowedOrigins("*") // 可以跨域
				.addInterceptors(handshakeInterceptor) // 自己定义的获取httpsession的拦截器
				.setHandshakeHandler(handshakeHandler) // 封装认证用户信息
				.withSockJS() // 支持socktJS访问
		; // --------------------------------------------------------------------------------------
	}

最后编写握手处理:

/**
 * <设置认证用户信息的握手拦截器>
 **/
@Slf4j
@Service
public class MyHandshakeInterceptor implements HandshakeInterceptor {

    @Resource(name = "sessionRepository")
    private SessionRepository sessionRepository;

    @Override
    public boolean beforeHandshake(ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse, WebSocketHandler wsHandler,
            Map attributes) {
        log.debug("websocket principal: " + serverHttpRequest.getPrincipal());
        log.debug("websocket connect, request uri: " + serverHttpRequest.getURI());
        String token = getToken(serverHttpRequest);
        if (StringUtils.isEmpty(token)) {
            log.error("Token empty!");
            return false;
        }
        Session session = sessionRepository.findById(token);
        if (session == null) {
            log.error("Session not exist!");
            return false;
        }
        Principal httpPrincipal = (Principal) session.getAttribute("http_principal");
        log.debug("http principal: " + httpPrincipal);
        attributes.put("http_principal", httpPrincipal);
        return true;
    }

    @Override
    public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {

    }

    private String getToken(ServerHttpRequest serverHttpRequest) {
        String token = "";
        // 从http请求的url参数中获取
        MultiValueMap parameters = UriComponentsBuilder.fromUri(serverHttpRequest.getURI()).build().getQueryParams();
        List authParameter = parameters.get("_auth");
        if (authParameter != null) token = authParameter.get(0);
        // 从http请求的header中获取
        if (token == null && serverHttpRequest.getHeaders().get("X-AUTH-TOKEN") != null)
            token = serverHttpRequest.getHeaders().get("X-AUTH-TOKEN").get(0);
        return token;
    }

}

原理:websocket 连接是建立在 http 上的,在 ws 连接前,会以 http 握手一次,这时即可以当普通 http 请求就行。

你可能感兴趣的:(Java,Springboot,Websocket)