第一步:配置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>
浙公网安备 33010602011771号