Java实现微信登录(网页授权)

时间:2024-02-29 13:24:48

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");
    }

}
View Code

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);
    }
}
View Code

获取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毫秒

从日志可以看出已经获得了用户的基本信息(昵称、性别、地区、头像等)

到这里微信登陆的主要逻辑就已经完成了,如果还是不理解可以看视频,该博客已录制成视频讲解,或者单独问我

更多的微信开发相关可以看之前的微信公众号开发教程。

完美!