目录
一、业务需求
手机扫描二维码后关注公众号,发送验证码给公众号,公众号返回验证码,然后输入到网页判断验证码是否正确后通过登录。
二、内网穿透
内网穿透是,它允许通过公共网络(比如互联网)将数据传输到内部网络中,即使内部网络处于防火墙或NAT(网络地址转换)等保护措施之后也能实现。这对于需要从外部访问内部网络资源的场景非常有用,使用场景是外部网络访问内部网络的情况,比如远程访问家庭网络中的设备。
这里使用的内网穿透工具是natapp
内网穿透使用指南:
NATAPP1分钟快速新手图文教程 - NATAPP-内网穿透 基于ngrok的国内高速内网映射工具
输入命令启动:start natapp -authtoken=xxxx
启动后会有一个地址
三、服务器配置
测试号地址:微信公众平台
这里的Token暂可随便输入
四、依赖引入
pom.xml
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-boot.version>2.4.2</spring-boot.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<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.16</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
<!-- json处理-->
<dependency>
<groupId>com.thoughtworks.xstream</groupId>
<artifactId>xstream</artifactId>
<version>1.4.18</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>2.8.5</version>
</dependency>
<dependency>
<groupId>org.dom4j</groupId>
<artifactId>dom4j</artifactId>
<version>2.1.1</version>
</dependency>
<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>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.9.0</version>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<repositories>
<repository>
<id>central</id>
<name>aliyun maven</name>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
<layout>default</layout>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>
<build>
<finalName>${project.artifactId}</finalName>
<!--打包成jar包时的名字-->
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.3.0.RELEASE</version>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
五、验证服务器有效性
开发者提交信息后,微信服务器将发送GET请求到填写的服务器地址URL上,GET请求携带参数如下表所示:
参数 | 描述 |
signature | 微信加密签名,signature结合了开发者填写的token参数和请求中的timestamp参数、nonce参数。 |
timestamp | 时间戳 |
nonce | 随机数 |
echostr | 随机字符串 |
开发者通过检验signature对请求进行校验(下面有校验方式)。若确认此次GET请求来自微信服务器,请原样返回echostr参数内容,则接入生效,成为开发者成功,否则接入失败。加密/校验流程如下:
1)将token、timestamp、nonce三个参数进行字典序排序
2)将三个参数字符串拼接成一个字符串进行sha1加密
3)开发者获得加密后的字符串可与signature对比,标识该请求来源于微信
代码
controller类
这里的token需要和上面服务器配置的token一致
private static final String token = "aeaae";
/**
* 回调消息校验
*/
@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";
}
SHA1工具类
/**
* sha1生成签名工具
*/
@Slf4j
public class SHA1 {
/**
* 用SHA1算法生成安全签名
*
* @param token 票据
* @param timestamp 时间戳
* @param nonce 随机字符串
* @param encrypt 密文
* @return 安全签名
*/
public static String getSHA1(String token, String timestamp, String nonce, String encrypt) {
try {
String[] array = new String[]{token, timestamp, nonce, encrypt};
StringBuffer sb = new StringBuffer();
// 字符串排序
Arrays.sort(array);
for (int i = 0; i < 4; i++) {
sb.append(array[i]);
}
String str = sb.toString();
// SHA1签名生成
MessageDigest md = MessageDigest.getInstance("SHA-1");
md.update(str.getBytes());
byte[] digest = md.digest();
StringBuffer hexStr = new StringBuffer();
String shaHex = "";
for (int i = 0; i < digest.length; i++) {
shaHex = Integer.toHexString(digest[i] & 0xFF);
if (shaHex.length() < 2) {
hexStr.append(0);
}
hexStr.append(shaHex);
}
return hexStr.toString();
} catch (Exception e) {
log.error("sha加密生成签名失败:", e);
return null;
}
}
}
六、用户订阅后自动回复消息
用户在关注与取消关注公众号时,微信会把这个事件推送到开发者填写的URL。方便开发者给用户下发欢迎消息或者做账号的解绑。为保护用户数据隐私,开发者收到用户取消关注事件时需要删除该用户的所有信息。
<xml>
<ToUserName><![CDATA[toUser]]></ToUserName>
<FromUserName><![CDATA[FromUser]]></FromUserName>
<CreateTime>123456789</CreateTime>
<MsgType><![CDATA[event]]></MsgType>
<Event><![CDATA[subscribe]]></Event>
</xml>
参数 | 描述 |
ToUserName | 开发者微信号 |
FromUserName | 发送方账号(一个OpenID) |
CreateTime | 消息创建时间 (整型) |
MsgType | 消息类型,event |
Event | 事件类型,subscribe(订阅)、unsubscribe(取消订阅) |
代码
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 = "msg_signature", required = false) String msgSignature){
//解析微信发来的xml
Map<String,String> messageMap = MessageUtil.parseXml(requestBody);
//获取事件类型和消息类型
String msgType = messageMap.get("MsgType");
String event = messageMap.get("Event") == null ? "" : messageMap.get("Event");
//拼接
StringBuilder sb = new StringBuilder();
sb.append(msgType);
if(!StringUtils.isEmpty(event)){
sb.append(".");
sb.append(event);
}
String msgTypeKey = sb.toString();
//用户关注事件
if(msgTypeKey.equals("event.subscribe")){
return dealMsg(messageMap);
}
//用户发送文本消息
else if(msgTypeKey.equals("text")){
return dealUserMsg(messageMap);
}
return "unknown";
}
//用户关注事件
public String dealMsg(Map<String,String> messageMap){
String fromUserName = messageMap.get("FromUserName");
String toUserName = messageMap.get("ToUserName");
String subscribeContent = "感谢您的关注,我是四月天行健";
String content = "<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 content;
}
MessageUtil工具类
public class MessageUtil {
/**
* 解析微信发来的请求(XML).
*
* @param msg 消息
* @return map
*/
public static Map<String, String> parseXml(final String msg) {
// 将解析结果存储在HashMap中
Map<String, String> map = new HashMap<String, String>();
// 从request中取得输入流
try (InputStream inputStream = new ByteArrayInputStream(msg.getBytes(StandardCharsets.UTF_8.name()))) {
// 读取输入流
SAXReader reader = new SAXReader();
Document document = reader.read(inputStream);
// 得到xml根元素
Element root = document.getRootElement();
// 得到根元素的所有子节点
List<Element> elementList = root.elements();
// 遍历所有子节点
for (Element e : elementList) {
map.put(e.getName(), e.getText());
}
} catch (Exception e) {
e.printStackTrace();
}
return map;
}
}
七、用户发送文本消息后回复消息
当用户发送消息给公众号时(或某些特定的用户操作引发的事件推送时),会产生一个POST请求,开发者可以在响应包(Get)中返回特定XML结构,来对该消息进行响应(现支持回复文本、图片、图文、语音、视频、音乐)。严格来说,发送被动响应消息其实并不是一种接口,而是对微信服务器发过来消息的一次回复。
<xml>
<ToUserName><![CDATA[toUser]]></ToUserName>
<FromUserName><![CDATA[fromUser]]></FromUserName>
<CreateTime>12345678</CreateTime>
<MsgType><![CDATA[text]]></MsgType>
<Content><![CDATA[你好]]></Content>
</xml>
参数 | 是否必须 | 描述 |
ToUserName | 是 | 接收方账号(收到的OpenID) |
FromUserName | 是 | 开发者微信号 |
CreateTime | 是 | 消息创建时间 (整型) |
MsgType | 是 | 消息类型,文本为text |
Content | 是 | 回复的消息内容(换行:在content中能够换行,微信客户端就支持换行显示) |
代码
controller类
//用户发送文本事件
public String dealUserMsg(Map<String,String> messageMap){
String content = messageMap.get("Content");
if (!"验证码".equals(content)) {
return "";
}
String fromUserName = messageMap.get("FromUserName");
String toUserName = messageMap.get("ToUserName");
Random random = new Random();
int num = random.nextInt(1000);
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;
}
八、拓展
可以将验证码和微信返回的openid存储到redis中,然后在需要时可以从redis中取出数据,完成登录。