現在開發業務系統,已不是一個單獨的系統。每每須要同多個不一樣系統相互調用,甚至有時還須要跟微信,釘釘,飛書這樣平臺對接。目前我開發的部分業務系統,已經完成微信公衆平臺對接。做爲知識總結,接下來,咱們探討下對接微信公衆平臺的一小部分功能,微信掃碼登陸。其中的關鍵點是獲取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獲取.