微信授权登录包含有几种场景:一是公众号;二是H5页面;三是其它APP;本文主要讲述公众号及H5页面的实现,关于其它APP的应用,请参考官方文档。

1. 公众号与H5

公众号应用其实也是H5页面,不过是H5页面在微信浏览器中打开。但这两种方式有着本质的区别。其最关键的不同在于,通过公众号打开H5页面可以直接弹出微信授权登录对话框,如下所示:

html5微信登陆 微信授权登录h5页面_html5微信登陆

而使用其它浏览器只能打开一个二维码的页面:

html5微信登陆 微信授权登录h5页面_公众号授权登录_02

在手机端要使用微信扫码,这个体验是非常之差劲的,只可惜微信目前并未对这种方式进行改进。对于H5页面如果想要兼容常规浏览器和公众号页面的,目前只有这种方式实现,聊胜于无!

另外一点需要注意的是:普通的h5页面应用和公众号应用,是两个应用,如在微信开放平台后台:

html5微信登陆 微信授权登录h5页面_微信_03

要实现公众号里面直接弹出微信授权登录对话框,需要使用公众账号中的应用,而普通浏览器,又需要使用网站应用。因此,我们必须在前端页面中识别出当前是不是微信浏览器打开的,然后再在后台接口中判断是使用哪个应用进行接口调用!不得不说腾讯这块真的是够烂,包括后续要写的支付那块,使用上与阿里的体验差距太大。

2. 应用创建

  • 进公众平台申请公众号;
  • 进微信开放平台申请网页应用;

注意如果只是在公众号中使用可以无需申请网页应用。如果想要兼容两个应用场景,那么两个都需要申请。

申请过程很简单,不再细述。

等待应用审核通过就可以继续下一步了。

注意应用审核通过后需要在应用详情中修改应用回调域:

html5微信登陆 微信授权登录h5页面_微信授权登录_04

应用回调域只能设置一个,比如我的应用是有两个地址的,一是m.ttcn.vip一是www.ttcn.vip,但在微信上只能设置一个,因此只好设置成www.ttcn.vip,然后通过后台的nginx进行跳转。

应用回调域就是在微信授权登录成功后返回的页面所在域名,如果授权登录时传的域名与此处配的域名不匹配,将会报redirect_uri参数错误异常。

3. 实现

微信授权登录等第三方授权登录基本采用的都是OAuth2的方式,具体实现原理可以查找相关文档,本文主要聚焦微信授权登录方面。与微信授权登录相关的流程如下所示:


Created with Raphaël 2.2.0 开始 JS获取微信授权码 微信跳转回调页面 Java获取Token及OpenId Java使用Token获取用户信息 结束


3.1 组装获取授权码的请求参数

需要包含关键的几个参数如下:

参数

说明

appid

应用id,从微信开放平台中查看,注意公众号应用和网页应用的appid是不一样的

redirect_uri

授权成功后的回调页面地址,微信将会将授权成功后的code作为URL参数来对回调页面进行调用

因此回调页面主要用于接收授权成功后的授权码,用于后续的接口调用

注意该参数需要进行URL编码

response_type

授权码模式写死code,其它参考官方文档

scope

应用授权作用域,拥有多个作用域用逗号(,)分隔,网页应用目前仅填写snsapi_login

公众号应用可以填写snsapi_userinfo

state

自定义参数

前端需要将这些参数拼装后跳转到微信授权登录页面去,需要注意的是,公众号与网页应用跳转的页面是不一样的,公众号需要跳转到:

https://open.weixin.qq.com/connect/oauth2/authorize

而网页应用需要跳转到:

https://open.weixin.qq.com/connect/qrconnect

前端VUE示例如下所示:

<template>
    <div>
       <el-button @click="wxLogin">微信登录</el-button>      
    </div>
</template>
<script>
	export default {
        methods: {
            wxLogin() {
                // 获取浏览器标识,判断需要使用公众号还是网页应用
                var url = 'http://.../wx-login';  
                let UA = navigator.userAgent.toLocaleLowerCase();
                var loginUrl = "https://open.weixin.qq.com/connect/qrconnect?";
                if (UA.indexOf("micromessenger") !== -1) {
                    // 使用公众号appId
                    loginUrl =
                        "https://open.weixin.qq.com/connect/oauth2/authorize?";
                    loginUrl +=
                        "appid=wx8******6c849c&" +
                        "redirect_uri=" +
                        encodeURIComponent(url) +
                        "&response_type=code&" +
                        "&scope=snsapi_userinfo&state=1#wechat_redirect";
                } else {
                    // 使用网页应用appId
                    loginUrl +=
                        "appid=wxba******847&" +
                        "redirect_uri=" +
                        encodeURIComponent(url) +
                        "&response_type=code&" +
                        "&scope=snsapi_login#wechat_redirect";
                }

                window.location.replace(loginUrl);
            }
        }
    }
</script>

3.2 回调页面实现

微信授权登录成功后将会在指定的回调页面后拼上code参数返回,如假设回调页面是http://…/wx-login,那么返回的页面路径将会是:http://…/wx-login?code=…,在使用Vue实现的页面中,我们可以使用this.$route.query.code来获取到这个授权码,然后调用后台接口使用授权码获取token/openId并最终获取用户信息,wx-login.vue页面示例如下:

<template>
    <div class="wx-login-page">
        <user-bind
            :initUserInfo="userInfo"
            :state="state"
            v-if="needBind"
        ></user-bind>
    </div>
</template>

<script>
import userBind from "@/components/UserBind";

