天天看點

Java實作微信登入(網頁授權)

1.背景

實際開發中,使用第三方登入是非常常見的業務...

這樣可以大提高使用者體驗,沒必要一來就要注冊,或者登入之類的...

并且開發一個登入或者注冊嚴格來說也是非常麻煩的(各種防止攻擊、機器操作等)

2.準備公衆号和測試環境

需要準備的如下

1.appid
2.appSecret
3.外網可以通路的映射位址      

如果你有服務号、并且是認證了的(這些認證需要企業資質),當然很好,通常來時如果你是學習應該沒有

即使沒有也沒關系,微信提供了測試賬号,并且擁有很多權限,開發好後,隻要替換為公司的生産公衆号就可以使用了

擷取測試公衆号步驟如下:

打開連結:https://developers.weixin.qq.com/doc/offiaccount/Getting_Started/Overview.html

建議初學者認證讀一下微信的開發文檔,正常情況下如果你是做開發很大機率會經常用到微信相關的接口

Java實作微信登入(網頁授權)

 點選測試号申請界面如下:

Java實作微信登入(網頁授權)

 點選微信登陸,掃碼即可快速獲得一個微信公衆号測試

Java實作微信登入(網頁授權)

 頁面下方有接口權限,設定網頁回調位址

Java實作微信登入(網頁授權)

如下,注意隻寫域名,不要寫http之類的

如果沒有外網位址可以使用外網映射:https://www.cnblogs.com/newAndHui/p/14241177.html  (免費、簡單、三步搞定)

Java實作微信登入(網頁授權)

 到此公衆号配置已經完成

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)      

具體實作代碼:

Java實作微信登入(網頁授權)
Java實作微信登入(網頁授權)
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");
    }

}      

View Code

4.測試

測試代碼

Java實作微信登入(網頁授權)
Java實作微信登入(網頁授權)
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毫秒      

從日志可以看出已經獲得了使用者的基本資訊(昵稱、性别、地區、頭像等)

到這裡微信登陸的主要邏輯就已經完成了,如果還是不了解可以看視訊,該部落格已錄制成視訊講解,或者單獨問我

更多的微信開發相關可以看之前的微信公衆号開發教程。

完美!

繼續閱讀