目录
一、业务需求
二、内网穿透
三、服务器配置
编辑
四、依赖引入
pom.xml
五、验证服务器有效性
代码
controller类
SHA1工具类
六、用户订阅后自动回复消息
代码
controller类
MessageUtil工具类
七、用户发送文本消息后回复消息
代码
controller类
八、拓展
一、业务需求
手机扫描二维码后关注公众号,发送验证码给公众号,公众号返回验证码,然后输入到网页判断验证码是否正确后通过登录。
二、内网穿透
内网穿透是,它允许通过公共网络(比如互联网)将数据传输到内部网络中,即使内部网络处于防火墙或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){//解析微信发来的xmlMap<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中取出数据,完成登录。