export default {
    components: { userBind },
    props: [],
    data() {
        return {
            wxCode: "",
            userInfo: {},
            needBind: false,
            state: 0,
        };
    },
    mounted() {
        let UA = navigator.userAgent.toLocaleLowerCase();
        this.state = UA.indexOf("micromessenger") !== -1 ? 1 : 0; 
        this.wxCode = this.$route.query.code;
    },
    methods: {
        getToken() {
            // 根据wxCode调用后台服务获取微信用户的openId,获取用户信息
            this.$get("/base/login/wx", {
                code: newValue,
                state: this.state,
            }).then((resp) => {
                if (resp.success) {
                    // 不需要绑定,openId已经存在
                    this.$setCookie("accessToken", resp.loginInfo.key);
                } else {
                    // 如果是微信端绑定,需要绑定mpOpenId
                    let openId =
                        resp.userInfo.openId || resp.userInfo.openid;

                    // 需要绑定
                    this.userInfo = {
                        nickname: resp.userInfo.nickname,
                        image:
                        resp.userInfo.headImage ||
                        resp.userInfo.headimgurl,
                        sex: resp.userInfo.sex,
                    };
                    if (0 === this.state) {
                        this.userInfo.wxOpenId = openId;
                    } else {
                        this.userInfo.mpOpenId = openId;
                    }

                    this.needBind = true;
                }
            });
        }
    },
};
</script>

<style lang="scss">
.wx-login-page {
}
</style>

以上页面主要就是调用后台/base/login/wx接口获取token/openId及用户信息;由于本项目应用场景需要,如果授权登录的用户是首次登录,还需要绑定手机号,因此后台会有一个判断,如果根据获取到的openId查询到的用户为空,则会调用微信获取用户信息的接口获取到用户信息并返回给前端,前端跳转到绑定页面进行用户绑定。而如果不是首次登录,则不需要获取用户信息及绑定这个步骤了。

另外,后台用户表设计了两个openId字段,一个存储网页应用的,一个存储公众号的;因为两个应用下的用户openId是不一样的。

3.3 后台获取openId及token实现

回调页面中调用的/base/login/wx接口实现如下:

/**
     * 微信登录
     *
     * @param code  微信授权后的Code
     * @param state 登录方式,0:非微信浏览器;1:微信浏览器;
     * @return 登录信息
     */
@GetMapping("/wx")
public ThirdLoginResultDTO wxLogin(@RequestParam("code") String code,
                                   @RequestParam(value = "state", defaultValue = "0") Integer state) {
    // 先根据code获取openId
    WxAuthInfo authInfo = wxService.getAuthInfo(code, state);
    String openId = authInfo.getOpenId();

    ThirdLoginResultDTO result = new ThirdLoginResultDTO();

    // 根据openId查找用户
    Optional<UserDTO> userOptional = userService.findByWxOpenId(openId, state);
    if (userOptional.isPresent()) {
        // 如果已经存在,直接登录后返回
        UserDTO user = userOptional.get();
        BiValue<String, UserDTO> biValue = loginService.localLogin(user);
        result.setLoginInfo(biValue);
        result.setSuccess(true);
        return result;
    }

    // 不存在,返回给前端相关信息并进行绑定
    // 如果openId不存在,则需要将昵称等返回前端,在前端关联手机号进行绑定
    ThirdUserInfo thirdUserInfo = wxService.getUserInfo(authInfo, state);
    result.setSuccess(false);
    result.setUserInfo(thirdUserInfo);
    return result;
}

注意我这有一个本地登录的方法,因为我的后台服务使用的是OAuth2的授权模式,授权成功后需要自动根据用户信息获取到接口访问需要的accessToken,否则前端所有的接口访问仍旧是未授权的。

wxservice.getAuthInfo的实现如下:

/**
     * 通过授权码获取token等授权信息
     *
     * @param code  授权码
     * @param state 微信登录来源,0:非微信浏览器,1:微信浏览器
     * @return 授权信息
     */
public WxAuthInfo getAuthInfo(String code, Integer state) {
    String url = "https://api.weixin.qq.com/sns/oauth2/access_token";
    String result = RestClient.of(url)
        .addUrlParam("appid", state.equals(0) ? APP_ID : MOBILE_APP_ID)
        .addUrlParam("secret", state.equals(0) ? APP_SECRET : MOBILE_APP_SECRET)
        .addUrlParam("code", code)
        .addUrlParam("grant_type", "authorization_code")
        .get(String.class);

    if (logger.isDebugEnabled()) {
        logger.debug("微信获取token结果: {}", result);
    }

    JSONObject jsonObject = JSONObject.parseObject(result);
    String accessToken = jsonObject.getString("access_token");
    String openid = jsonObject.getString("openid");
    WxAuthInfo authInfo = new WxAuthInfo();
    authInfo.setAccessToken(accessToken);
    authInfo.setOpenId(openid);
    return authInfo;
}

getUserInfo实现如下:

/**
     * 获取微信登录用户信息
     *
     * @return 用户信息
     */
public ThirdUserInfo getUserInfo(WxAuthInfo authInfo, Integer state) {
    String infoUrl = "https://api.weixin.qq.com/sns/userinfo";
    String str = RestClient.of(infoUrl)
        .addUrlParam("access_token", authInfo.getAccessToken())
        .addUrlParam("openid", authInfo.getOpenId())
        .get(String.class);

    if (logger.isDebugEnabled()) {
        logger.debug("微信获取用户信息结果: {}", str);
    }

    if (str.contains("errorcode")) {
        throw BusinessException.create("微信接口调用失败,请尝试其它登录方式");
    }

    ThirdUserInfo userInfo = JSONObject.parseObject(str, ThirdUserInfo.class);
    return userInfo;
}

这样整个微信授权登录的过程就已经完成。