如何实现微信公众平台(本地测试号)功能,动态二维码、授权获取code、置换openId,操作全流程,带避坑,超详细~
目录
壹|微信平台操作流程
一、前置准备
JS回调在用户信息获取等时需要使用,目前暂不使用。在测试时必须要扫码关注测试公众号才能测试扫描之后形成的动态二维码。(注:域名本身没有特定含义,可以随便定义,也可以通过内网穿透工具获取网络域名,可同后续的网络回调域名保持一致)
- 首先我们需要在微信公众平台注册一个测试账号。可跳转链接:微信公众平台
- 登录成功页面,其中的 appID,appsecret 都需要后续用到(注意保护隐私)。这里因为是本地测试,所以不需要URL和Token信息。
在网络授权用户信息处点击“修改”,修改回调域名(这里要注意不加 http://)。
这里讲一下我当时写网络授权回调域名的心路和遇到的问题:
最开始的时候我在网上找了很多资料,然后发现全都是打码的,不知道该些啥,然后根据官方文档和网上资料,在这里写了一个自定义的域名 "www.hanying.com", 然后保持JS接口一致,接着我将生成二维码的text路径中的 redirect_uri=www.hanying.com,并在本地hosts文件中,将 ip地址跟这个域名进行映射,随后我调用nginx静态web服务器进行监听 80 端口,如果监听到访问这个路径就跳转到 nginx/html/bangding/xx.html页面。 这里还一切顺利,但是当我在html页面设置点击确认按钮发送 ajax 请求给后端传 code 值时,一直发送不了。后来归结起来有三个简单的问题:其一,我没有在html页面引入JS或JQuery,所以使用 $.ajax 没有效果;其二,我的手机用的是流量,我的电脑用的是公司wife,不在局域网下,而ajax发送请求的路径为本地(ip)路径,所以无法接受成功;其三,我的路径中scope=SCOPE,权限错误,所以在解决10003错误以后又爆了10005错误。 如果在这里扫描二维码出现 10003 错误,检查以下三个地方配置是否一致,回调域名是否加了http://,以及url路径是否进行了编码(需要编码url路径)。 如果出现了 10005 错误,是权限不足,检查scope属性,看看是否为 scope=snsapi_base。
二、流程梳理
可以参考官方开发文档:微信网页开发-网页授权
- 生成二维码(我这里是通过前端生成),用户访问跳转自定义授权页面
- 用户跳转到授权页面,获取code
- 后端获取 code后,通过code换取网页授权access_token和openId
- 保存openId到数据库(有的需要别的操作,可根据官方文档具体操作)
code: 微信授权码,就像一个临时的票据,用来访问微信服务器获取用户的 openId 和 token 令牌。
state: 状态码。
scope: 权限域,微信授权中有两种权限,一种是snsapi_base,一种是获取用户信息snsapi_userinfo。
appId:访问的微信公众号唯一ID。
redirect_uri:回调域名,可以链接到自定义的授权页面,用户访问时获取code信息(需进行url编码)。
三、具体实施(代码附录
参考了很多,最主要参考还是其他博客,虽然很想把链接附上,但是找了很久也没找到。
注:主要为方法,页面根据自己需求编写,如果用JQuery,记得引包
1. 前端页面生成二维码(vue-elementui)
<el-button
size="mini"
type="text"
icon="el-icon-check"
@click="bangding(scope.row)"
>绑定</el-button
>
<el-dialog
title="微信扫码绑定用户"
:visible.sync="isShowCard"
width="400px"
center
:before-close="jieBangClose"
>
<!-- 存放二维码的地方 -->
<div style="display: flex; justify-content: center">
<div id="qrCode" ref="qrCodeDiv"></div>
</div>
</el-dialog>
import QRCode from "qrcodejs2"; //引入生成二维码插件
data() {
return {
//用的时候把AppID,REDIRECT_URI 改了就可以
url: "https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=REDIRECT_URI&response_type=code&scope=snsapi_base&state=STATE#wechat_redirect"
}
}
methods: {
jieBangClose(){
this.isShowCard = false;
this.getList();
},
/** 绑定微信(生成动态二维码) */
bangding(row) {
const memberId = row.id;
this.isShowCard = true;
this.createQRCode(memberId);
},
createQRCode(id) {
this.$nextTick(() => {
this.$refs.qrCodeDiv.innerHTML = ""; //二维码清除
this.bangdingId = id;
new QRCode(this.$refs.qrCodeDiv, {
text: this.url, //二维码链接,参数是否添加看需求
width: 200, //二维码宽度
height: 200, //二维码高度
colorDark: "#333333", //二维码颜色
colorLight: "#ffffff", //二维码背景色
correctLevel: QRCode.CorrectLevel.L, //容错率,L/M/H
});
});
}
}
2. 授权页面
<button onclick="getCode()" id="right">确认</button>
<script>
function GetQueryString(code) {
var reg = new RegExp("(^|&)" + code + "=([^&]*)(&|$)");
var r = window.location.search.substr(1).match(reg);
if (r != null) return unescape(r[2]); return null;
}
function getCode() {
var code = GetQueryString("code");
alert(code);
if (code == null || code == "") {
var url = "http%3A%2F%2F1cee3816.r10.cpolar.top";
window.location.href = "https://open.weixin.qq.com/connect/oauth2/authorize?" +
"appid=APPID&redirect_uri=" + url + "&response_type=code" +
"&scope=snsapi_base&state=STATE#wechat_redirect";
} else {
alert('发送')
$.ajax({
type: "get", //提交方式
url: "192.168.3.62:8080/ubp/member/test/" + code ,//路径
success: function (result) {//返回数据根据结果进行相应的处理
alert("成功发送")
}
});
}
}
</script>
3. Java后端接收code参数,获取用户openId
-
pom 文件引入依赖
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
<dependency>
<groupId>net.sf.json-lib</groupId>
<artifactId>json-lib</artifactId>
<version>2.3</version>
<classifier>jdk15</classifier>
</dependency>
-
util和config和manger
CommonUtil:
import com.ruoyi.framework.manager.MyX509TrustManager;
import net.sf.json.JSONObject;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.ConnectException;
import java.net.URL;
import java.security.SecureRandom;
/**
* 微信二维码扫码认证的工具类
*/
public class CommonUtil {
/**
* 发送https请求
* @param requestUrl 请求地址
* @param requestMethod 请求方式(GET、POST)
* @param outputStr 提交的数据
* @return JSONObject(通过JSONObject.get(key)的方式获取json对象的属性值)
*/
public static JSONObject httpsRequest(String requestUrl, String requestMethod, String outputStr) {
JSONObject jsonObject = null;
try {
// 创建SSLContext对象,并使用我们指定的信任管理器初始化
TrustManager[] tm = { new MyX509TrustManager() };
SSLContext sslContext = SSLContext.getInstance("SSL", "SunJSSE");
sslContext.init(null, tm, new SecureRandom());
// 从上述SSLContext对象中得到SSLSocketFactory对象
SSLSocketFactory ssf = sslContext.getSocketFactory();
URL url = new URL(requestUrl);
HttpsURLConnection conn = (HttpsURLConnection) url.openConnection();
conn.setSSLSocketFactory(ssf);
conn.setDoOutput(true);
conn.setDoInput(true);
conn.setUseCaches(false);
// 设置请求方式(GET/POST)
conn.setRequestMethod(requestMethod);
// 当outputStr不为null时向输出流写数据
if (null != outputStr) {
OutputStream outputStream = conn.getOutputStream();
// 注意编码格式
outputStream.write(outputStr.getBytes("UTF-8"));
outputStream.close();
}
// 从输入流读取返回内容
InputStream inputStream = conn.getInputStream();
InputStreamReader inputStreamReader = new InputStreamReader(inputStream, "utf-8");
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
String str = null;
StringBuffer buffer = new StringBuffer();
while ((str = bufferedReader.readLine()) != null) {
buffer.append(str);
}
// 释放资源
bufferedReader.close();
inputStreamReader.close();
inputStream.close();
inputStream = null;
conn.disconnect();
//jsonObject = JSONObject.fromObject(buffer.toString());
jsonObject = JSONObject.fromObject(buffer.toString());
} catch (ConnectException ce) {
System.out.println("连接超时");
} catch (Exception e) {
System.out.println("请求异常");
}
return jsonObject;
}
}
WeChatConfig:
public class WeChatConfig {
/**
* 微信服务号APPID
*/
public static String APPID=APPID;
/**
* appsecret
*/
public static String APPSECRECT=APPSECRECT;
/**
* grant_type
*/
public static String GRANTTYPE="authorization_code";
}
Manger:
import javax.net.ssl.X509TrustManager;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
/**
* 微信扫码认证的 manger
*/
public class MyX509TrustManager implements X509TrustManager {
// 检查客户端证书
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
}
// 检查服务器端证书
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
}
// 返回受信任的X509证书数组
public X509Certificate[] getAcceptedIssuers() {
return null;
}
}
Controller,Service 和 ServiceImpl:
Controller:
@PostMapping("/getLoginQrCode")
public Object bangding( String code){
System.out.println("走到了这个方法,打印code值为:" + code);
return service.insertYyMemberCode(code);
}
Service:
Object insertYyMemberCode(Long id, String name, String code);
ServiceImpl:
@Override
public Object insertYyMemberCode(Long id, String name, String code) {
String WX_URL = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=APPID&secret=SECRET&code=CODE&grant_type=authorization_code";
try {
if ( StringUtils.isBlank(code)) {
return "用户信息有误,绑定失败。";
} else {
String requestUrl = WX_URL.replace("APPID", WeChatConfig.APPID).replace("SECRET", WeChatConfig.APPSECRECT)
.replace("CODE", code).replace("authorization_code", WeChatConfig.GRANTTYPE);
JSONObject jsonObject = CommonUtil.httpsRequest(requestUrl, "GET", null);
if (jsonObject != null) {
try {
System.out.println("------jsonObject: " + jsonObject);
// 业务操作
String access_token = jsonObject.getString("access_token");
String openId = jsonObject.getString("openid");
return "绑定成功!";
} catch (Exception e) {
e.printStackTrace();
}
} else {
System.out.println("code无效");
return "code无效";
}
}
} catch (Exception e) {
e.printStackTrace();
}
System.out.println("error");
return "error";
}
贰|微信公众开发二维码
样式:
一、 解决 H5 跳转路径的 # 问题
问题描述: H5 路径中,都会默认带有#,如路径:http://localhost:8081/#/?payment=15&selected=0 而这个 #,会导致在 nginx 反向代理时变成:http://localhost:8081/?payment=15&selected=0/#
我第一反应是 nginx 的代理路径问题,但在网上找了很多也没有办法处理"#",因为"#"在nginx.conf文件中默认为注释内容,如果强行添加会出现两种情况,nginx启动/重启不成功;重启成功后访问展现空白页面。
最后解决的办法是 H5 路径忽略 "#", 只需要在 uniapp 项目中 -> manifest.json文件 -> Web配置 -> 修改路由模式为 history。(如图)
当然,这样简单的修改可能伴随着打印台循环报H5相关错误(但不影响程序执行),所以我在这里就没有去详细解决这个问题,有兴趣的伙伴可以分享自己遇到的问题和解决办法。
nginx 配置
server {
listen 8083;
//这里使用了虚拟路径,因为是本地测试,所以在hosts里进行了配对,
//正式上线应当使用正式域名。
server_name www.hanying.com;
//这里跳转有两个页面,一个是用来绑定微信公众号,一个是为了根据绑定的openId查询用户信息,
//出于保护用户信息的原因,不管是哪个页面操作,都要通过临时 code 去后端置换 openId,
//所以通过不同的跳转路径,获取自己页面需要的 code。
//注:将两个页面的code分开考虑,自己跳转时获取的code自己用)
//跳转页面一:
location /pages/bingPage/bindPage {
proxy_pass http://192.168.3.62:8081;
//最终映射路径:http://192.168.3.62:8081/pages/bingPage/bindPage
}
//跳转页面二:
location / {
proxy_pass http://192.168.3.62:8081;
//最终映射路径:http://192.168.3.62:8081
}
二、二维码中传递信息,并在授权页面获取
代码实现:
bangding 方法更新
/** 绑定微信(生成动态二维码),并将state赋值 */
/** 解释一:这里需要编码两次,因为浏览器会自动解码一次,但不是utf-8解码,所以中文会乱码,编码两次,浏览器解码以后,在用utf-8解码一次,就可以获取到正确的中文信息 */
/** 解释二:之所以要替换掉STATE,是因为微信访问时自动过滤掉了url上的其他信息,只保留了code和state。我最开始写的是 &id=id&name=name&state=STATE, 但是数据直接没有了,更获取不到 */
bangding(row) {
const id = row.id;
const str = row.code + "," + row.name ;//将id 和 name 获取到组成字符串
this.url = this.url.replace("STATE", encodeURI(encodeURI(str)));//替换掉STATE
this.isShowCard = true;
this.createQRCode(id);
}
授权页面(这里改有了mui模板)
<body>
<header class="mui-bar mui-bar-nav">
<h1 class="mui-title">绑定</h1>
</header>
<div class="mui-content">
<form id='login-form' class="mui-input-group">
<div class="mui-input-row">
<label>工号</label>
<input id='id' type="text" class="mui-input-clear mui-input" placeholder="请输入您的工号">
</div>
<div class="mui-input-row">
<label>姓名</label>
<input id='name' type="text" class="mui-input-clear mui-input" placeholder="请输入您的姓名">
</div>
</form>
<div class="mui-content-padded">
<button id='login' class="mui-btn mui-btn-block mui-btn-primary">绑定</button>
</div>
</div>
</div>
<script src="js/mui.min.js"></script>
<script src="js/mui.enterfocus.js"></script>
<script src="js/app.js"></script>
<script>
(function($, doc) {
$.init({
statusBarBackground: '#f7f7f7'
});
var loginButton = doc.getElementById('login');
var idBox = doc.getElementById('id');
var name = doc.getElementById('name');
var str;
$.ready(function(){
strs = GetQueryString("state").split(",")
console.log(strs)
// str = decodeURIComponent()
idBox.value = strs[0]
name.value = decodeURIComponent(strs[1])
})
loginButton.addEventListener('tap', function(event) {
var code = GetQueryString("code")
if (code == null || code == "") {
var url = "http%3A%2F%2Fwww.hanying.com";
window.location.href = "https://open.weixin.qq.com/connect/oauth2/authorize?" +
"appid=APPID&redirect_uri=" + url + "&response_type=code" +
"&scope=snsapi_base&state=STATE#wechat_redirect";
}
var loginInfo = {
id: idBox.value,
name: name.value,
code: code
};
mui.post('http://192.168.3.62:8080/ubp/member/getLoginQrCode', loginInfo, function(data) {
alert(data);
}, 'json');
});
function GetQueryString(code) {
var reg = new RegExp("(^|&)" + code + "=([^&]*)(&|$)");
var r = window.location.search.substr(1).match(reg);
if (r != null) return unescape(r[2]);
return null;
}
}(mui, document));
</script>
</body>
叁|补充
补充一:
在uniapp组件中,后来用到了 onload 生命周期,没有在用 GetQueryString 方法,虽然在本地访问微信时获取不到数据,但如果在 uniapp 上直接拼接路径的话是完全可以获取到的。
详细可以查看 uniapp 官方文档。
补充二:
微信公众平台和微信小程序调用获取code的接口不一样,包括微信公众平台调用接口时的scope权限也不一样,具体请参考微信开发平台文档。
wx.login读取的code请用auth.code2Session的接口获取openid
wx.pluginLogin读取的code请用auth.getPluginOpenPId获取openid
如果用 前者的 code 去调用后者的接口来获取 openId,会报 10007(has_no_permisson) 错误码。
补充三:
每一个页面调用的 code 都应该分开考虑,这个是我最新踩的坑。当我通过绑定页面绑定用户信息,将 openId 存入数据库后,当我想要在支付页面根据 openId 查询用户基本信息时,因为涉及用户信息安全性,所以 openId 不能外显,所以要再一次的通过 code 来置换 openId 从而查询数据库。
这里我最开始想的就是要获取 code 就要再次绑定,就要跳转绑定页面,从而数据无法获取到支付页面,进入了死循环。
最后在大佬的指点下,只需要在支付页面访问的时候再次通过微信跳转支付页面(刷新页面),从而获取 code 值,数据仍然是存在 state 中,然后就可以通过 code 向后端发送请求,再次置换到用户的 openId,查询数据库。
可能有朋友就问,每次的 code 都不一样,为什么能获取到同一个 openId 呢,这是因为一个公众号对应下的一个微信账户会根据固定的编码方式生成同样的一个 openId(微信绑定唯一识别id)。我其实觉得把 code 和 openId 比作公钥,私钥更好理解,code作为私钥来生成jwt ,随后使用 openId 作为公钥进行校验验证,但不知道这样的理解是否正确。