SpringBoot整合微信公众号开发(二)| 扫码关注登录

1,607 阅读2分钟

「这是我参与11月更文挑战的第16天,活动详情查看:2021最后一次更文挑战」。

前言

上一篇 我们已经打通了微信服务端和我们本地项目之间的通道,接下来,我们来实现扫码登录的功能。

登录逻辑

  • 用户扫码
  • 带参传到服务端,服务端根据信息获取用户的具体信息
  • 本地服务端根据获取到的微信信息去做逻辑处理,用户已存在则登录,返回登录认证信息,不存在则先注册,之后返回登录认证信息。

具体实现

目前微信服务端已经可以和本地实现通信,接下来我们要去调取微信服务端的接口获取用户的信息,以及公众号二维码等信息。

获取这些信息的前提是获取用户微信服务端授权,基于Oauth2协议,我们已经有了appID,appsecret,接下来获取accessToken,就可以了。

因为频繁的使用到accessToken,所有这里我存在了redis里。

具体实习可以看后面的工具类

1. 获取微信公众号二维码

获取一个带参数的二维码,用户扫码时会将用户一些信息以及参数传给后端,接下我们就可以围绕着这些参数做文章了

2. 扫码触发事件

这里有个小坑,之前看文档一直不明白,微信扫码触发事件,参数是通过xml包的形式发送给我们的,所以正常拿是拿不到的,这里需要整合一下xml解析的工具类,拿到参数。

主要是我们可以拿到用户的openid,之后就是获取用户信息,然后具体的登录逻辑就可以根据实际情况去写代码了。

添加依赖

<dependency>
    <groupId>org.dom4j</groupId>
    <artifactId>dom4j</artifactId>
    <version>2.1.1</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.thoughtworks.xstream/xstream -->
<dependency>
    <groupId>com.thoughtworks.xstream</groupId>
    <artifactId>xstream</artifactId>
    <version>1.4.18</version>
</dependency>

实现逻辑(伪代码)

@ApiOperation(value = "用户关注/或者取消关注")
@PostMapping("/wechat")
public void follow(HttpServletRequest request) throws Exception {
    try {
        // 接受扫描二维码回调参数
        Map<String, String> map = new HashMap<>();
        // 读取输入流
        SAXReader reader = new SAXReader();
        Document document = reader.read(request.getInputStream());
        // 得到xml根元素
        Element root = document.getRootElement();
        XmlUtil.parserXml(root, map);
        log.info("【map】= {}", map);
        String userInfo = weChatUtil.getUserInfo(request.getParameter("openid"));
        JSONObject userInfoJson = JSONUtil.parseObj(userInfo);
        String openid = userInfoJson.getStr("openid");
        String eventKey = map.get("EventKey").replace("qrscene_", "");
        log.info(eventKey);
        // 监听扫码回调事件
        String event = map.get("Event");
        switch (event) {
            // 扫码触发
            case "SCAN":
                //查询用户存在就直接返回登录信息,不存在先注册再返回
                break;
            //  关注
            case "subscribe":
                //查询用户存在就直接返回登录信息,不存在先注册再返回
                break;
            //  取关
            case "unsubscribe":
                System.out.println("取关了");
                break;
            default:
                log.info("【map】= {}", map);
                break;
        }
    } catch (Exception e) {
        throw new ServiceException("获取微信用户信息异常");
    }
}

工具类

XmlUtil

public class XmlUtil {
    /**
     * 扩展 xstream,获取CDATA内容
     */
    public static XStream xstream = new XStream(new XppDriver() {
        @Override
        public HierarchicalStreamWriter createWriter(Writer out) {
            return new PrettyPrintWriter(out) {
                // 对所有xml节点的转换都增加CDATA标记
                boolean cdata = true;

                @Override
                public void startNode(String name, @SuppressWarnings("rawtypes") Class clazz) {
                    super.startNode(name, clazz);
                }

                @Override
                protected void writeText(QuickWriter writer, String text) {
                    if (cdata) {
                        writer.write("<![CDATA[");
                        writer.write(text);
                        writer.write("]]>");
                    } else {
                        writer.write(text);
                    }
                }
            };
        }
    });

    /**
     * xml解析为map
     *
     * @param root 根节点
     * @param map  返回的map
     */
    @SuppressWarnings("unchecked")
    public static void parserXml(Element root, Map<String, String> map) {
        // 得到根元素的所有子节点
        List<Element> elementList = root.elements();
        // 判断有没有子元素列表
        if (elementList.size() == 0) {
            map.put(root.getName(), root.getText());
        } else {
            // 遍历
            for (Element e : elementList) {
                parserXml(e, map);
            }
        }
    }
}

WeChatUtil

@Component
public class WeChatUtil {

    @Value("${wechat.appID}")
    private String appID;

    @Value("${wechat.appsecret}")
    private String appsecret;

    @Autowired
    private RedisCache redisCache;

    private static final String ACCESS_TOKEN_URL = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s";
    private static final String TICKET_URL = "https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token=%s";
    private static final String USER_INFO_URL = "https://api.weixin.qq.com/cgi-bin/user/info?access_token=%s&openid=%s&lang=zh_CN";


    /**
     * 获取 access_token
     */
    public String getAccessToken() {
        String accessTokenUrl = String.format(ACCESS_TOKEN_URL, appID, appsecret);
        String s = HttpUtil.get(accessTokenUrl);
        JSONObject result = JSONUtil.parseObj(s);
        String access_token = result.getStr("access_token");
        Integer expires_in = result.getInt("expires_in");
        redisCache.setCacheObject(Constants.ACCESS_TOKEN, access_token, expires_in, TimeUnit.SECONDS);
        return access_token;
    }

    /**
     * 获取 创建二维码的ticket
     * @param scene_str
     */
    public JSONObject getTicket(String scene_str) throws Exception {
        String ticketUrl = String.format(TICKET_URL, getReidAccessToken());
        String body = "{" +
                ""expire_seconds": 1200," +
                ""action_name": "QR_STR_SCENE"," +
                ""action_info": {" +
                ""scene": {" +
                ""scene_str": "" + scene_str + """ +
                "}" +
                "}" +
                "}";
        String ticketJson = HttpUtil.post(ticketUrl, body);
        return JSONUtil.parseObj(ticketJson);
    }

    /**
     * 根据 openid 和 access_token 获取用户信息
     */
    public String getUserInfo(String openid) throws Exception {
        String userInfoUrl = String.format(USER_INFO_URL, getReidAccessToken(), openid);
        return HttpUtil.get(userInfoUrl);
    }

    private String getReidAccessToken() throws Exception {
        String access_token = redisCache.getCacheObject(Constants.ACCESS_TOKEN);
        if (StringUtils.isBlank(access_token)) {
            // access_token 过期了
            // 刷新 access_token
            access_token = getAccessToken();
        }
        return access_token;
    }

}