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<String> 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<Session> sessionRepository;

    @Override
    public boolean beforeHandshake(ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse, WebSocketHandler wsHandler,
            Map<String, Object> 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<String, String> parameters = UriComponentsBuilder.fromUri(serverHttpRequest.getURI()).build().getQueryParams();
        List<String> 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 請求就行。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章