今天实现一下与微信公众号进行对接,通过扫描二维码的方式来进行注册与登录,获取用户的微信唯一标识作为用户的username,下面我们开始编写。
骨架建立:
建包:
第一步还是先将骨架建好,与网关骨架差不多,我们需要的东西不多,如图:
controller:接收微信请求
handler:进行微信信息处理
redis:redis的工具类和配置文件
utils:一些工具类
依赖引入:
本次依赖如下:每个依赖我都写好注释了
<dependencies>
<!-- 启动依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.4.2</version>
<exclusions>
<exclusion>
<artifactId>spring-boot-starter-logging</artifactId>
<groupId>org.springframework.boot</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.22</version>
</dependency>
<!-- 日志依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
<version>2.4.2</version>
</dependency>
<!-- xml解析器-->
<dependency>
<groupId>org.dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>2.1.3</version>
</dependency>
<!-- xml和Java对象的序列化-->
<dependency>
<groupId>com.thoughtworks.xstream</groupId>
<artifactId>xstream</artifactId>
<version>1.4.18</version>
</dependency>
<!-- json序列化-->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.12.7</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.12.7</version>
</dependency>
<!-- json序列化工具-->
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.6</version>
</dependency>
<!-- 操作Redis的依赖库-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.4.2</version>
</dependency>
<!-- 对象池-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.9.0</version>
</dependency>
</dependencies>
配置文件:
没什么好说的,不懂大家问问ai,很简单
server:
port: 1500
spring:
# redis配置
redis:
# Redis数据库索引(默认为0)
database: 1
# Redis服务器地址
host: 127.0.0.1
# Redis服务器连接端口
port: 6379
# Redis服务器连接密码(默认为空)
password:
# 连接超时时间
timeout: 2s
lettuce:
pool:
# 连接池最大连接数
max-active: 200
# 连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: -1ms
# 连接池中的最大空闲连接
max-idle: 10
# 连接池中的最小空闲连接
min-idle: 0
微信打通初测试:
阅读文档:
接入概述 | 微信开放文档
第一步:
根据文档的要求,我们第一步需要配置服务器。点击箭头位置进入接口调试工具
NATAPP-内网穿透 基于ngrok的国内高速内网映射工具
这是我使用的内网穿透工具,大家可以自行选择,因为微信公众号的对接需要443端口,我们需要内网穿透 。进入后购买免费的就可以了
修改你的地址和端口,端口为你服务的端口号 ,并保存你的authtoken
链接:https://pan.baidu.com/s/1Sr13Bea82z4oO_AbpDrKDA?pwd=1111
提取码:1111
下载它的客户端,解压后是一个exe文件,我们双击打开,输入命令。authtoken换成你自己的
start natapp.exe -authtoken (authtoken)
成功会会出现以下界面,我们将Forwarding后的地址复制到测试号的地址中
如下图所示:
第二步:
阅读文档发现我们需要一个方法接收微信服务器的访问,微信服务器会携带四个参数,我们需要将刚刚定义的token,与其携带的timestamo,nonce进行字典排序,并将其进行sha1加密,将加密后的字符串与signature参数进行比较,如果成功了,就证明这是来自微信的请求
下面我们在controller中实现这个逻辑,sha1加密是一个固定的写法,我这里不去细讲了,大家知道它进行了一次加密就好,在结尾我粘贴出来,文章里我不粘贴工具类了
CallBackController:
package com.yizhiliulianta.wx.controller;
import com.yizhiliulianta.wx.utils.SHA1;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
@Slf4j
public class CallBackController {
private static final String token = "yizhiliulianta";
@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";
}
}
大家可以阅读一下代码,和刚刚的过程是一样的,接收参数,加密,判断是否合法,最后返回,如果不合法返回unknown。
下面我们用aippost测试一下该方法是否成功:
成功了!我们继续
第三步:
我们将刚刚的方法修改为post请求,新增一个就好,并多添加一个参数为requestBody,因为我们在进行扫码成功时,微信会给我们返回一个请求体,为xml格式,并且我们需要将我们的返回格式也设置为xml格式,而且我们需要将echostr变为不必须的,不然会报错,代码如下:
@PostMapping(value = "callback", produces = "application/xml;charset=UTF-8")
public String callback(
@RequestBody String requestBody,
@RequestParam("signature") String signature,
@RequestParam("timestamp") String timestamp,
@RequestParam("nonce") String nonce,
@RequestParam(value = "echostr",required = false) String echostr) {
log.info("接收到微信的请求:requestBody:{},signature:{},timestamp:{},nonce:{},echostr:{}"
,requestBody,signature,timestamp,nonce,echostr);
return "unknown";
}
下面我们测试一下,首先,我们在测试号的网页下,会有一个二维码,这里我就不截图了,当大家配置好网址,token和端口后,我们扫当前的二维码,并且关注,我们在后台会收到如下信息:
我来分析一下都是什么,大家也可以看文档,都是有介绍的
ToUserName | 开发者微信号(本次服务的标识) |
FromUserName | 发送方账号(一个OpenID)(也就是说,谁关注了我们) |
CreateTime | 消息创建时间 |
MsgType | 消息类型 |
Event | 事件类型,subscribe(订阅)、unsubscribe(取消订阅)(证明该用户进行了关注动作) |
下面当我们给该公众号发送一则消息,响应的内容如下:
唯一改变的是消息的类型,变成了text,Event变成了Content,并包含着本次发的内容:你好
第四步:
当我们接收到用户的消息,我们需要对其进行回复,因为我们接收的格式为xml,所以我们返回也需要xml。这里我们看一下文档,发现需要返回的内容如下:
我们需要知道发送方是谁,接收方是谁,因为我们给用户返回,所以发送方变成后台程序,而接收方变成了用户。
我们清楚了这个逻辑后,只需要将用户关注后,微信给我们推送的xml信息解析然后编写一个xml返回就好了
这里我使用一个工具类,还是贴在最后,重要的是逻辑和如何对接,代码如下:
@PostMapping(value = "callback", produces = "application/xml;charset=UTF-8")
public String callback(
@RequestBody String requestBody,
@RequestParam("signature") String signature,
@RequestParam("timestamp") String timestamp,
@RequestParam("nonce") String nonce,
@RequestParam(value = "echostr",required = false) String echostr) {
log.info("接收到微信的请求:requestBody:{},signature:{},timestamp:{},nonce:{},echostr:{}"
,requestBody,signature,timestamp,nonce,echostr);
Map<String, String> msgMap = MessageUtil.parseXml(requestBody);
String toUserName = msgMap.get("ToUserName");
String fromUserName = msgMap.get("FromUserName");
String mag = "<xml>\n" +
" <ToUserName><![CDATA["+fromUserName+"]]></ToUserName>\n" +
" <FromUserName><![CDATA["+toUserName+"]]></FromUserName>\n" +
" <CreateTime>12345678</CreateTime>\n" +
" <MsgType><![CDATA[text]]></MsgType>\n" +
" <Content><![CDATA[你好,我是一支榴莲挞]]></Content>\n" +
"</xml>\n";
return mag;
}
我们再来测试一下,给公众号发送一个消息,如图:
成功!到这里对接就已经成功了
思考:
我们到这里打通了微信的对接,我们对接后需要什么?
一:用户关注后,我们需要给其反馈
二:我们需要获取用户的唯一标识作为用户名
三:我们需要根据用户不同的动作和发的信息做出不同的回应
我们目前实现了一,二也很简单,下面我们需要实现三,我的想法是用户发送验证码,我们给其一个随机的4位数,并将用户的唯一标识与验证码放入到redis里,然后用户进入auth服务,实现登录,需要输入刚刚我们发送的验证码,接着,我们在redis里根据用户发送的验证码进行查询,如果查询到了,登录成功,否则登录失败,这样我们就实现了整个登录的实现。
微信打通功能优化与完善:
本次我们使用工厂模式来进行解耦:
因为用户至少有两个动作,一个是关注的动作,一个是发送消息的动作。
枚举:
首先我们定义动作的枚举:
一个是用户关注事件,一个是消息事件
package com.yizhiliulianta.wx.handler;
public enum WxChatMsgTypeEnum {
SUBSCRIPE("event.subscribe","用户关注事件"),
TEXT_MSG("text","接收用户文本消息");
private String msgType;
private String desc;
WxChatMsgTypeEnum(String msgType, String desc) {
this.msgType = msgType;
this.desc = desc;
}
public static WxChatMsgTypeEnum getByMsgType(String msgType){
for (WxChatMsgTypeEnum wxChatMsgTypeEnum : WxChatMsgTypeEnum.values()){
if (wxChatMsgTypeEnum.msgType.equals(msgType)){
return wxChatMsgTypeEnum;
}
}
return null;
}
}
工厂设计:
接口规范:
首先我们需要一个接口规范,告诉每个实现类,需要执行什么方法
一:我需要知道你是干什么的,哪个实现类
二:处理微信的请求
代码如下:
package com.yizhiliulianta.wx.handler;
import java.util.Map;
public interface WxChatMsgHandler {
WxChatMsgTypeEnum getMsgType();
String dealMsg(Map<String, String> messageMap);
}
工厂管理:
package com.yizhiliulianta.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 final Map<WxChatMsgTypeEnum,WxChatMsgHandler> handlerMap = new HashMap<>();
public WxChatMsgHandler getHandlerByMsgType(String msgType){
WxChatMsgTypeEnum msgTypeEnum = WxChatMsgTypeEnum.getByMsgType(msgType);
return handlerMap.get(msgTypeEnum);
}
@Override
public void afterPropertiesSet() {
for (WxChatMsgHandler wxChatMsgHandler : wxChatMsgHandlerList) {
handlerMap.put(wxChatMsgHandler.getMsgType(),wxChatMsgHandler);
}
}
}
首先我们需要一个工厂管理类,通过这个类来获取和管理每个实现接口的实现类。
首先我们通过Spring的自动注入,列出实现接口的所有类,并注入到一个集合中
我们应该怎么通过对应的枚举,来获取到对应的实现类呢,这里我使用实现InitializingBean接口,重写afterPropertiesSet方法,让我们在bean初始化后,进行处理的一个方法。
首先我们构建一个map集合,键为枚举值,值为实现类
在afterPropertiesSet方法中,循环所有实现类,并将实现类的枚举值,和实现类放入map集合,这样我们就可以通过枚举来获取对应的实现类了,那么getHandlerByMsgType也就理解了,就是通过传入的枚举值获取对应的枚举,并在map表中取到对应的实现类。
关注实现:
package com.yizhiliulianta.wx.handler;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.Map;
@Component
@Slf4j
public class SubscribeMsgHandler implements WxChatMsgHandler{
@Override
public WxChatMsgTypeEnum getMsgType() {
return WxChatMsgTypeEnum.SUBSCRIPE;
}
@Override
public String dealMsg(Map<String, String> messageMap) {
log.info("触发用户关注事件");
String fromUserName = messageMap.get("FromUserName");
String toUserName = messageMap.get("ToUserName");
String subscribeContent = "谢谢您的关注,我是一支榴莲挞!";
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["+subscribeContent+"]]></Content>\n" +
"</xml>";
return replyContent;
}
}
这个代码应该不难理解,实现刚刚的接口,返回对应的枚举和实现处理请求。
处理请求的方式和刚刚的是一样的,不再去详细说了,相信大家可以看懂
验证码实现:
package com.yizhiliulianta.wx.handler;
import com.yizhiliulianta.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");
//接收方 --》 发送方
String toUserName = messageMap.get("ToUserName");
//随机四位数验证码
Random r = new Random();
int one = r.nextInt(10); //产生0到9的随机数
int two = r.nextInt(10);
int three = r.nextInt(10);
int four = r.nextInt(10);
String num = "" + one + two + three + four;//将4个随机数组成一个字符串
//存入redis
String numKey = redisUtil.buildKey(LOGIN_PREFIX,num);
redisUtil.setNx(numKey, fromUserName, 5L, TimeUnit.MINUTES);
//返回内容
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>";
return replyContent;
}
}
这部分代码就是实现了给用户返回一个四位数的验证码,并将用户的id和验证码存储到redis的一个操作,首先取出content也就是用户发送的信息,如果是验证码这个信息就继续执行,然后生成一个4位随机数
定义redis的主键,与随机数进行构建一个放入redis的键,然后将用户唯一标识作为值放入redis,然后将处理的内容返回
controller使用:
那该如何在controller中使用呢,重写后的代码如下:
@PostMapping(value = "callback", produces = "application/xml;charset=UTF-8")
public String callback(
@RequestBody String requestBody,
@RequestParam("signature") String signature,
@RequestParam("timestamp") String timestamp,
@RequestParam("nonce") String nonce,
@RequestParam(value = "echostr",required = false) String echostr) {
log.info("接收到微信的请求:requestBody:{},signature:{},timestamp:{},nonce:{},echostr:{}"
,requestBody,signature,timestamp,nonce,echostr);
Map<String, String> msgMap = MessageUtil.parseXml(requestBody);
String msgType = msgMap.get("MsgType");
String event = msgMap.get("Event");
log.info("msgType:{},event:{}",msgType,event);
StringBuilder sb = new StringBuilder();
sb.append(msgType);
if (event!=null){
sb.append(".");
sb.append(event);
}
String msgTypeKey = sb.toString();
WxChatMsgHandler wxChatMsgHandler = wxChatMsgFactory.getHandlerByMsgType(msgTypeKey);
if (Objects.isNull(wxChatMsgHandler)){
return "unknown";
}
String replyContent = wxChatMsgHandler.dealMsg(msgMap);
log.info("replyContent:{}",replyContent);
return replyContent;
}
前面没有什么变化,但是我们需要先将微信的xml解析,通过分析msgtype来判断是什么动作,这里我使用了一个字符串的拼接,因为如果是信息而不是关注动作,那么event是为空的,所以经过判断我们就可以直接将msgTypeKey传入,获取到对应的实现类,然后调用其dealMsg,也就是处理方法,获取返回值,最后将其返回。
测试:
下面我们来测试一下整个流程
重启项目后,我们给公众号发送,验证码。
成功接收!然后我们去redis里看看
成功啦!到此为止,我们的微信服务彻底打通,并且与redis进行了集成
登录实现:
auth服务:
下面我们进行auth服务的登录验证逻辑,首先我们进行登录需要用户给我们传一个验证码,然后我们判断成功后,返回给其一个token,代码如下:
auth的controller层:
// 会话登录接口
@RequestMapping("doLogin")
public Result<SaTokenInfo> doLogin(@RequestParam("validCode") String validCode) {
try {
Preconditions.checkArgument(!StringUtils.isBlank(validCode),"验证码不能为空");
SaTokenInfo tokenInfo = authUserDomainService.doLogin(validCode);
return Result.ok(tokenInfo);
}catch (Exception e){
log.error("UserController.doLogin.error:{}", e.getMessage(), e);
return Result.fail("用户登录失败");
}
}
auth的domain层:
下面就是执行doLogin的逻辑实现,代码如下:
@Override
public SaTokenInfo doLogin(String validCode) {
String loginKey = redisUtil.buildKey(LOGIN_PREFIX, validCode);
String openId = redisUtil.get(loginKey);
if (StringUtils.isBlank(openId)){
return null;
}
AuthUserBO authUserBO = new AuthUserBO();
authUserBO.setUserName(openId);
this.register(authUserBO);
StpUtil.login(openId);
SaTokenInfo tokenInfo = StpUtil.getTokenInfo();
return tokenInfo;
}
大家记得把LOGIN_PREFIX这个redis的键复制过来。
首先还是先把redis里的主键构建处理,然后通过封装好的工具类,获取该键的值,也就是用户的唯一标识,我们新建一个authUserBO,将唯一标识作为用户名放入,并调用register也就是注册方法,将其插入数据库中
然后使用satoken的login方法,进行登录,并获取其对应的token进行返回。
这里我对register注册方法进行完善:代码如下
@Override
public Boolean register(AuthUserBO authUserBO) {
AuthUser existAuthUser = new AuthUser();
existAuthUser.setUserName(authUserBO.getUserName());
//校验用户是否存在
List<AuthUser> existUser = authUserService.queryByCondition(existAuthUser);
if (existUser.size() > 0){
return true;
}
AuthUser authUser = AuthUserBOConverter.INSTANCE.convertBOToAuth(authUserBO);
if (StringUtils.isNotBlank(authUser.getPassword())){
authUser.setPassword(SaSecureUtil.md5BySalt(authUser.getPassword(),salt));
}
//插入用户
AuthUser user = authUserService.insert(authUser);
AuthPermission authPermission = new AuthPermission();
//获取对应权限信息
authPermission.setPermissionKey(AuthConstant.PERMISSION_KEY);
AuthPermission permissionResult = authPermissionService.queryByCondition(authPermission);
//用户与权限做关联
Long userId = user.getId();
Long permissionResultId = permissionResult.getId();
AuthUserPermission authUserPermission = new AuthUserPermission();
authUserPermission.setUserId(userId);
authUserPermission.setPermissionId(permissionResultId);
authUserPermissionService.insert(authUserPermission);
//放入redis
String buildKey = redisUtil.buildKey(userPermissionPrefix, authUser.getUserName());
List<AuthPermission> authPermissionList = new LinkedList<>();
authPermissionList.add(permissionResult);
redisUtil.set(buildKey,new Gson().toJson(authPermissionList));
return true;
}
就是在开头新增两个判断,首先在数据库查询有没有该用户,如果有的话直接返回,如果没有再进行注册,并对密码进行判空处理,因为我们使用的是微信登录,所以是没有密码的,不然会出现空指针问题
测试:
那么到这里,整个登录的逻辑完成了,下面让我们测试一下,重启服务并请出apipost
看一眼nacos,ok,都正常
获取一个验证码
这里还是通过网关来进行分配登录,将验证码作为参数传入,成功了!我们可以看到下方成功返回了token
看一下redis
成功!
这里有一个点需要注意,我们的redis配置是不同的索引,所以我们需要将wx服务的redis配置文件 和auth服务的redis配置文件索引相同,不然是无法获取的
到这里我们的登录服务彻底结束了,下一篇是微服务直接的调用,我们该如何在其他服务里知道,本次登录的用户是谁呢?这需要使用feign了