第一步:配置Spring
<!--spring websocket库--> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>${spring.context.version}</version> </dependency>
第二步:配置WebSocket
使用Configurer类和 Annotation来进行WebSocket配置。
首先要创建一个类,继承WebSocketMessageBrokerConfigurer,并且在类上加上annotation:@Configuration和@EnableWebSocketMessageBroker。这样,Spring就会将这个类当做配置类,并且打开WebSocket。
import org.springframework.context.annotation.Configuration; import org.springframework.messaging.simp.config.MessageBrokerRegistry; import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; import org.springframework.web.socket.config.annotation.StompEndpointRegistry; import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer; @Configuration @EnableWebSocketMessageBroker public class WebSocketConfig implements WebSocketMessageBrokerConfigurer{ @Override public void registerStompEndpoints(StompEndpointRegistry registry) { //添加这个Endpoint,这样在网页中就可以通过websocket连接上服务了 registry.addEndpoint("/ws").withSockJS(); } @Override public void configureMessageBroker(MessageBrokerRegistry config) { System.out.println("服务器启动成功"); //这里设置的simple broker是指可以订阅的地址,也就是服务器可以发送的地址 /** * user 用于用户聊天 */ config.enableSimpleBroker("/topic","/user"); config.setApplicationDestinationPrefixes("/app"); } }
第一个方法,是registerStompEndpoints,大意就是注册消息连接点(我自己的理解),所以我们进行了连接点的注册:
registry.addEndpoint("/ws").withSockJS();
我们加了一个叫coordination的连接点,在网页上我们就可以通过这个链接来和服务器的WebSocket连接了。但是后面还有一句withSockJs,这是什么呢?SockJs是一个WebSocket的通信js库,Spring对这个js库进行了后台的自动支持,也就是说,我们如果使用SockJs,那么我们就不需要对后台进行更多的配置,只需要加上这一句就可以了。
第二个方法,configureMessageBroker,大意是设置消息代理,也就是页面上用js来订阅的地址,也是我们服务器往WebSocket端接收js端发送消息的地址。
config.enableSimpleBroker("/user");
config.setApplicationDestinationPrefixes("/app");
首先,定义了一个连接点叫user,从名字可以看的出,最后我会做一个聊天的例子。然后,设置了一个应用程序访问地址的前缀,目的估计是为了和其他的普通请求区分开吧。也就是说,网页上要发送消息到服务器上的地址是/app/user。
后端实现类:
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@Api(tags = "WebSocket控制器") @ApiSupport(author = "abc", order = 10) @RestController public class WebSocketController { @Resource private SimpMessagingTemplate template; @Resource private AuthService authService; @Resource private CommonCacheOperator commonCacheOperator; public static final String WS_CACHE_ALL_KEY = "ws:"; /** * 发送信息 * * @author abc * @date 2024年10月10日10:36:00 **/ @ApiOperationSupport(order = 1) @ApiOperation("发送信息") @GetMapping("/app/send") public CommonResult<String> send(@ApiParam(value="用户Id", required = true) @RequestParam String userId, @ApiParam(value="发送消息message:(0='取消确认,1='扫码成功,2=小程序确认登录')", required = true) @RequestParam String message) { String qrCodeLink =CommonCryptogramUtil.doSm4CbcDecrypt(userId); Object obj=commonCacheOperator.get(WS_CACHE_ALL_KEY+userId); if(ObjectUtil.isEmpty(obj)) { return CommonResult.error("二维码已失效"); } if(message.trim().equals("0")) { template.convertAndSendToUser(userId, "/token", CommonResult.data(false)); } else if(message.trim().equals("1")) { template.convertAndSendToUser(userId, "/token", CommonResult.data(true)); } else if(message.trim().equals("2")) { String token= authService.doLoginById(StpLoginUserUtil.getLoginUser().getId(), AuthDeviceTypeEnum.PC.getValue(), SaClientTypeEnum.B.getValue()); template.convertAndSendToUser(userId, "/token", CommonResult.data(token)); commonCacheOperator.remove(WS_CACHE_ALL_KEY+userId); } return CommonResult.ok(); } /** * 订阅信息 * * @author abc * @date 2024年10月10日10:36:12 **/ @ApiOperationSupport(order = 2) @ApiOperation("订阅信息") @GetMapping("/user/{userId}/token") public CommonResult<String> user(@PathVariable String userId) { return CommonResult.ok(); } /** * 获取二维码 * * @author abc * @date 2024年10月10日10:36:12 **/ @ApiOperationSupport(order = 3) @ApiOperation("获取二维码") @CommonNoRepeat @GetMapping("/app/getQRCode") public CommonResult<Map<String,String>> getQRCode(@ApiParam(value="时间戳", required = true) @RequestParam String tiem) { Map<String,String> map=new HashMap<String, String>(); String randomString = RandomUtil.randomString(11); String qrCodeLink =CommonCryptogramUtil.doSm4CbcEncrypt(randomString); map.put("str", qrCodeLink); // 二维码宽度 int qrCodeWidth = 190; // 二维码高度 int qrCodeHeight = 190; try { commonCacheOperator.put(WS_CACHE_ALL_KEY+qrCodeLink, qrCodeLink, 5 * 60); BufferedImage qrCodeImage = QRCodeUtil.getQrCodeImage(qrCodeLink, qrCodeWidth, qrCodeHeight); String base64QrCode = QRCodeUtil.convertImageToBase64(qrCodeImage); map.put("img", base64QrCode); } catch (WriterException e) { e.printStackTrace(); } return CommonResult.data(map); } @MessageMapping("/send") // 映射客户端发送的消息 public void greeting(String message) throws Exception { System.out.println(message); //template.convertAndSendToUser("1", "/token", message); commonCacheOperator.put(WS_CACHE_ALL_KEY+message, message, 5 * 60); } }
template.convertAndSendToUser(username, destination, message) API。
它接受一个字符串用户名(客户端即前端创建随机码UUID),这意味着如果我们以某种方式为每个连接设置唯一的用户名,我们应该能够发送消息给订阅主题的特定用户。
第三步:配置Web端
首先我们要使用两个js库,一个是之前说过的SockJs,一个是stomp,这是一种通信协议,暂时不介绍它,只需要知道是一种更方便更安全的发送消息的库就行了。
需要连接服务端的WebSocket:
var socket = new SockJS('/ws'); var stompClient = Stomp.over(socket); stompClient.connect('', '', function (frame) {});
没错,就只需要两句话。有了这三句话,我们就已经可以连接上了服务器。
使用SockJs还有一个好处,那就是对浏览器进行兼容,如果是IE11以下等对WebSocket支持不好的浏览器,SockJs会自动的将WebSocket降级到轮询(这个不知道的可以去百度一下),之前也说了,Spring对SockJs也进行了支持,也就是说,如果之前加了withSockJs那句代码,那么服务器也会自动的降级为轮询。(怎么样,是不是很兴奋,Spring这个特性太让人舒服了)
Vue3 源码:
<template> <div class="loginmain"> <div class="loginbox relative"> <div class="absolute top-13 right-2 w-15 h-15 bg-cyan-100 QRCode" v-if="isShowQRCode" @click="handleSwitch" ></div> <div class="absolute top-13 right-2 w-15 h-15 bg-cyan-100 password" v-else @click="handleSwitch" ></div> <span class="logo"><img src="@/assets/images/logo.png" style="height: 35px" /></span> <h2 style="margin-bottom: 0"><b>平台</b> </h2> <LoginForm v-if="isShowQRCode" /> <div class="QRCodeCon" v-else> <div> <div class="w-full h-65.8 bg-light-50 flex justify-center items-center" v-if="showStatus == 1" > <!-- <img src="/static/images/组5196.png" class="w-50 h-50" /> --> <QrCode :value="qrCodeUrl" class="enter-x flex justify-center xl:justify-start" :width="240" /> <div class="w-full h-50% z-100 bg-light-50/50 absolute flex justify-center items-center" v-if="timeoutStatus" > <RedoOutlined style="color: #000; font-size: 50px; font-weight: 1000; cursor: pointer" @click="handleRefresh" /> </div> </div> <div class="w-full h-65.8 bg-light-50 flex flex-col items-center justify-center" v-if="showStatus == 2" > <div class="mb-2"> <CheckCircleOutlined :style="{ fontSize: '50px', color: '#1890ff' }" /> </div> <div class="text-[22px] mb-1">扫描成功!</div> <div class="text-[#9d9da7] text-[16px]">请在手机上根据提示确认登录!</div> </div> <div class="w-full h-65.8 bg-light-50 flex flex-col items-center justify-center" v-if="showStatus == 3" > <div class="mb-2"> <CheckCircleOutlined :style="{ fontSize: '50px', color: '#9eba20' }" /> </div> <div class="text-[22px] mb-1">登录成功!</div> </div> <div class="w-full h-65.8 bg-light-50 flex flex-col items-center justify-center" v-if="showStatus == 4" > <div class="mb-2"> <CloseCircleOutlined :style="{ fontSize: '50px', color: '#f80000' }" /> </div> <div class="text-[22px] mb-1">手机已拒绝登录!</div> </div> </div> </div> </div> <div style="clear: both"></div> <div class="bottom" style="position: fixed; bottom: 0; width: 100%; height: 80px"> <div class="main-bottom-wrapper"> <a style="color: rgb(160 160 160); text-decoration: none" href="https://beian.miit.gov.cn" target="_blank" > XXXXXXXXXXXXXXXXXXXXX</a > <br /> <img src="/static/images/gtimg.png" class="home-icon" /> <a style="color: rgb(160 160 160); text-decoration: none" href="https://beian.mps.gov.cn/#/query/webSearch?code=0000" rel="noreferrer" target="_blank" >XXXXXXXXXXXXXXXXX</a > </div> </div> </div> <!-- <div class="bottom" style="height: 60px; text-align: center"> <div class="container"> <a href="https://beian.miit.gov.cn/" target="_blank" style="text-decoration: none; color: #49e" >xxxxxx</a > </div> </div> --> </template> <script lang="ts" setup> import { ref, onMounted, onUnmounted, computed } from 'vue'; import { AppLogo, AppLocalePicker, AppDarkModeToggle } from '@/components/Application'; import LoginForm from './LoginForm.vue'; import ForgetPasswordForm from './ForgetPasswordForm.vue'; import RegisterForm from './RegisterForm.vue'; import MobileForm from './MobileForm.vue'; import QrCodeForm from './QrCodeForm.vue'; import { useGlobSetting } from '@/hooks/setting'; import { useI18n } from '@/hooks/web/useI18n'; import { useDesign } from '@/hooks/web/useDesign'; import { useLocaleStore } from '@/store/modules/locale'; import { CheckCircleOutlined, CloseCircleOutlined, RedoOutlined } from '@ant-design/icons-vue'; import moment from 'moment'; import { QrCode } from '@/components/Qrcode'; import SockJS from 'sockjs-client/dist/sockjs.min.js'; import Stomp from 'stompjs'; import { useUserStore } from '@/store/modules/user'; import { router } from '@/router'; const userStore = useUserStore(); defineProps({ sessionTimeout: { type: Boolean, }, }); const globSetting = useGlobSetting(); const { prefixCls } = useDesign('login'); const { t } = useI18n(); const localeStore = useLocaleStore(); const showLocale = localeStore.getShowPicker; const title = computed(() => globSetting?.title ?? ''); const isShowQRCode = ref(true); const handleSwitch = () => { isShowQRCode.value = !isShowQRCode.value; if (!isShowQRCode.value) { handleShowCode(); countdown(countdownNum.value); } showStatus.value = 1; }; const qrCodeUrl = ref(''); const handleShowCode = () => { const generateRandomString = () => { const characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; let randomString = ''; for (let i = 0; i < 10; i++) { const randomIndex = Math.floor(Math.random() * characters.length); randomString += characters.charAt(randomIndex); } return randomString; }; qrCodeUrl.value = generateRandomString(); webSocketJS(); }; const stompClient = ref(null); const showStatus = ref(1); const webSocketJS = () => { let socket = new SockJS('/api/ws'); stompClient.value = Stomp.over(socket); stompClient.value.heartbeat.outgoing = 10000; stompClient.value.heartbeat.incoming = 0; //去掉debug打印 // stompClient.value.debug = null; //开始连接 stompClient.value.connect( {}, (frame) => { console.log('Connected:' + frame); console.info('[WebSocket] 连接请求发送成功!'); //进行订阅服务 //***连接 stompClient.value.subscribe(`/user/${qrCodeUrl.value}/token`, (message) => { const { body } = message; if (body) { const _body = JSON.parse(body); const { data } = _body; if (data.toString() == 'true') { showStatus.value = 2; } if (data.toString().length > 10) { showStatus.value = 3; userStore.setToken(data); router.push('/index/index'); } } }); stompClient.value.send('/app/send', {}, qrCodeUrl.value, (res) => { console.log(res); }); }, () => { // 断开连接 console.log('连接请求发送失败'); //一连接请求发送失败就关闭 // this.buttonStatus = true; }, ); }; const timeoutStatus = ref(false); const intervalId = ref(null); const countdown = (durationInSeconds) => { let remainingTime = durationInSeconds; intervalId.value = setInterval(() => { const minutes = Math.floor(remainingTime / 60); const seconds = remainingTime % 60; console.log(`${minutes}:${seconds < 10 ? '0' : ''}${seconds}`); if (remainingTime === 0) { clearInterval(intervalId.value); timeoutStatus.value = true; } else { remainingTime--; } }, 1000); }; const handleRefresh = () => { timeoutStatus.value = false; countdown(countdownNum.value); handleShowCode(); clearInterval(intervalId.value); }; const countdownNum = ref(60); onUnmounted(() => { stompClient.value.disconnect(); clearInterval(intervalId.value); }); </script> <style lang="less"> @import 'login'; </style> <style lang="less" scoped> .main-bottom-wrapper { bottom: 0; width: 370px; height: 40px; margin: 0 auto; margin-top: 15px; padding-bottom: 15px; text-align: center; } .home-icon { display: inline-block; width: 20px; height: 20px; vertical-align: text-bottom; } .QRCode { background: url('/static/images/组5196.png') no-repeat center center; background-size: 100% 100%; cursor: pointer; } .password { background: url('/static/images/组5197.png') no-repeat center center; background-size: 100% 100%; cursor: pointer; } .loginbox { border-radius: 0; } </style>