1.背景
實際開發中,使用第三方登錄是非常常見的業務...
這樣可以大提高用戶體驗,沒必要一來就要注冊,或者登錄之類的...
並且開發一個登錄或者注冊嚴格來說也是非常麻煩的(各種防止攻擊、機器操作等)
2.准備公眾號和測試環境
需要准備的如下
1.appid
2.appSecret
3.外網可以訪問的映射地址
如果你有服務號、並且是認證了的(這些認證需要企業資質),當然很好,通常來時如果你是學習應該沒有
即使沒有也沒關系,微信提供了測試賬號,並且擁有很多權限,開發好后,只要替換為公司的生產公眾號就可以使用了
獲取測試公眾號步驟如下:
打開鏈接:https://developers.weixin.qq.com/doc/offiaccount/Getting_Started/Overview.html
建議初學者認證讀一下微信的開發文檔,正常情況下如果你是做開發很大概率會經常用到微信相關的接口
點擊測試號申請界面如下:
點擊微信登陸,掃碼即可快速獲得一個微信公眾號測試
頁面下方有接口權限,設置網頁回調地址
如下,注意只寫域名,不要寫http之類的
如果沒有外網地址可以使用外網映射:https://www.cnblogs.com/newAndHui/p/14241177.html (免費、簡單、三步搞定)
到此公眾號配置已經完成
3.實現網頁授權(微信登陸)
1.微信登陸的本質就是,通過用戶授權獲得用戶的 信息,然后保存到數據庫,就像用戶注冊時保存用戶信息是一個道理,只是數據來源不同
2.具體實現步驟,微信文檔已經寫得非常清楚
官方文檔地址:https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/Wechat_webpage_authorization.html
一共4個步驟,其實不論是微信授權登錄,還是QQ授權登錄,或者支付寶授權登錄.....等只要是OAuth2.0協議都是這邏輯
換句話說,OAuth2.0是一種三方授權登錄的協議,大部分授權登錄都是遵循這個協議的,使用開發思路都是一樣的
那么如果你要開一個系統,然后允許別的系統使用你的三方授權登錄是不是也可以安裝這個思路設計
1 第一步:用戶同意授權,獲取code
2 第二步:通過code換取網頁授權access_token
3 第三步:刷新access_token(如果需要)
4 第四步:拉取用戶信息(需scope為 snsapi_userinfo)
具體實現代碼:

