webSocket+jwt實現方式


背景:

原項目是通過前端定時器獲取消息,存在消息滯后、空刷服務器、浪費帶寬和資源的問題,在springboot項目集成websocket可以實現實時點對點消息推送。

原項目是在header添加jwt令牌實現認證,由於websocket不支持在頭部添加信息(或許是我打開的方式不對?),最終只能采用在url添加令牌參數實現認證,感覺不夠優雅,后續再想辦法重構改進。

ps:至於放行websocket相關url,完全不要去考慮,危害巨大。

 

1、websocket核心依賴

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>

2、config

@Configuration
public class WebSocketConfig {
    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}

3、WebSocketServer

@Slf4j
@ServerEndpoint("/webSocket/{code}")
@Component
public class WebSocketServer {
    /**
     * concurrent包的線程安全Set,用來存放每個客戶端對應的WebSocket對象。
     */
    private static CopyOnWriteArraySet<WebSocketServer> webSocketSet = new CopyOnWriteArraySet<>();

    /**
     * 與客戶端的連接會話,需要通過它來給客戶端發送數據
     */
    private Session session;

    /**
     * 接收識別碼
     */
    private String code = "";

    /**
     * 連接建立成功調用的方法
     */
    @OnOpen
    public void onOpen(Session session, @PathParam("code") String code) {
        this.session = session;
        //如果存在就先刪除一個,防止重復推送消息,實際這里實現了set,不刪除問題也不大
        webSocketSet.removeIf(webSocket -> webSocket.code.equals(code));
        webSocketSet.add(this);
        this.code = code;
        log.info("建立WebSocket連接,code:" + code+",當前連接數:"+webSocketSet.size());
    }

    /**
     * 連接關閉調用的方法
     */
    @OnClose
    public void onClose() {
        webSocketSet.remove(this);
        log.info("關閉WebSocket連接,code:" + this.code+",當前連接數:"+webSocketSet.size());
    }

    /**
     * 收到客戶端消息后調用的方法
     *
     * @param message 客戶端發送過來的消息
     */
    @OnMessage
    public void onMessage(String message, Session session) {
        log.info("收到來[" + code + "]的信息:" + message);

    }

    @OnError
    public void onError(Session session, Throwable error) {
        log.error("websocket發生錯誤");
        error.printStackTrace();
    }

    /**
     * 實現服務器主動推送
     */
    private void sendMessage(String message) throws IOException {
        this.session.getBasicRemote().sendText(message);
    }


    /**
     * 群發自定義消息
     */
    public void sendAll(String message) {
        log.info("推送消息到" + code + ",推送內容:" + message);
        for (WebSocketServer item : webSocketSet) {
            try {
                item.sendMessage(message);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 定點推送
     */
    public void sendTo(String message, @PathParam("code") String code) {
        for (WebSocketServer item : webSocketSet) {
            try {
                if (item.code.equals(code)) {
                    log.info("推送消息到[" + code + "],推送內容:" + message);
                    item.sendMessage(message);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        WebSocketServer that = (WebSocketServer) o;
        return Objects.equals(session, that.session) &&
                Objects.equals(code, that.code);
    }

    @Override
    public int hashCode() {
        return Objects.hash(session, code);
    }
}

4、令牌過濾器

@Slf4j
@Component
public class JwtTokenFilter extends OncePerRequestFilter {

    @Resource
    JwtProperties jwtProperties;

    @Resource
    TokenProvider tokenProvider;

    @Resource
    OnlineUserService onlineUserService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // http連接時,客戶端應該是在頭信息中攜帶令牌
        String authorizationHeader = request.getHeader(jwtProperties.getHeader());
        if(StringUtils.isBlank(authorizationHeader)) {
            // websocket連接時,令牌放在url參數上,以后重構
            authorizationHeader = request.getParameter(jwtProperties.getHeader());
        }

        String token = null;
        if(!StringUtils.isEmpty(authorizationHeader) && authorizationHeader.startsWith(jwtProperties.getTokenStartWith())){
            token = authorizationHeader.replace(jwtProperties.getTokenStartWith(),"");
        }
        //驗證token
        if(StringUtils.isNotBlank(token) && tokenProvider.validateToken(token)){
            //驗證token是否在緩存中
            OnlineUserDto onlineUserDto = onlineUserService.getOne(jwtProperties.getOnlineKey() + token);
            if(onlineUserDto!=null){
                Authentication authentication = tokenProvider.getAuthentication(token, request);
                SecurityContextHolder.getContext().setAuthentication(authentication);
                log.debug("set Authentication to security context for '{}', uri: {}", authentication.getName(), request.getRequestURI());
            }
        }

        filterChain.doFilter(request, response);
    }
}

5、在業務中調用方式(偽代碼)

    @Resource
    private WebSocketServer webSocketServer;

    // 向客戶端推送實時消息
    webSocketServer.sendTo(content, sysUser.getId());

6、前端,偽代碼

    getMessageCount() {
      getMyMessageCount().then(res => {
        const count = res
        this.messageCount = count > 0 ? count : null
      })
    },
    initWebSocket() {
      const wsUri = process.env.VUE_APP_WS_API + '/webSocket/' + this.user.id + '?Authorization=' + getToken()
      this.websock = new WebSocket(wsUri)
      this.websock.onopen = this.webSocketOnOpen
      this.websock.onerror = this.webSocketOnError
      this.websock.onmessage = this.webSocketOnMessage
    },
    webSocketOnOpen(e) {
      console.log('websocket 已經連接', e)
    },
    webSocketOnError(e) {
      this.$notify({
        title: 'WebSocket連接發生錯誤',
        type: 'error',
        duration: 0
      })
    },
    webSocketOnMessage(e) {
      const data = e.data
      this.$notify({
        title: '',
        message: data,
        type: 'success',
        dangerouslyUseHTMLString: true,
        duration: 5500
      })
      this.getMessageCount()
    }

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM