现在开发业务系统,已不是一个单独的系统。每每须要同多个不一样系统相互调用,甚至有时还须要跟微信,钉钉,飞书这样平台对接。目前我开发的部分业务系统,已经完成微信公众平台对接。做为知识总结,接下来,咱们探讨下对接微信公众平台的一小部分功能,微信扫码登陆。其中的关键点是获取openid。我仔细查找了微信提供的开发文档,主要有如下三个方式可实现。javascript
微信全部的接口访问,都要求使用域名。但多数开发者是没有域名,给不少开发者测试带来了麻烦。不过有如下两种方案能够尝试:html
那钉钉的内网穿透工具具体怎么使用用的呢?前端
首先使用git下载钉钉内网穿透工具,下载好后找到windows_64
目录,在这里新建一个start.bat
文件,内容为java
ding -config=ding.cfg -subdomain=pro 8080
复制代码
其中-subdomain
是用来生成子域名8080
表示隐射本地8080端口 双击start.bat
文件,最终启动成功界面以下git
通过我测试,这个至关稳定,而且能够指定静态子域名。简直就是业界良心github
访问公众平台测试帐号系统,能够经过微信登陆,可快速获得一个测试帐号。而后咱们须要如下两个配置web
在点击提交按钮时,微信服务器会验证咱们配置的这个URL是否有效。这个URL有两个用途apache
签名生成逻辑是用配置的token
结合微信回传的timestamp
,nonce
,经过字符串数组排序造成新的字符串,作SHA签名,再将签名后的二进制数组转换成十六进制字符串。最终的内容就是具体的签名信息。对应的java代码以下json
// author: herbert 公众号:小院不小 20210424
public static String getSignature(String token, String timestamp, String nonce) {
String[] array = new String[] { token, timestamp, nonce };
Arrays.sort(array);
StringBuffer sb = new StringBuffer();
for (String str : array) {
sb.append(str);
}
try {
MessageDigest md = MessageDigest.getInstance("SHA-1");
md.update(sb.toString().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 (NoSuchAlgorithmException e) {
logger.error("获取签名信息失败", e.getCause());
}
return "";
}
复制代码
对应GET请求代码以下小程序
// author: herbert 公众号:小院不小 20210424
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
logger.info("微信在配置服务器传递验证参数");
Map<String, String[]> reqParam = request.getParameterMap();
for (String key : reqParam.keySet()) {
logger.info(" {} = {}", key, reqParam.get(key));
}
String signature = request.getParameter("signature");
String echostr = request.getParameter("echostr");
String timestamp = request.getParameter("timestamp");
String nonce = request.getParameter("nonce");
String buildSign = WeixinUtils.getSignature(TOKEN, timestamp, nonce);
logger.info("服务器生成签名信息:{}", buildSign);
if (buildSign.equals(signature)) {
response.getWriter().write(echostr);
logger.info("服务生成签名与微信服务器生成签名相等,验证成功");
return;
}
}
复制代码
微信服务器验证规则是原样返回echostr
,若是以为签名麻烦,直接返回echostr
也是能够的。
这个配置主要用途是解决H5与微信JSSDK集成。微信必需要求指定的域名下,才能调用JSSDK
为了测试扫码登陆效果,咱们须要搭建一个简单的maven工程。工程中具体文件目录以下
用户扫描二维码获得对应的openid
,而后在userdata.json
文件中,根据openid
查找对应的用户。找到了,就把用户信息写入缓存。没找到,就提醒用户绑定业务帐号信息。前端经过定时轮询,从服务缓存中查找对应扫码的用户信息
userdata.json
文件中的内容以下
[{
"userName": "张三",
"password":"1234",
"userId": "000001",
"note": "扫码登陆",
"openId": ""
}]
复制代码
从代码能够知道,后端提供了5个Servlet,其做用分别是
须要调用微信接口信息以下
// author: herbert 公众号小院不小 20210424
private static final String ACCESS_TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={0}&secret={1}";
private static final String QRCODE_TICKET_URL = "https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token={0}";
private static final String QRCODE_SRC_URL = "https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket={0}";
private static final String STENDTEMPLATE_URL = "https://api.weixin.qq.com/cgi-bin/message/template/send?access_token={0}";
private static final String WEB_AUTH_URL = "https://open.weixin.qq.com/connect/oauth2/authorize?appid={0}&redirect_uri={1}&response_type=code&scope=snsapi_base&state={2}#wechat_redirect";
private static final String WEB_AUTH_TOKEN_URL = "https://api.weixin.qq.com/sns/oauth2/access_token?appid={0}&secret={1}&code={2}&grant_type=authorization_code";
复制代码
前端对应的三个页面分别是
最终实现的效果以下
已绑定openid直接跳转到首页
未绑定用户,在手机到会收到邀请微信绑定连接
生成带参数的二维码主要经过如下三个步骤来实现
换取ACCESSTOKEN 代码以下
// author: herbert 公众号小院不小 20210424
public static String getAccessToken() {
if (ACCESSTOKEN != null) {
logger.info("从内存中获取到AccessToken:{}", ACCESSTOKEN);
return ACCESSTOKEN;
}
String access_token_url = MessageFormat.format(ACCESS_TOKEN_URL, APPID, APPSECRET);
logger.info("access_token_url转换后的访问地址");
logger.info(access_token_url);
Request request = new Request.Builder().url(access_token_url).build();
OkHttpClient httpClient = new OkHttpClient();
Call call = httpClient.newCall(request);
try {
Response response = call.execute();
String resBody = response.body().string();
logger.info("获取到相应正文:{}", resBody);
JSONObject jo = JSONObject.parseObject(resBody);
String accessToken = jo.getString("access_token");
String errCode = jo.getString("errcode");
if (StringUtils.isBlank(errCode)) {
errCode = "0";
}
if ("0".equals(errCode)) {
logger.info("获取accessToken成功,值为:{}", accessToken);
ACCESSTOKEN = accessToken;
}
return accessToken;
} catch (IOException e) {
logger.error("获取accessToken出现错误", e.getCause());
}
return null;
}
复制代码
根据ACCESSTOKEN获取二维码TICKET代码以下
// author: herbert 公众号:小院不小 20210424
public static String getQrCodeTiket(String accessToken, String qeCodeType, String qrCodeValue) {
String qrcode_ticket_url = MessageFormat.format(QRCODE_TICKET_URL, accessToken);
logger.info("qrcode_ticket_url转换后的访问地址");
logger.info(qrcode_ticket_url);
JSONObject pd = new JSONObject();
pd.put("expire_seconds", 604800);
pd.put("action_name", "QR_STR_SCENE");
JSONObject sence = new JSONObject();
sence.put("scene", JSONObject
.parseObject("{\"scene_str\":\"" + MessageFormat.format("{0}#{1}", qeCodeType, qrCodeValue) + "\"}"));
pd.put("action_info", sence);
logger.info("提交内容{}", pd.toJSONString());
RequestBody body = RequestBody.create(JSON, pd.toJSONString());
Request request = new Request.Builder().url(qrcode_ticket_url).post(body).build();
OkHttpClient httpClient = new OkHttpClient();
Call call = httpClient.newCall(request);
try {
Response response = call.execute();
String resBody = response.body().string();
logger.info("获取到相应正文:{}", resBody);
JSONObject jo = JSONObject.parseObject(resBody);
String qrTicket = jo.getString("ticket");
String errCode = jo.getString("errcode");
if (StringUtils.isBlank(errCode)) {
errCode = "0";
}
if ("0".equals(jo.getString(errCode))) {
logger.info("获取QrCodeTicket成功,值为:{}", qrTicket);
}
return qrTicket;
} catch (IOException e) {
logger.error("获取QrCodeTicket出现错误", e.getCause());
}
return null;
}
复制代码
获取二维码图片流代码以下
// author: herbert 公众号:小院不小 20210424
public static InputStream getQrCodeStream(String qrCodeTicket) {
String qrcode_src_url = MessageFormat.format(QRCODE_SRC_URL, qrCodeTicket);
logger.info("qrcode_src_url转换后的访问地址");
logger.info(qrcode_src_url);
Request request = new Request.Builder().url(qrcode_src_url).get().build();
OkHttpClient httpClient = new OkHttpClient();
Call call = httpClient.newCall(request);
try {
Response response = call.execute();
return response.body().byteStream();
} catch (IOException e) {
logger.error("获取qrcode_src_url出现错误", e.getCause());
}
return null;
}
复制代码
最终二维码图片经过servlet
中的get方法返回到前端,须要注意的地方就是为当前session添加key用于存储扫码用户信息或openid
// author: herbert 公众号:小院不小 20210424
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
accessToken = WeixinUtils.getAccessToken();
String cacheKey = request.getParameter("key");
logger.info("当前用户缓存key:{}", cacheKey);
WeixinCache.put(cacheKey, "none");
WeixinCache.put(cacheKey + "_done", false);
if (qrCodeTicket == null) {
qrCodeTicket = WeixinUtils.getQrCodeTiket(accessToken, QRCODETYPE, cacheKey);
}
InputStream in = WeixinUtils.getQrCodeStream(qrCodeTicket);
response.setContentType("image/jpeg; charset=utf-8");
OutputStream os = null;
os = response.getOutputStream();
byte[] buffer = new byte[1024];
int len = 0;
while ((len = in.read(buffer)) != -1) {
os.write(buffer, 0, len);
}
os.flush();
}
复制代码
前端可使用image
标签,src
指向这个servlet
地址就能够了
<div class="loginPanel" style="margin-left: 25%;">
<div class="title">微信登陆(微信场景二维码)</div>
<div class="panelContent">
<div class="wrp_code"><img class="qrcode lightBorder" src="/weixin-server/weixinqrcode?key=herbert_test_key"></div>
<div class="info">
<div id="wx_default_tip">
<p>请使用微信扫描二维码登陆</p>
<p>“扫码登陆测试系统”</p>
</div>
</div>
</div>
</div>
复制代码
pc端访问login
页面时,除了显示对应的二维码,也须要开启定时轮询操做。查询到扫码用户信息就跳转到index
页面,没有就间隔2秒继续查询。轮询的代码以下
// author: herbert 公众号:小院不小 20210424
function doPolling() {
fetch("/weixin-server/weixinqrcode?key=herbert_test_key", { method: 'POST' }).then(resp => resp.json()).then(data => {
if (data.errcode == 0) {
console.log("获取到绑定用户信息")
console.log(data.binduser)
localStorage.setItem("loginuser", JSON.stringify(data.binduser));
window.location.replace("index.html")
}
setTimeout(() => {
doPolling()
}, 2000);
})
}
doPolling()
复制代码
能够看到前端访问了后台一个POST接口,这个对应的后台代码以下
// author: herbert 公众号:小院不小 20210424
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String cacheKey = request.getParameter("key");
logger.info("登陆轮询读取缓存key:{}", cacheKey);
Boolean cacheDone = (Boolean) WeixinCache.get(cacheKey + "_done");
response.setContentType("application/json;charset=utf-8");
String rquestBody = WeixinUtils.InPutstream2String(request.getInputStream(), charSet);
logger.info("获取到请求正文");
logger.info(rquestBody);
logger.info("是否扫码成功:{}", cacheDone);
JSONObject ret = new JSONObject();
if (cacheDone != null && cacheDone) {
JSONObject bindUser = (JSONObject) WeixinCache.get(cacheKey);
ret.put("binduser", bindUser);
ret.put("errcode", 0);
ret.put("errmsg", "ok");
WeixinCache.remove(cacheKey);
WeixinCache.remove(cacheKey + "_done");
logger.info("已移除缓存数据,key:{}", cacheKey);
response.getWriter().write(ret.toJSONString());
return;
}
ret.put("errcode", 99);
ret.put("errmsg", "用户还未扫码");
response.getWriter().write(ret.toJSONString());
}
复制代码
经过以上的操做,完美解决了二维显示和轮询功能。但用户扫描了咱们提供二维码,咱们系统怎么知道呢?还记得咱们最初配置的URL么,微信会把扫描状况经过POST的方式发送给咱们。对应接收的POST代码以下
// author: herbert 公众号:小院不小 20210424
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String rquestBody = WeixinUtils.InPutstream2String(request.getInputStream(), charSet);
logger.info("获取到微信推送消息正文");
logger.info(rquestBody);
try {
DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance();
dbf.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
dbf.setFeature("http://xml.org/sax/features/external-general-entities", false);
dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
dbf.setXIncludeAware(false);
dbf.setExpandEntityReferences(false);
DocumentBuilder db = dbf.newDocumentBuilder();
StringReader sr = new StringReader(rquestBody);
InputSource is = new InputSource(sr);
Document document = db.parse(is);
Element root = document.getDocumentElement();
NodeList fromUserName = document.getElementsByTagName("FromUserName");
String openId = fromUserName.item(0).getTextContent();
logger.info("获取到扫码用户openid:{}", openId);
NodeList msgType = root.getElementsByTagName("MsgType");
String msgTypeStr = msgType.item(0).getTextContent();
if ("event".equals(msgTypeStr)) {
NodeList event = root.getElementsByTagName("Event");
String eventStr = event.item(0).getTextContent();
logger.info("获取到event类型:{}", eventStr);
if ("SCAN".equals(eventStr)) {
NodeList eventKey = root.getElementsByTagName("EventKey");
String eventKeyStr = eventKey.item(0).getTextContent();
logger.info("获取到扫码场景值:{}", eventKeyStr);
if (eventKeyStr.indexOf("QRCODE_LOGIN") == 0) {
String cacheKey = eventKeyStr.split("#")[1];
scanLogin(openId, cacheKey);
}
}
}
if ("text".equals(msgTypeStr)) {
NodeList content = root.getElementsByTagName("Content");
String contentStr = content.item(0).getTextContent();
logger.info("用户发送信息:{}", contentStr);
}
} catch (Exception e) {
logger.error("微信调用服务后台出现错误", e.getCause());
}
}
复制代码
咱们须要的扫码数据是 MsgType=="event" and Event=="SCAN"
,找到这条数据,解析出咱们在生成二维码时传递的key
值,再写入缓存便可。代码中的 scanLogin(openId, cacheKey)
完成具体业务逻辑,若是用户已经绑定业务帐号,则直接发送模板消息登陆成功,不然发送模板消息邀请微信绑定,对应的代码逻辑以下
// author: herbert 公众号:小院不小 20210424
private void scanLogin(String openId, String cacheKey) throws IOException {
JSONObject user = findUserByOpenId(openId);
if (user == null) {
// 发送消息让用户绑定帐号
logger.info("用户还未绑定微信,正在发送邀请绑定微信消息");
WeixinUtils.sendTempalteMsg(WeixinUtils.getAccessToken(), openId,
"LWP44mgp0rEGlb0pK6foatU0Q1tWhi5ELiAjsnwEZF4",
"http://pro.vaiwan.com/weixin-server/weixinbind.html?key=" + cacheKey, null);
WeixinCache.put(cacheKey, openId);
return;
}
// 更新缓存
WeixinCache.put(cacheKey, user);
WeixinCache.put(cacheKey + "_done", true);
logger.info("已将缓存标志[key]:{}设置为true", cacheKey + "_done");
logger.info("已更新缓存[key]:{}", cacheKey);
logger.info("已发送登陆成功微信消息");
WeixinUtils.sendTempalteMsg(WeixinUtils.getAccessToken(), openId, "MpiOChWEygaviWsIB9dUJLFGXqsPvAAT2U5LcIZEf_o",
null, null);
}
复制代码
以上就完成了经过场景二维实现微信登陆的逻辑
网页受权登陆的二维码须要咱们构建好具体的内容,而后使用二维码代码库生成二维码
// author: herbert 公众号:小院不小 20210424
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String cacheKey = request.getParameter("key");
logger.info("当前用户缓存key:{}", cacheKey);
BufferedImage bImg = WeixinUtils.buildWebAuthUrlQrCode("http://pro.vaiwan.com/weixin-server/weixinredirect",
cacheKey);
if (bImg != null) {
response.setContentType("image/png; charset=utf-8");
OutputStream os = null;
os = response.getOutputStream();
ImageIO.write(bImg, "png", os);
os.flush();
}
}
复制代码
能够看到,咱们这里缓存key
值,经过state
方式传递给微信服务器。微信服务器会将该值原样返回给我咱们的跳转地址,而且附带上受权码。咱们经过二维码库生成二维码,而后直接返回二维码图。前端直接指向这个地址就可显示图片了。对应前端代码以下
<div class="loginPanel">
<div class="title">微信登陆(微信网页受权)</div>
<div class="panelContent">
<div class="wrp_code"><img class="qrcode lightBorder" src="/weixin-server/weixinwebqrcode?key=herbert_test_key"></div>
<div class="info">
<div id="wx_default_tip">
<p>请使用微信扫描二维码登陆</p>
<p>“扫码登陆测试系统”</p>
</div>
</div>
</div>
</div>
复制代码
用户扫描咱们生成的二维码之后,微信服务器会发送一个GET请求到咱们配置的跳转地址,咱们在这里完成openid
的验证和业务系统用户信息获取操做,对应代码以下
// author: herbert 公众号:小院不小 20210424
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
String code = request.getParameter("code");
String state = request.getParameter("state");
logger.info("获取到微信回传参数code:{},state:{}", code, state);
JSONObject webTokenInfo = WeixinUtils.getWebAuthTokenInfo(code);
if (webTokenInfo != null && !webTokenInfo.containsKey("errcode")) {
String openId = webTokenInfo.getString("openid");
logger.info("获取到用opeind", openId);
JSONObject user = findUserByOpenId(openId);
if (user == null) {
//用户未绑定 将openid存入缓存方便下一步绑定用户
WeixinCache.put(state, openId);
response.sendRedirect("weixinbind.html?key=" + state);
return;
}
WeixinCache.put(state, user);
WeixinCache.put(state + "_done", true);
logger.info("已将缓存标志[key]:{}设置为true", state + "_done");
logger.info("已更新缓存[key]:{}", state);
response.setCharacterEncoding("GBK");
response.getWriter().print("扫码成功,已成功登陆系统");
}
}
复制代码
用户扫描这个二维码后,逻辑跟场景二维码同样,找到用户信息就提示用户已成功登录系统,不然就跳转到微信绑定页面
开放平台登陆须要认证事后才能测试,认证须要交钱。对不起,我不配测试。
扫描登陆主要逻辑是生成带key值二维,而后一直轮询服务器查询登陆状态。以上两个方式各有优劣,主要区别以下
这里涉及到的知识点有
开发过程当中,须要多查帮助文档。开发过程当中的各类环境配置,对开发者来讲,也是不小的挑战。作微信开发也有好多年,从企业微信,到公众号,到小程序,到小游戏,一直没有总结。此次专门作了一个微信扫码登陆专题。先写代码,再写总结也花费了数周时间。若是以为好,还望关注公众号支持下,您的点赞和在看是我写做力量的源泉。对微信集成和企业微信集成方面有问题的,也欢迎在公众号回复,我看到了会第一时间力所能及的为您解答。须要文中说起的项目,请扫描下方的二维码,关注公众号[小院不小],回复wxqrcode获取.