package com.ldp.user.controller; import cn.hutool.core.date.DateField; import cn.hutool.core.date.DateUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.http.HttpUtil; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import com.ldp.user.common.base.BaseResponse; import com.ldp.user.common.base.ResponseBuilder; import com.ldp.user.common.exception.ParamException; import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.util.Date; import java.util.HashMap; import java.util.Map; /** * @Copyright (C) 四川千行你我科技股份技有限公司 * @Author: lidongping * @Date: 2021-01-04 16:16 * @Description: <p> * 微信網頁授權 * https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/Wechat_webpage_authorization.html * </p> */ @Slf4j @RestController @RequestMapping("/wc") public class WeChatLoginController { // 模擬存放(實際開發中應該存放在數據庫和Redis) private static Map<String, String> mapData = new HashMap<>(); // appId\appSecret redirectUri 實際生產中應該配置到數據庫 private static String appId = "wxeb91796d8fbb1"; private static String appSecret = "e7aeb6cb4be6fe3388cfd4580f36"; // 微信授權code后的回調地址 private static String redirectUri = "http://lidongping.free.idcfengye.com"; /** * 請求CODE */ @GetMapping("/codeUrl") public BaseResponse getCodeUrl() { String url = "https://open.weixin.qq.com/connect/qrconnect?appid=%s&redirect_uri=%s&response_type=code&scope=SCOPE&state=STATE#wechat_redirec"; url = String.format(url, appId, redirectUri); return ResponseBuilder.success(url); } /** * 第二步:通過code換取網頁授權access_token * code=011NQuFa1OiTgA0spVGa1Cvyff1NQuFC&state=STATE * <p> * { * "access_token":"ACCESS_TOKEN", * "expires_in":7200, * "refresh_token":"REFRESH_TOKEN", * "openid":"OPENID", * "scope":"SCOPE" * } */ @GetMapping("/notify/code") public BaseResponse notifyCode(String code, String state) { log.info("code={},state={}", code, state); if (StrUtil.isEmpty(code)) { return ResponseBuilder.failed("獲取code失敗"); } String url = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=%s&secret=%s&code=%s&grant_type=authorization_code"; url = String.format(url, appId, appSecret, code); log.info("第二步:通過code換取網頁授權access_token,請求url={}", url); String response = HttpUtil.get(url, 60000); log.info("響應結果:{}", response); // 模擬數據入庫,便於下次使用 JSONObject object = JSON.parseObject(response); String openId = object.getString("openid"); // 設置token失效時間 Long timeOutAccessToken = DateUtil.offset(new Date(), DateField.SECOND, object.getInteger("expires_in") - 120).getTime(); object.put("timeOutAccessToken", timeOutAccessToken); // refresh_token有效期為30天 object.put("timeOutRefreshToken", DateUtil.offsetDay(new Date(), 30)); mapData.put(openId, JSON.toJSONString(object)); // 將openid返回給調用者便於,下次使用openid獲取用戶信息 return ResponseBuilder.success(openId); } /** * 拉取用戶信息(需scope為 snsapi_userinfo) */ @GetMapping("/userInfo") public BaseResponse userInfo(String openId) { String obj = mapData.get(openId); if (obj == null) { return ResponseBuilder.failed("未授權"); } JSONObject object = JSON.parseObject(obj); String accessToken = object.getString("access_token"); Long timeOutAccessToken = object.getLong("timeOutAccessToken"); if (timeOutAccessToken < System.currentTimeMillis()) { // 重新獲取 access_token accessToken = refreshToken(openId); } String url = "https://api.weixin.qq.com/sns/userinfo?access_token=%s&openid=%s&lang=zh_CN"; url = String.format(url, accessToken, openId); log.info("拉取用戶信息 請求url={}", url); String response = HttpUtil.get(url, 60000); log.info("響應結果:{}", response); return ResponseBuilder.success(response); } /** * 刷新access_token(如果需要) * <p> * { * "access_token":"ACCESS_TOKEN", * "expires_in":7200, * "refresh_token":"REFRESH_TOKEN", * "openid":"OPENID", * "scope":"SCOPE" * } * * @return */ public String refreshToken(String openId) { String obj = mapData.get(openId); if (obj == null) { throw new ParamException("用戶沒有授權"); } JSONObject objectMap = JSON.parseObject(obj); Long timeOutAccessTokenOld = objectMap.getLong("timeOutRefreshToken"); // 判定refresh_token是否過期 if (timeOutAccessTokenOld < System.currentTimeMillis()) { throw new ParamException("授權已過期,請重新授權"); } String refreshToken = objectMap.getString("refresh_token"); String url = "https://api.weixin.qq.com/sns/oauth2/refresh_token?appid=%s&grant_type=refresh_token&refresh_token=%s"; url = String.format(url, appId, refreshToken); log.info("拉取用戶信息 請求url={}", url); String response = HttpUtil.get(url, 60000); log.info("響應結果:{}", response); // 模擬數據入庫,便於下次使用 JSONObject object = JSON.parseObject(response); // 設置token失效時間 Long timeOutAccessToken = DateUtil.offset(new Date(), DateField.SECOND, object.getInteger("expires_in") - 120).getTime(); object.put("timeOutAccessToken", timeOutAccessToken); // refresh_token有效期為30天 object.put("timeOutRefreshToken", DateUtil.offsetDay(new Date(), 30)); mapData.put(openId, JSON.toJSONString(object)); return object.getString("access_token"); } }
4.測試
測試代碼

package com.ldp.user.controller; import org.junit.jupiter.api.Test; /** * @Copyright (C) 四川千行你我科技股份技有限公司 * @Author: lidongping * @Date: 2021-01-04 17:16 * @Description: */ class WeChatLoginControllerTest { // 個人測試 private static String appId = "wxeb91796d8f74dbb1"; private static String redirectUri = "http://lidongping.free.idcfengye.com/api/wc/notify/code"; /** * 通過code獲取openid (微信通知地址) * http://192.168.5.195:8080/api/wc/notify/code (http://lidongping.free.idcfengye.com/api/wc/notify/code) * <p> * 通過openid獲取用戶信息 * http://192.168.5.195:8080/api/wc/userInfo?openId=oNHe35yo1LCRfTd5TGytemISl4xs */ /** * 獲取授權鏈接(注意鏈接只能在微信公眾號里面打開) */ @Test void getCodeUrl() { String url = "https://open.weixin.qq.com/connect/oauth2/authorize?appid=%s&redirect_uri=%s&response_type=code&scope=snsapi_userinfo&state=STATE#wechat_redirect"; url = String.format(url, appId, redirectUri); System.out.println(url); } }
獲取code的鏈接:
https://open.weixin.qq.com/connect/oauth2/authorize?appid=wxeb91hh798f74dbb1&redirect_uri=http://lidongping.free.idcfengye.com/api/wc/notify/code&response_type=code&scope=snsapi_userinfo&state=STATE#wechat_redirect
通過code獲取token日志如下
2021-01-06 16:01:11.733-[a8a444bc-705]-[ INFO ] [ c.l.u.c.i.HttpServletRequestWrapperFilter ] - ContentType: null 2021-01-06 16:01:11.734-[a8a444bc-705]-[ INFO ] [ c.l.u.c.i.HttpServletRequestWrapperFilter ] - 請求地址: http://lidongping.free.idcfengye.com/api/wc/notify/code 2021-01-06 16:01:11.734-[a8a444bc-705]-[ INFO ] [ c.l.u.c.i.HttpServletRequestWrapperFilter ] - 請求方法: GET 2021-01-06 16:01:11.945-[a8a444bc-705]-[ INFO ] [ com.ldp.user.controller.WeChatLoginController ] - code=011wGO0w3fGCCV2Q5f3w3F92W02wGO08,state=STATE 2021-01-06 16:01:11.945-[a8a444bc-705]-[ INFO ] [ com.ldp.user.controller.WeChatLoginController ] - 第二步:通過code換取網頁授權access_token,請求url=https://api.weixin.qq.com/sns/oauth2/access_token?appid=wxeb996ddd74dbb1&secret=e7aeb4be6dd33d7fe33cfd4580f36&code=011wGO0w3fGCCV2Q5f3w3F92W02wGO08&grant_type=authorization_code 2021-01-06 16:01:12.681-[a8a444bc-705]-[ INFO ] [ com.ldp.user.controller.WeChatLoginController ] - 響應結果:{"access_token":"40_rg_AbGORcVygaz45XxihaF1Qzd5HCZaO0FbEssxhCAxwgoBajWEtozl1GLFtEPQ3YI-Gir-KMjwzkUPcE--SnhEicwwd1P3W8w0e3FXe8lg","expires_in":7200,"refresh_token":"40_PDo-sss9H6Shvh6LRX6VgU2wFWfKxlAevJ5879ij9uYqlSKunrxiPKX9S16INvlTp5jczRw-Nu9bSrHzLKrj0lzNdmE9I68Hg4vG_Wz3je-iU","openid":"oNHe35yo1LCRfTd5TsssGytemISl4xs","scope":"snsapi_userinfo"} 2021-01-06 16:01:12.732-[a8a444bc-705]-[ INFO ] [ c.l.u.c.i.HttpServletRequestWrapperFilter ] - 響應結果: {"message":"success","code":100,"data":"oNHe35ysso1LCR5TGytemISl4xs"} 2021-01-06 16:01:12.732-[a8a444bc-705]-[ INFO ] [ c.l.u.c.i.HttpServletRequestWrapperFilter ] - HTTP狀態: 200 2021-01-06 16:01:12.732-[a8a444bc-705]-[ INFO ] [ c.l.u.c.i.HttpServletRequestWrapperFilter ] - 處理時長: 998毫秒
通過openid獲取用戶行測試日志如下:
測試地址:http://127.0.0.1:8080/api/wc/userInfo?openId=oNHe35yo1LCRfTd5TGytemIxs
2021-01-06 16:04:29.835-[b7caeebc-7ef]-[ INFO ] [ c.l.u.c.i.HttpServletRequestWrapperFilter ] - ContentType: null 2021-01-06 16:04:29.835-[b7caeebc-7ef]-[ INFO ] [ c.l.u.c.i.HttpServletRequestWrapperFilter ] - 請求地址: http://127.0.0.1:8080/api/wc/userInfo 2021-01-06 16:04:29.835-[b7caeebc-7ef]-[ INFO ] [ c.l.u.c.i.HttpServletRequestWrapperFilter ] - 請求方法: GET 2021-01-06 16:04:29.838-[b7caeebc-7ef]-[ INFO ] [ com.ldp.user.controller.WeChatLoginController ] - 拉取用戶信息 請求url=https://api.weixin.qq.com/sns/userinfo?access_token=40_rg_AbGORcVyg45XxigggghaF1Qzd5HCZaO0FbExhCAxwgoBarFjWEtozl1GLFtEPQ3YI-Gir-KMjwzkUPcE--SnhEicwwd1P3W8w0e3FXe8lg&openid=oNHe35yoggg1LCRfTd5TGytemISl4xs&lang=zh_CN 2021-01-06 16:04:30.197-[b7caeebc-7ef]-[ INFO ] [ com.ldp.user.controller.WeChatLoginController ] - 響應結果:{"openid":"oNHe35yo1LggCRfTd5TGytemISl4xs","nickname":"陽光飛陽","sex":1,"language":"zh_CN","city":"成都","province":"四川","country":"中國","headimgurl":"https:\/\/thirdwx.qlogo.cn\/mmopen\/vi_32\/yaZDgUs7xJHcxMsCcbLbQgU2cJvn9iajDeW8Dj2gic9UfHgBggWgshNiaIWUcpsVqz4RTLEl5aJ3FtQHKoMicicNVQVRw\/132","privilege":[]} 2021-01-06 16:04:30.202-[b7caeebc-7ef]-[ INFO ] [ c.l.u.c.i.HttpServletRequestWrapperFilter ] - 響應結果: {"message":"success","code":100,"data":"{\"openid\":\"oNHe35yo1ggLCRfTd5TGemISl4xs\",\"nickname\":\"陽光飛陽\",\"sex\":1,\"language\":\"zh_CN\",\"city\":\"成都\",\"province\":\"四川\",\"country\":\"中國\",\"headimgurl\":\"https:\\/\\/thirdwx.qlogo.cn\\/mmopen\\/vi_32\\/yaZDgUs7xJHcxMsCcbLbQgU2cJvn9iajDeW8Dj2gic9UfHgBWgshNiaIWUcpsVqz4RTLEl5aJ3FtQHKoMicicNVQVRw\\/132\",\"privilege\":[]}"} 2021-01-06 16:04:30.203-[b7caeebc-7ef]-[ INFO ] [ c.l.u.c.i.HttpServletRequestWrapperFilter ] - HTTP狀態: 200gg 2021-01-06 16:04:30.203-[b7caeebc-7ef]-[ INFO ] [ c.l.u.c.i.HttpServletRequestWrapperFilter ] - 處理時長: 367毫秒
從日志可以看出已經獲得了用戶的基本信息(昵稱、性別、地區、頭像等)
到這里微信登陸的主要邏輯就已經完成了,如果還是不理解可以看視頻,該博客已錄制成視頻講解,或者單獨問我
更多的微信開發相關可以看之前的微信公眾號開發教程。