1.设计
我们采用的是个人号登录方式,这样拿不到我们的userInfo用户信息,然后我们将用户发来的消息(xml消息体)中的FromUser
作为我们唯一的openId
整体流程:
1
.用户扫码公众号码,然后发一条消息:验证码,我们就会通过api回复一个随机的码存入Redis中(主要结构是loginCode.随机码,value为openId)
2
.当用户输入后点击登录就进入我们的注册模块,同时关联角色和权限,实现网关的统一鉴权
3
.用户就可以根据个人的openId来维护个人信息
4
.用户登录成功后,返回token,前端所有亲亲贵带着token就可以访问了
2.代码
1.微信的callback接口,实现了对微信公众平台的回调验证功能,确保请求真实有效
在接收微信公众平台的回调请求时,该方法会对请求中的参数进行验证,确保这个请求是真实的,如果验证成功就会返回一个随机的字符串确保它的有效性;
/*** 1.回调验证(当前这个服务是否同样的)* 该代码实现了微信公众平台的回调验证功能。在接收到来自微信服务器的消息回调请求时,* 该方法会首先对请求中的参数进行签名验证,以确保请求是真实的。如果验证通过,* 则方法会返回一个随机字符串,作为确认该请求的有效性。如果验证失败,则方法会返回一个错误消息。* @param signature:微信加密签名* @param timestamp:时间戳作加密使用* @param nonce:随机数* @param echostr:随机字符串* @return:如果通过则返回随机字符串 echostr,否则unknown*/@GetMapping("callback")public String callback(@RequestParam("signature") String signature,@RequestParam("timestamp") String timestamp,@RequestParam("nonce") String nonce,@RequestParam("echostr") String echostr) {log.info("get验签请求参数:signature:{},timestamp:{},nonce:{},echostr:{}",signature, timestamp, nonce, echostr);String shaStr = SHA1.getSHA1(token, timestamp, nonce, "");if (signature.equals(shaStr)) { //判断生成的字符串签名与传入的签名是否一致return echostr;}return "unknown";}
确定请求有效性后,返回:
2.对于普通消息的处理:
**1.在我们接收到来自微信服务器的普通消息时,我们会将消息解析为XML
格式——>2.然后我们利用子当以的Util
将Xml
转为Map
集合,然后提取消息类型
和事件类型
,然后将消息类型和事件类型封装起来——>3.**根据类型确定一个消息处理器(WxChatMsgHandler对象
),我们的处理器会根据不同的类型
,生成对应的回复内容
/*** 2.普通消息的处理* 。在接收到来自微信服务器的普通消息时,该方法会将消息解析为 XML 格式,并从消息中提取出消息类型和事件类型(如果有)。* 根据消息类型和事件类型,该方法会选择一个适当的消息处理器(即 WxChatMsgHandler 对象),并将消息的具体内容传给该处理器进行处理。* 处理器会根据不同的消息类型和事件类型,生成相应的回复内容,并返回给微信服务器。* @param requestBody* @param signature* @param timestamp* @param nonce* @param msgSignature* @return*/@PostMapping(value = "callback", produces = "application/xml;charset=UTF-8")public String callback(@RequestBody String requestBody, // 接收到的原始 XML 消息内容@RequestParam("signature") String signature, // 签名串@RequestParam("timestamp") String timestamp, // 时间戳@RequestParam("nonce") String nonce, // 随机数@RequestParam(value = "msg_signature", required = false) String msgSignature // 消息签名) {// 打印接收到的消息内容log.info("接收到微信消息:requestBody:{}", requestBody);// 使用 MessageUtil 工具类将 XML 消息解析为 MapMap<String, String> messageMap = MessageUtil.parseXml(requestBody);// 获取消息类型和事件类型String msgType = messageMap.get("MsgType");String event = messageMap.get("Event") == null ? "" : messageMap.get("Event");log.info("msgType:{},event:{}", msgType, event);// 构造一个字符串,用于标识消息类型和事件类型StringBuilder sb = new StringBuilder();sb.append(msgType);if (!StringUtils.isEmpty(event)) {sb.append(".");sb.append(event);}// 根据消息类型和事件类型获取对应的处理器String msgTypeKey = sb.toString();WxChatMsgHandler wxChatMsgHandler = wxChatMsgFactory.getHandlerByMsgType(msgTypeKey);if (Objects.isNull(wxChatMsgHandler)) {return "unknown";}// 使用处理器处理消息,并生成回复内容String replyContent = wxChatMsgHandler.dealMsg(messageMap);log.info("replyContent:{}", replyContent);return replyContent;}
将XML转为Map的工具类:
https://blog.csdn.net/weixin_57128596/article/details/136136650?spm=1001.2014.3001.5501
根据类型确定处理器:
实现InitalLizingBean
,重写afterPropertiesSet方法
,将类型以及对应的处理器放入Map中
package com.wyh.wx.handler;import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Component;import javax.annotation.Resource;
import java.util.HashMap;
import java.util.List;
import java.util.Map;@Component
public class WxChatMsgFactory implements InitializingBean {@Resourceprivate List<WxChatMsgHandler> wxChatMsgHandlerList;private Map<WxChatMsgTypeEnum, WxChatMsgHandler> handlerMap = new HashMap<>();public WxChatMsgHandler getHandlerByMsgType(String msgType) {WxChatMsgTypeEnum msgTypeEnum = WxChatMsgTypeEnum.getByMsgType(msgType);return handlerMap.get(msgTypeEnum);}@Overridepublic void afterPropertiesSet() throws Exception {for (WxChatMsgHandler wxChatMsgHandler : wxChatMsgHandlerList) {handlerMap.put(wxChatMsgHandler.getMsgType(), wxChatMsgHandler);}}}
处理器WxChatMsgHandler根据具体类型响应消息:
package com.wyh.wx.handler;import com.wyh.wx.redis.RedisUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;import javax.annotation.Resource;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.TimeUnit;@Component
@Slf4j
public class ReceiveTextMsgHandler implements WxChatMsgHandler {private static final String KEY_WORD = "验证码";private static final String LOGIN_PREFIX = "loginCode";@Resourceprivate RedisUtil redisUtil;@Overridepublic WxChatMsgTypeEnum getMsgType() {return WxChatMsgTypeEnum.TEXT_MSG; // 处理文本消息}@Overridepublic String dealMsg(Map<String, String> messageMap) {log.info("接收到文本消息事件");String content = messageMap.get("Content"); // 文本消息内容if (!KEY_WORD.equals(content)) {return ""; // 如果不是“验证码”,则返回空字符串}String fromUserName = messageMap.get("FromUserName"); // 发送者的 OpenIDString toUserName = messageMap.get("ToUserName"); // 接收者的 OpenIDRandom random = new Random();int num = random.nextInt(9000) + 1000; // 生成一个 4 位数字的验证码String numKey = redisUtil.buildKey(LOGIN_PREFIX, String.valueOf(num)); // 验证码的键redisUtil.setNx(numKey, fromUserName, 5L, TimeUnit.MINUTES); // 将发送者的 OpenID 作为值,并将其存储在 Redis 中,有效期为 5 分钟String numContent = "您当前的验证码是:" + num + "! 5分钟内有效"; // 验证码的文本内容String replyContent = "<xml>\n" +" <ToUserName><![CDATA[" + fromUserName + "]]></ToUserName>\n" +" <FromUserName><![CDATA[" + toUserName + "]]></FromUserName>\n" +" <CreateTime>12345678</CreateTime>\n" +" <MsgType><![CDATA[text]]></MsgType>\n" +" <Content><![CDATA[" + numContent + "]]></Content>\n" +"</xml>"; // 回复的 XML 内容return replyContent; // 返回回复的 XML 内容}}
3.得到验证码后我们请求登录
流程:
在得到验证码后,我们利用验证码请求登录,将用户注册到数据库中并得到tokenValue
代码:
@RequestMapping("doLogin")public Result<SaTokenInfo> doLogin(@RequestParam("validCode") String validCode) {try {Preconditions.checkArgument(!StringUtils.isBlank(validCode), "验证码不能为空!");return Result.ok(authUserDomainService.doLogin(validCode));} catch (Exception e) {log.error("UserController.doLogin.error:{}", e.getMessage(), e);return Result.fail("用户登录失败");}}
业务Domain层:
1
:首先会根据我们的验证码
得到我们的唯一的openId
,然后将其封装到AuthUser
类中,进行注册(如果数据库中已经存在则直接返回true
),否则进行注册,关联角色权限
2
:注册完后,利用SaToken
进行登录loginId()
,然后获取token信息
3
:然后我们就可以通过tokenValue
去请求一些权限接口
/*** 4.登录1. 根据验证码生成登录键(loginKey)2. 从 Redis 中获取 openId,如果不存在则返回 null3. 如果 openId 不为空,则使用 AuthUserBO 对象封装用户信息,并调用 register 方法将其注册到系统中4. 使用 StpUtil.login 方法登录系统,并获取 Token 信息5. 返回 Token 信息* @param validCode* @return*/@Overridepublic SaTokenInfo doLogin(String validCode) {// 根据验证码生成登录键String loginKey = redisUtil.buildKey(LOGIN_PREFIX, validCode);// 从 Redis 中获取 openId,如果不存在则返回 nullString openId = redisUtil.get(loginKey);if (StringUtils.isBlank(openId)) {return null;}// 使用 AuthUserBO 对象封装用户信息,并调用 register 方法将其注册到系统中(new一个authUser封装openid去数据库查询)AuthUserBO authUserBO = new AuthUserBO();authUserBO.setUserName(openId);this.register(authUserBO);// 使用 StpUtil.login 方法登录系统,并获取 Token 信息StpUtil.login(openId);SaTokenInfo tokenInfo = StpUtil.getTokenInfo();// 返回 Token 信息return tokenInfo;}/*** 注册** @param authUserBO* @return*/@Override@SneakyThrows@Transactional(rollbackFor = Exception.class)public Boolean register(AuthUserBO authUserBO) {//校验用户是否存在,如果存在该用户,直接returnAuthUser existAuthUser = new AuthUser();existAuthUser.setUserName(authUserBO.getUserName());List<AuthUser> existUser = authUserService.queryByCondition(existAuthUser);if (existUser.size() > 0) {return true;}//1.Bo转entity类AuthUser authUser = AuthUserBOConverter.INSTANCE.convertBOToEntity(authUserBO);//2.如果密码不为空,则进行md+盐值加密(照顾微信登录)if(!StringUtils.isBlank(authUser.getPassword())){authUser.setPassword(SaSecureUtil.md5BySalt(authUser.getPassword(), salt));}//3.用户open状态authUser.setStatus(AuthUserStatusEnum.OPEN.getCode());authUser.setIsDeleted(IsDeletedFlagEnum.UN_DELETED.getCode());//4.插入用户数据Integer count = authUserService.insert(authUser);//5.建立一个角色与用户的关系AuthRole authRole = new AuthRole();authRole.setRoleKey(AuthConstant.NORMAL_USER);AuthRole roleResult = authRoleService.queryByCondition(authRole); //查询 普通用户 这一角色Long roleId = roleResult.getId();Long userId = authUser.getId();//5.将用户与角色的关系进行建立AuthUserRole authUserRole = new AuthUserRole();authUserRole.setUserId(userId);authUserRole.setRoleId(roleId);authUserRole.setIsDeleted(IsDeletedFlagEnum.UN_DELETED.getCode());authUserRoleService.insert(authUserRole);//4.auth业务中将用户角色以及对应的权限注入redis(UserName作为openId)String roleKey = redisUtil.buildKey(authRolePrefix, authUser.getUserName());LinkedList<AuthRole> roleList = new LinkedList<>();roleList.add(authRole);redisUtil.set(roleKey,new Gson().toJson(roleList));//4.2将权限注入RedisAuthRolePermission authRolePermission = new AuthRolePermission();authRolePermission.setRoleId(roleId); //新注册用户的角色idList<AuthRolePermission> authRolePermissionList = authRolePermissionService.queryByCondition(authRolePermission);List<Long> permissionIdList = authRolePermissionList.stream().map(AuthRolePermission::getPermissionId).collect(Collectors.toList()); //对应角色id的所有权限idsList<AuthPermission> permissionList = authPermissionService.queryByPermissionIds(permissionIdList);String permissionKey = redisUtil.buildKey(authPermissionPrefix, authUser.getUserName());redisUtil.set(permissionKey,new Gson().toJson(permissionList)); //将权限对应key和集合注入redis中return count > 0;}