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 消息解析为 Map
Map<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 {
@Resource
private List<WxChatMsgHandler> wxChatMsgHandlerList;
private Map<WxChatMsgTypeEnum, WxChatMsgHandler> handlerMap = new HashMap<>();
public WxChatMsgHandler getHandlerByMsgType(String msgType) {
WxChatMsgTypeEnum msgTypeEnum = WxChatMsgTypeEnum.getByMsgType(msgType);
return handlerMap.get(msgTypeEnum);
}
@Override
public 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";
@Resource
private RedisUtil redisUtil;
@Override
public WxChatMsgTypeEnum getMsgType() {
return WxChatMsgTypeEnum.TEXT_MSG; // 处理文本消息
}
@Override
public String dealMsg(Map<String, String> messageMap) {
log.info("接收到文本消息事件");
String content = messageMap.get("Content"); // 文本消息内容
if (!KEY_WORD.equals(content)) {
return ""; // 如果不是“验证码”,则返回空字符串
}
String fromUserName = messageMap.get("FromUserName"); // 发送者的 OpenID
String toUserName = messageMap.get("ToUserName"); // 接收者的 OpenID
Random 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,如果不存在则返回 null
3. 如果 openId 不为空,则使用 AuthUserBO 对象封装用户信息,并调用 register 方法将其注册到系统中
4. 使用 StpUtil.login 方法登录系统,并获取 Token 信息
5. 返回 Token 信息
* @param validCode
* @return
*/
@Override
public SaTokenInfo doLogin(String validCode) {
// 根据验证码生成登录键
String loginKey = redisUtil.buildKey(LOGIN_PREFIX, validCode);
// 从 Redis 中获取 openId,如果不存在则返回 null
String 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) {
//校验用户是否存在,如果存在该用户,直接return
AuthUser 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将权限注入Redis
AuthRolePermission authRolePermission = new AuthRolePermission();
authRolePermission.setRoleId(roleId); //新注册用户的角色id
List<AuthRolePermission> authRolePermissionList = authRolePermissionService.queryByCondition(authRolePermission);
List<Long> permissionIdList = authRolePermissionList.stream()
.map(AuthRolePermission::getPermissionId)
.collect(Collectors.toList()); //对应角色id的所有权限ids
List<AuthPermission> permissionList = authPermissionService.queryByPermissionIds(permissionIdList);
String permissionKey = redisUtil.buildKey(authPermissionPrefix, authUser.getUserName());
redisUtil.set(permissionKey,new Gson().toJson(permissionList)); //将权限对应key和集合注入redis中
return count > 0;
}