微信扫码登陆+公众号关注即可+websocket+springboot3+vue3+无感登陆实现
·
先看效果:
我的门户地址 你可以亲自试一试
门户页面登陆效果
微信扫码登陆
已经关注的用户返回

##首次扫码关注的信息反馈
说明:
- 页面是用vue3+vite做的,在移动端展示效果也好
- 生成环境用的是wss协议
- 无论是已经关注还是首次关注的,扫码均可登陆
前提准备:
必要的
认证的服务号类型的公众号
非必要
自己的域名 域名证书 等 生产环境https页面用的是wss协议
代码部分:
后端
依赖 本人用的是3.2.5
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.5</version>
</parent>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.github.binarywang</groupId>
<artifactId>weixin-java-mp</artifactId>
<version>4.4.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
配置类
公众号账号配置
/**
* @author
*/
@Data
@ConfigurationProperties(prefix = "cc.wechat.mp")
public class WeChatMpProperties {
/**
* appId
*/
private String appId;
/**
* secret
*/
private String secret;
/**
* token
*/
private String token;
/**
* aesKey
*/
private String aesKey;
/**
* aesKey
*/
private String baseHost;
}
公众号配置
@AllArgsConstructor
@Configuration
@EnableConfigurationProperties(WeChatMpProperties.class)
public class WxMpConfiguration {
private final ScanHandler scanHandler;
private final WeChatMpProperties weChatMpProperties;
@Bean
public WxMpService wxMpService() {
WxMpService service = new WxMpServiceImpl();
service.setMultiConfigStorages(List.of(weChatMpProperties)
.stream().map(a -> {
WxMpDefaultConfigImpl configStorage;
configStorage = new WxMpDefaultConfigImpl();
configStorage.setAppId(a.getAppId());
configStorage.setSecret(a.getSecret());
configStorage.setToken(a.getToken());
configStorage.setAesKey(a.getAesKey());
return configStorage;
}).collect(Collectors.toMap(WxMpDefaultConfigImpl::getAppId, a -> a, (o, n) -> o)));
return service;
}
@Bean
public WxMpMessageRouter messageRouter(WxMpService wxMpService) {
final WxMpMessageRouter newRouter = new WxMpMessageRouter(wxMpService);
// 扫码事件
newRouter.rule().async(false).msgType(EVENT).event(SCAN).handler(scanHandler).end();
return newRouter;
}
}
扫码拦截器(本人没有用到,供参考)
@Slf4j
@Component
public class ScanHandler implements WxMpMessageHandler {
@Autowired
private SystemBaseProperties systemBaseProperties;
public static final String URL = "https://open.weixin.qq.com/connect/oauth2/authorize?appid=%s&redirect_uri=%s&response_type=code&scope=snsapi_userinfo&state=STATE#wechat_redirect";
public WxMpXmlOutMessage handle(WxMpXmlMessage wxMpXmlMessage, Map<String, Object> map,
WxMpService wxMpService, WxSessionManager wxSessionManager) throws WxErrorException {
String openId = wxMpXmlMessage.getFromUser();
Integer code;
log.info("handle openId:{}", openId);
try {
String eventKey = wxMpXmlMessage.getEventKey();
eventKey = eventKey.replace("qrscene_", "");
code = Integer.parseInt(eventKey);
} catch (Exception e) {
System.err.println("getEventKey error eventKey:{}" + wxMpXmlMessage.getEventKey() + e);
return null;
}
// 推送链接让用户授权
String authorizeUrl = String.format(URL, wxMpService.getWxMpConfigStorage().getAppId(), URLEncoder.encode(systemBaseProperties.getBaseHost() + "/api/open/wx/callBack"));
String content = "请点击登录:<a href=\"" + authorizeUrl + "\">登录</a>";
WxMpXmlOutTextMessage m = WxMpXmlOutMessage.TEXT().content(content)
.fromUser(wxMpXmlMessage.getToUser()).toUser(wxMpXmlMessage.getFromUser())
.build();
return m;
}
}
控制层
wx扫码登录
@GetMapping("/wx-scan-code")
@Operation(summary = "wx扫码登录")
public WxScanCodeRespVO wxcanode() throws WxErrorException {
String code = CodeUtil.getRandomCode(10);
WxMpQrCodeTicket wxMpQrCodeTicket = wxMpService.getQrcodeService().qrCodeCreateTmpTicket(code, 60);
String base64 = qrCodeService.generateQrCodeBase64(new GenerateQrCodeReqVO(wxMpQrCodeTicket.getUrl()));
return new WxScanCodeRespVO(base64, wxMpQrCodeTicket.getExpireSeconds(), code);
}
公众号鉴权监听控制层(这个要自己在官网配置,不多叙述)
@GetMapping(value = "/auth")
public void authGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
if (StringUtils.isNotBlank(request.getParameter("signature"))) {
String signature = request.getParameter("signature");
String timestamp = request.getParameter("timestamp");
String nonce = request.getParameter("nonce");
String echostr = request.getParameter("echostr");
log.info("signature[{}], timestamp[{}], nonce[{}], echostr[{}]", signature, timestamp, nonce, echostr);
if (SignUtil.checkSignature(signature, timestamp, nonce)) {
log.info("数据源为微信后台,将echostr[{}]返回!", echostr);
response.getOutputStream().println(echostr);
}
}
}
@PostMapping(value = "/auth")
public void authPost(HttpServletResponse response,
HttpServletRequest request,
@RequestBody String body) throws IOException {
log.info("body -> {}", body);
String sendMsg = wxService.processRequest(body);
response.setCharacterEncoding("UTF-8");
// 将sendMsg写入到response的body中
response.getWriter().print(sendMsg);;
}
业务层
QrCodeServiceImpl
@Slf4j
@Service
public class QrCodeServiceImpl implements IQrCodeService {
// 中间图片占二维码的最大比例,降低此值以提高扫描成功率
private static final double MAX_LOGO_RATIO = 0.8;
// 中间图片周围的白色边框比例
private static final double LOGO_PADDING_RATIO = 0.1;
// 强制使用最高容错级别
private static final ErrorCorrectionLevel ERROR_CORRECTION_LEVEL = ErrorCorrectionLevel.H;
@Autowired
private IRedisService redisService;
/**
* 生成带中间图片的二维码并返回Base64编码
*
* @param reqVO
* @return 二维码图片的Base64编码
*/
@Override
public String generateQrCodeBase64(GenerateQrCodeReqVO reqVO) {
try {
log.info("开始生成二维码,参数: {}", reqVO);
String hash = HashUtils.calculateSHA256FromToString(reqVO);
String redisKey = RedisPreKey.CACHE_QR_CODE + hash;
Boolean hasKey = redisService.hasKey(redisKey);
if (Boolean.TRUE.equals(hasKey)) {
return String.valueOf(redisService.get(redisKey));
}
// 配置二维码参数
QrConfig config = new QrConfig(reqVO.getWidth(), reqVO.getHeight());
config.setMargin(reqVO.getMargin());
config.setForeColor(reqVO.getForeColor());
config.setBackColor(reqVO.getBackgroundColor());
// 设置最高容错级别
config.setErrorCorrection(ERROR_CORRECTION_LEVEL);
BufferedImage qrImage = null;
if (StringUtils.isNotBlank(reqVO.getImageUrl())) {
// 下载中间图片
Image logoImage = downloadImage(reqVO.getImageUrl());
// 计算合适的中间图片尺寸
int logoWidth = (int) (reqVO.getWidth() * MAX_LOGO_RATIO);
int logoHeight = (int) (reqVO.getHeight() * MAX_LOGO_RATIO);
// 缩放图片
logoImage = logoImage.getScaledInstance(
logoWidth,
logoHeight,
Image.SCALE_SMOOTH
);
// 将图片转换为带白色边框的BufferedImage
BufferedImage logoWithPadding = addWhitePadding(
logoImage,
logoWidth,
logoHeight,
(int) (logoWidth * LOGO_PADDING_RATIO)
);
// 设置中间图片
config.setImg(logoWithPadding);
log.info("中间图片处理完成,尺寸: {}x{},带边框尺寸: {}x{}",
logoWidth, logoHeight,
logoWithPadding.getWidth(),
logoWithPadding.getHeight()
);
}
// 生成带中间图片的二维码
qrImage = QrCodeUtil.generate(reqVO.getContent(), config);
// 转换为Base64
String base64Result = imageToBase64(qrImage);
log.info("二维码生成成功,尺寸: {}x{}", qrImage.getWidth(), qrImage.getHeight());
redisService.set(redisKey, base64Result, 1, TimeUnit.DAYS);
return base64Result;
} catch (Exception e) {
log.error("生成二维码失败", e);
return null;
}
}
/**
* 下载远程图片并转换为Image对象
*/
private Image downloadImage(String imageUrl) throws IOException {
try (InputStream inputStream = HttpUtil.createGet(imageUrl).execute().bodyStream()) {
return ImageIO.read(inputStream);
}
}
/**
* 为图片添加白色边框
*/
private BufferedImage addWhitePadding(Image image, int width, int height, int padding) {
BufferedImage result = new BufferedImage(
width + padding * 2,
height + padding * 2,
BufferedImage.TYPE_INT_ARGB
);
Graphics2D g2d = result.createGraphics();
try {
// 填充白色背景
g2d.setColor(Color.WHITE);
g2d.fillRect(0, 0, result.getWidth(), result.getHeight());
// 绘制图片到中间
g2d.drawImage(
image,
padding,
padding,
width,
height,
null
);
} finally {
g2d.dispose();
}
return result;
}
/**
* 将图片转换为Base64编码
*/
private String imageToBase64(BufferedImage image) throws IOException {
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
ImageIO.write(image, "png", baos);
byte[] bytes = baos.toByteArray();
return Base64.encode(bytes);
}
}
}
WxServiceImpl
@Slf4j
@Service
public class WxServiceImpl implements IWxService {
@Autowired
private IChatService chatService;
@Autowired
private IUserService userService;
@Autowired
private MyEventPubLisher myEventPubLisher;
@Autowired
private IRedisService redisService;
@Autowired
private SystemBaseProperties systemBaseProperties;
public static final String URL = "https://open.weixin.qq.com/connect/oauth2/authorize?appid=%s&redirect_uri=%s&response_type=code&scope=snsapi_userinfo&state=STATE#wechat_redirect";
@Autowired
private WxMpService wxMpService;
@Override
public String processRequest(String requestBody) {
ReceivedWXMessage receivedWXMessage = XMLUtil.parseXML(requestBody, ReceivedWXMessage.class);
SendWXMessage sendWXMessage = BeanHelper.copyProperties(receivedWXMessage, SendWXMessage.class);
String question = receivedWXMessage.getContent();
String fromUserName = receivedWXMessage.getFromUserName();
String msgType = receivedWXMessage.getMsgType();
String event = receivedWXMessage.getEvent();
String eventKey = receivedWXMessage.getEventKey();
log.info("wx processRequest mgType -> {}, event -> {}, eventKey -> {}", msgType, event, eventKey);
if ("event".equals(msgType)) {
// 关注事件和扫码事件
if ("SCAN".equals(event)) {
// 先找到该用户
User user = userService.getUserByOpenId(fromUserName);
if (user == null) {
user = userService.addPortalUser(fromUserName);
}
log.info("user -> {}", user);
if (StringUtils.isNotBlank(eventKey) && eventKey.length() == 10) {
userService.wxScanCodeLogin(eventKey, user);
// 推送链接让用户授权
// String authorizeUrl = String.format(URL, wxMpService.getWxMpConfigStorage().getAppId(), URLEncoder.encode(systemBaseProperties.getBaseHost() + "/api/open/wx/callBack"));
// String content = "请点击登录:<a href=\"" + authorizeUrl + "\">登录</a>";
String content = "扫码登陆成功,请返回浏览器";
sendWXMessage.setContent(content);
sendWXMessage.setMsgType("text");
sendWXMessage.setCreateTime(System.currentTimeMillis() / 1000);
sendWXMessage.setToUserName(receivedWXMessage.getFromUserName());
sendWXMessage.setFromUserName(receivedWXMessage.getToUserName());
String xml = XMLUtil.genXML(sendWXMessage, "xml");
log.info("xml -> {}", xml);
return xml;
}
} else if ("subscribe".equals(event)) {
String content = "感谢你的关注";
if (StringUtils.isNotBlank(eventKey) && eventKey.startsWith("qrscene_")) {
// 扫码关注
String code = eventKey.replace("qrscene_", "");
User user = userService.getUserByOpenId(fromUserName);
if (user == null) {
user = userService.addPortalUser(fromUserName);
}
userService.wxScanCodeLogin(code, user);
content += ",扫码登陆成功,请返回浏览器";
}
sendWXMessage.setContent(content);
sendWXMessage.setMsgType("text");
sendWXMessage.setCreateTime(System.currentTimeMillis() / 1000);
sendWXMessage.setToUserName(receivedWXMessage.getFromUserName());
sendWXMessage.setFromUserName(receivedWXMessage.getToUserName());
String xml = XMLUtil.genXML(sendWXMessage, "xml");
log.info("xml -> {}", xml);
return xml;
} else if ("unsubscribe".equals(event)) {
log.warn("用户取消了关注 -> fromUserName:{}", fromUserName);
}
} else if ("text".equals(msgType)) {
String lastQuestionKey = String.format("cache:chat:lastQuestion:%s:%s", fromUserName, question.hashCode());
Boolean hasKey = redisService.hasKey(lastQuestionKey);
String call = null;
boolean dateTimeShow = false;
if (hasKey) {
call = redisService.get(lastQuestionKey).toString();
} else {
List<ChatMessageReqVO> messages = userService.getContext(receivedWXMessage.getFromUserName());
dateTimeShow = DtsUtil.dateTimeShow(messages);
messages.add(new ChatMessageReqVO(question, "user", dateTimeShow));
call = chatService.sendMessage(messages);
}
boolean success = StringUtils.isNotBlank(call);
// 去掉call中所有的换行
call = success ? call : "抱歉,我暂时无法回答你的问题。";
sendWXMessage.setCreateTime(System.currentTimeMillis() / 1000);
sendWXMessage.setContent(call);
sendWXMessage.setToUserName(receivedWXMessage.getFromUserName());
sendWXMessage.setFromUserName(receivedWXMessage.getToUserName());
String xml = XMLUtil.genXML(sendWXMessage, "xml");
log.info("xml -> {}", xml);
LifyMsgModel lifyMsgModel = new LifyMsgModel(receivedWXMessage.getFromUserName(), question, receivedWXMessage, sendWXMessage, dateTimeShow);
// 发布事件
myEventPubLisher.pushLifyMsgEvent(JSON.toJSONString(lifyMsgModel));
return xml;
}
return null;
}
}
POJO
@Data
@Schema(name = "微信扫码结果")
@AllArgsConstructor
public class WxScanCodeRespVO {
@Schema(name = "base64编码的图片")
private String base64Code;
@Schema(name = "过期时间")
private int expireSeconds;
@Schema(name = "二维码ID")
private String qrCodeId;
}
@Data
@NoArgsConstructor
public class WebSocketMessageModel<T> {
@NotBlank(message = "消息类型不能为空")
private String type;
private String message;
private T data;
public WebSocketMessageModel(WebSocketMessageTypeEnum webSocketMessageTypeEnum, String message) {
this.type = webSocketMessageTypeEnum.getType();
this.message = message;
}
public WebSocketMessageModel(WebSocketMessageTypeEnum webSocketMessageTypeEnum, T data) {
this.type = webSocketMessageTypeEnum.getType();
this.data = data;
this.message = "success";
}
}
@Getter
@Schema(name = "WebSocket消息类型", description = "WebSocket消息类型枚举")
public enum WebSocketMessageTypeEnum {
/**
* 全局通知
*/
GLOBAL_NOTIFICATION("globalNotification", "全局通知"),
/**
* 心跳
*/
BASE_HEARTBEAT("baseHeartbeat", "基础心跳"),
/**
* 我的照片点评数量
*/
MY_PHOTO_REVIEW_COUNTS("myPhotoReviewCounts", "我的照片点评数量"),
/**
* 微信扫二维码登录
*/
WX_SCAN_QR_CODE_LOGIN("wxScanQrCodeLogin", "微信扫二维码登录"),
;
private String type;
private String name;
WebSocketMessageTypeEnum(String type, String name) {
this.type = type;
this.name = name;
}
}
websocket相关
websocket处理器 我自己用的jwt 你替换自己的认证
@Slf4j
@Component
@SuppressWarnings("all")
public class WebSocketHandler extends TextWebSocketHandler {
@Autowired
private JwtProperties jwtProperties;
/**
* 使用ConcurrentHashMap存储所有在线会话
* key为sessionId,value为WebSocketSession
*/
private static final ConcurrentHashMap<String, WebSocketSession> sessions = new ConcurrentHashMap<>();
/**
* 存储用户和sessionIds的映射关系
*/
private static final ConcurrentHashMap<Long, List<String>> userMap = new ConcurrentHashMap<>();
/**
* 微信扫码登陆 qrCodeId和sessionId的映射关系
*/
private static final ConcurrentHashMap<String, String> wxScanQrCodeLoginMap = new ConcurrentHashMap<>();
/**
* 接收消息并处理
*
* @param session 会话
* @param textMessage 消息
* @throws Exception 异常
*/
@Override
protected void handleTextMessage(WebSocketSession session, TextMessage textMessage) throws Exception {
String payload = textMessage.getPayload();
String sessionId = session.getId();
WebSocketMessageModel wsmm = JSON.parseObject(payload, WebSocketMessageModel.class);
if (wsmm == null) {
log.error("收到了空白的WebSocketMessageModel-->{}", payload);
return;
}
String type = wsmm.getType();
Object message = wsmm.getMessage();
// 处理心跳消息
if (WebSocketMessageTypeEnum.BASE_HEARTBEAT.getType().equals(type)) {
if ("ping".equals(String.valueOf(message))) {
session.sendMessage(new TextMessage(JSON.toJSONString(WebSocketMessageModelEnum.PONG.getWebSocketMessageModel())));
return;
}
} else if (WebSocketMessageTypeEnum.WX_SCAN_QR_CODE_LOGIN.getType().equals(type)) {
String wxScanQrCodeLogin = String.valueOf(message);
wxScanQrCodeLoginMap.put(wxScanQrCodeLogin, sessionId);
return;
}
log.error("收到了无法识别的WebSocketMessageModel类型-->{}", payload);
}
/**
* 处理连接关闭
*
* @param session 会话
* @param status 关闭状态
* @throws Exception 异常
*/
@Override
public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
String sessionId = session.getId();
sessions.remove(sessionId);
// 从 userMap 中移除对应的会话 ID
userMap.forEach((userId, sessionIds) -> {
sessionIds.remove(sessionId);
if (sessionIds.isEmpty()) {
userMap.remove(userId);
}
});
// 从 wxScanQrCodeLoginMap 中移除对应的会话 ID
wxScanQrCodeLoginMap.entrySet().removeIf(entry -> entry.getValue().equals(sessionId));
broadcastOnlineCount();
log.info("连接关闭: " + sessionId + ",当前在线连接数: " + sessions.size() + ",当前在线用户数: " + userMap.size());
}
/**
* 处理传输错误
*
* @param session 会话
* @param exception 错误
* @throws Exception 异常
*/
@Override
public void handleTransportError(WebSocketSession session, Throwable exception) throws Exception {
String sessionId = session.getId();
log.error("传输错误发生在 session: " + sessionId + ", 错误: " + exception.getMessage());
if (session.isOpen()) {
session.close();
}
sessions.remove(sessionId);
broadcastOnlineCount();
}
/**
* 处理连接建立
*
* @param session 会话
* @throws Exception 异常
*/
@Override
public void afterConnectionEstablished(WebSocketSession session) throws Exception {
String sessionId = session.getId();
String authToken = extractAuthTokenFromUri(session.getUri().toString());
if (StringUtils.isNotBlank(authToken)) {
try {
Payload<UserInfo> payload = JwtUtils.getInfoFromToken(authToken, jwtProperties.getPublicKey(), UserInfo.class);
Long userId = payload.getUserInfo().getId();
if (userId != null) {
List<String> orDefault = userMap.computeIfAbsent(userId, k -> new CopyOnWriteArrayList<>());
orDefault.add(sessionId);
}
} catch (Exception e) {
log.error("WebSocket解析token失败-->{}", e.getMessage());
}
}
sessions.put(sessionId, session);
broadcastOnlineCount();
log.info("新连接建立: " + sessionId + ",当前在线连接数: " + sessions.size() + ",当前在线用户数: " + userMap.size());
}
/**
* 从URI中提取auth参数
*/
private String extractAuthTokenFromUri(String uri) {
int index = uri.indexOf("auth=");
if (index != -1) {
String param = uri.substring(index + "auth=".length());
// 处理可能的其他参数(如&符号)
return param.split("&")[0];
}
return null;
}
// 广播在线人数到所有客户端
private void broadcastOnlineCount() {
String message = "{\"type\":\"onlineCount\",\"count\":" + sessions.size() + "}";
TextMessage textMessage = new TextMessage(message);
sessions.forEach((sessionId, session) -> {
try {
if (session.isOpen()) {
session.sendMessage(textMessage);
}
} catch (IOException e) {
log.info("发送在线人数消息失败 to session: " + sessionId + ", 错误: " + e.getMessage());
try {
session.close();
} catch (IOException ex) {
// 忽略
}
sessions.remove(sessionId);
}
});
}
// 获取所有会话
public static ConcurrentHashMap<String, WebSocketSession> getSessions() {
return sessions;
}
// 获取所有用户和sessionIds的映射关系
public static ConcurrentHashMap<Long, List<String>> getUserMap() {
return userMap;
}
// 获取所有微信扫码登陆 qrCodeId和sessionId的映射关系
public static ConcurrentHashMap<String, String> getWxScanQrCodeLoginMap() {
return wxScanQrCodeLoginMap;
}
// 获取总体信息
public static JSONObject getGeneralInfo() {
return new JSONObject()
.set("sessionsSzie", getSessions().size())
.set("userMapSize", getUserMap().size())
.set("wxScanQrCodeLoginMapSize", getWxScanQrCodeLoginMap().size());
}
/**
* 广播消息到所有客户端
*
* @param message 要广播的消息
*/
public int broadcastMessage(WebSocketMessageModel message) {
TextMessage textMessage = new TextMessage(JSON.toJSONString(message));
AtomicInteger count = new AtomicInteger();
sessions.forEach((sessionId, session) -> {
try {
if (session.isOpen()) {
session.sendMessage(textMessage);
count.getAndIncrement();
}
} catch (IOException e) {
log.info("广播消息失败 to session: " + sessionId + ", 错误: " + e.getMessage());
try {
session.close();
} catch (IOException ex) {
log.error("关闭 session 失败: " + sessionId + ", 错误: " + ex.getMessage(), ex);
}
sessions.remove(sessionId);
}
});
return count.get();
}
/**
* 发送消息到指定会话
*
* @param sessionId 会话 ID
* @param message 要发送的消息
*/
public int sendMessageToSession(String sessionId, WebSocketMessageModel message) {
AtomicInteger count = new AtomicInteger();
WebSocketSession session = sessions.get(sessionId);
if (session != null && session.isOpen()) {
try {
TextMessage textMessage = new TextMessage(JSON.toJSONString(message));
session.sendMessage(textMessage);
count.getAndIncrement();
} catch (IOException e) {
log.info("发送消息失败 to session: " + sessionId + ", 错误: " + e.getMessage());
try {
session.close();
} catch (IOException ex) {
}
sessions.remove(sessionId);
}
}
return count.get();
}
public int sendMessageToUser(Long userId, WebSocketMessageModel message) {
AtomicInteger count = new AtomicInteger();
List<String> sessionIds = userMap.get(userId);
if (sessionIds != null) {
sessionIds.forEach(sessionId -> {
WebSocketSession session = sessions.get(sessionId);
if (session != null && session.isOpen()) {
try {
TextMessage textMessage = new TextMessage(JSON.toJSONString(message));
session.sendMessage(textMessage);
count.getAndIncrement();
} catch (IOException e) {
log.info("发送消息失败 to session: " + sessionId + ", 错误: " + e.getMessage());
try {
session.close();
} catch (IOException ex) {
}
sessions.remove(sessionId);
}
}
});
}
return count.get();
}
public int wxScanQrCodeLoginResult(String qrCodeId, WebSocketMessageModel message) {
AtomicInteger count = new AtomicInteger(0);
String sessionId = wxScanQrCodeLoginMap.get(qrCodeId);
if (sessionId != null) {
WebSocketSession session = sessions.get(sessionId);
if (session != null && session.isOpen()) {
try {
TextMessage textMessage = new TextMessage(JSON.toJSONString(message));
session.sendMessage(textMessage);
count.getAndIncrement();
} catch (IOException e) {
log.info("发送消息失败 to session: " + sessionId + ", 错误: " + e.getMessage());
try {
session.close();
} catch (IOException ex) {
}
sessions.remove(sessionId);
}
}
}
return count.get();
}
}
注册websocket
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Autowired
private WebSocketHandler webSocketHandler;
@Autowired
private HttpHandshakeInterceptor handshakeInterceptor;
public WebSocketConfig(WebSocketHandler webSocketHandler,
HttpHandshakeInterceptor handshakeInterceptor) {
this.webSocketHandler = webSocketHandler;
this.handshakeInterceptor = handshakeInterceptor;
}
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(webSocketHandler, "/ws")
.addInterceptors(handshakeInterceptor)
.setAllowedOrigins("*"); // 生产环境应设置具体域名
}
@Bean
public ServletServerContainerFactoryBean createWebSocketContainer() {
ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean();
container.setMaxTextMessageBufferSize(8192);
container.setMaxBinaryMessageBufferSize(8192);
container.setMaxSessionIdleTimeout(30 * 1000L); // 30秒空闲超时
return container;
}
}
前端
websocketservice.js 在全局配置里配置好自己的后端ws的host
import {ref, computed, onMounted, onUnmounted} from 'vue';
export default function useWebSocket() {
const AUTH_COOKIE_NAME = import.meta.env.VITE_APP_AUTH_COOKIE_NAME;
const WS_URL = import.meta.env.VITE_APP_WS_URL;
const ENV_PROD = import.meta.env.VITE_ENV_PROD;
const socket = ref(null);
const connectionStatus = ref('disconnected'); // disconnected, connecting, connected
const onlineCount = ref(0);
const errorMessage = ref('');
let heartbeatInterval = null;
let reconnectTimer = null;
const RECONNECT_DELAY = 5000; // 5秒后重连
const MAX_RECONNECT_ATTEMPTS = 5; // 最大重连次数
let reconnectAttempts = 0;
const onAuthSuccess = ref(null);
const globalNotification = ref(null);
const myPhotoReviewCounts = ref(null);
const baseHeartbeat = ref(null);
const wxScanQrCodeLogin = ref(null);
const PING = {
type: 'baseHeartbeat',
message: "ping"
};
const PONG = {
type: 'baseHeartbeat',
message: "pong"
}
// 消息处理器映射
const messageHandlers = {
onlineCount: (data) => {
onlineCount.value = data.message;
}
, onAuthSuccess: (data) => {
if (onAuthSuccess.value) {
onAuthSuccess.value(data.message);
}
}, globalNotification: (data) => {
if (globalNotification.value) {
globalNotification.value(data.message);
}
}, myPhotoReviewCounts: (data) => {
if (myPhotoReviewCounts.value) {
myPhotoReviewCounts.value(data.message);
}
}, baseHeartbeat: (data) => {
if (baseHeartbeat.value) {
baseHeartbeat.value(data.message);
}
}, wxScanQrCodeLogin: (data) => {
if (wxScanQrCodeLogin.value) {
wxScanQrCodeLogin.value(data.data);
}
}
};
// 连接到 WebSocket 服务器
const connect = () => {
if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
errorMessage.value = '达到最大重连次数,请检查网络或服务器状态';
return;
}
if (socket.value && (socket.value.readyState === WebSocket.CONNECTING || socket.value.readyState === WebSocket.OPEN)) {
return;
}
connectionStatus.value = 'connecting';
errorMessage.value = '';
try {
// 获取用户信息
let auth = localStorage.getItem(AUTH_COOKIE_NAME);
let wsUrl = WS_URL;
// 如果是null 改为''
if (auth === null) {
auth = '';
}
// 创建 WebSocket 连接,指向你的后端 8080 端口
socket.value = new WebSocket(`${wsUrl}?auth=${auth}`);
// 连接建立成功
socket.value.onopen = () => {
connectionStatus.value = 'connected';
console.log('WebSocket 连接已建立');
reconnectAttempts = 0; // 连接成功,重置重连次数
// 启动心跳机制
startHeartbeat();
// 清除重连计时器
if (reconnectTimer) {
clearTimeout(reconnectTimer);
reconnectTimer = null;
}
};
// 接收到消息
socket.value.onmessage = (event) => {
try {
const edata = JSON.parse(event.data);
const {type, message, data} = edata;
console.log('收到消息:', data);
if (messageHandlers[type]) {
messageHandlers[type]({type, message,data});
} else {
console.log('收到未知类型消息:', edata);
}
} catch (error) {
console.log('收到非 JSON 消息:', event.data);
}
};
// 连接关闭
socket.value.onclose = (event) => {
connectionStatus.value = 'disconnected';
console.log(`WebSocket 连接已关闭,代码: ${event.code}, 原因: ${event.reason}`);
// 停止心跳
stopHeartbeat();
// 自动重连
reconnectAttempts++;
if (reconnectTimer === null) {
reconnectTimer = setTimeout(connect, RECONNECT_DELAY);
}
};
// 发生错误
socket.value.onerror = (error) => {
connectionStatus.value = 'disconnected';
errorMessage.value = `WebSocket 错误: ${error.message}`;
console.error('WebSocket 错误:', error);
// 自动重连
reconnectAttempts++;
if (reconnectTimer === null) {
reconnectTimer = setTimeout(connect, RECONNECT_DELAY);
}
};
} catch (error) {
connectionStatus.value = 'disconnected';
errorMessage.value = `创建 WebSocket 失败: ${error.message}`;
console.error('创建 WebSocket 失败:', error);
// 自动重连
reconnectAttempts++;
if (reconnectTimer === null) {
reconnectTimer = setTimeout(connect, RECONNECT_DELAY);
}
}
};
// 启动心跳机制
const startHeartbeat = () => {
// 每25秒发送一次心跳,比服务器的超时时间短
heartbeatInterval = setInterval(() => {
if (socket.value && socket.value.readyState === WebSocket.OPEN) {
socket.value.send(JSON.stringify(PING));
console.log('发送心跳');
} else {
console.log('无法发送心跳,连接未打开');
}
}, 25000);
};
// 停止心跳机制
const stopHeartbeat = () => {
if (heartbeatInterval) {
clearInterval(heartbeatInterval);
heartbeatInterval = null;
}
};
// 手动断开连接
const disconnect = () => {
if (socket.value) {
stopHeartbeat();
// 清除重连计时器
if (reconnectTimer) {
clearTimeout(reconnectTimer);
reconnectTimer = null;
}
socket.value.close();
socket.value = null;
connectionStatus.value = 'disconnected';
console.log('WebSocket 连接已手动关闭');
}
};
// 发送消息到服务器
const sendMessage = (message) => {
if (socket.value && socket.value.readyState === WebSocket.OPEN) {
socket.value.send(JSON.stringify(message));
console.log('发送消息:', message);
} else {
console.error('无法发送消息,连接未打开');
errorMessage.value = '无法发送消息,连接未打开';
}
};
// 计算属性:连接是否活跃
const isConnected = computed(() => {
return connectionStatus.value === 'connected';
});
// 生命周期钩子:组件挂载时连接
onMounted(() => {
connect();
});
// 生命周期钩子:组件卸载时断开连接
onUnmounted(() => {
disconnect();
});
return {
connectionStatus,
onlineCount,
errorMessage,
isConnected,
connect,
disconnect,
sendMessage,
onAuthSuccess,
wxScanQrCodeLogin,
globalNotification,
myPhotoReviewCounts
};
}
在登陆页面用websocket回调
// 扫码登录回调处理
wxScanQrCodeLogin.value = (res) => {
handleLoginSuccess(res);
};
更多推荐


所有评论(0)