OpenID 和 UnionID 是什么
OpenID 是微信提供给开发者的用户唯一标识。然而当开发者拥有多个移动应用、网站应用、和公众账号(包括小程序)时,同一用户、不同应用下的 OpenID 是不一样的。

而同一个微信开放平台账号下的不同应用,用户的 UnionID 是唯一的。

因此,对有多个应用的开发者来讲,只有通过 UnionID 来区分用户的唯一性,才能实现多个应用间的账号打通。

应该使用哪个 ID 登录
理论上讲,当产品有App、小程序、公众号等多种形式时,用 UnionID 是最好的选择,否则会因为同一用户在不同应用下的 OpenID 不一样导致产生多个账号,那处理起来就很麻烦了。

然而 UnionID 并不是那么容易获取的。微信小程序最新的限制是:

必须使用一个专用按钮控件让用户主动点击,否则无法弹出授权弹窗
用户必须点击「允许」同意小程序获取公开信息

以上2步,每一步都会造成一定的用户流失。所以有的开发者会使用 OpenID ,以最大程度的降低用户获取的成本,从而推动注册用户数的快速增长。

拿拼多多来说,用户打开小程序之后会静默获取 OpenID 并生成账号,可以正常使用购物车、历史记录等服务。用户在「个人中心」页面点击「更新资料」时就会触发授权弹窗获取公开信息和 UnionID。

对于不同的业务场景,会有各自最适合的选择。深入研究下微信的机制,或许能有一些启发,在某些场景下可以尝试优化用户获取的路径。

小程序 OpenID 和 UnionID 获取机制
在小程序的官方文档里有一张图解释了小程序调用wx.login接口的登录流程。不过那张图除了开发能看懂,一般人应该都看不懂什么意思。翻译成一般人都能看懂的图,小程序登录流程是这样的:

avatar

首先,开发者可以在小程序中静默调用登录接口,拿到一个凭证
小程序把凭证发送到开发者的服务器上
服务器拿着这个凭证以及小程序密钥向微信接口请求换取 OpenID
微信接口返回 OpenID 给开发者服务器,满足特殊条件时会一并返回 UnionID
开发者服务器创建登录态并返回给小程序,从而完成登录

总结一下:

OpenID可以无感获取。

而无感获取 UnionID 必须满足以下任一条件:

用户已经关注了同主体的公众号
用户已经授权过同主体的其他应用获取 UnionID
用户刚刚通过小程序完成了支付
否则就必须让用户主动点击按钮并允许获取公开信息后,才可以获得 UnionID。
公开信息有哪些
最后说说微信的用户公开信息(UserInfo)究竟包含哪些信息:

微信昵称
微信头像图片的URL,如果用户没有头像,URL会是空的。如果用户更换了头像,原有头像的URL会失效
用户性别:未知、男性、女性
所在国家
所在省份
所在城市
国家、省份、城市所用的语言:英文、简体中文、繁体中文
与用户信息一并返回的还有一串加密信息,转交给开发者的服务器解密之后,就可以得到用户的 OpenID 和 UnionID 了。

一件比较tricky的事情是,如果只是需要在小程序中展示用户头像和昵称,可以使用 ,微信在渲染小程序的时候会显示用户的头像和昵称。但是此时只是显示出来能被用户看到,开发者并不能拿到用户头像昵称的数据,所以这个时候就不要想什么分享到聊天的时候小程序卡片标题能带上用户昵称了。

<open-data type="userNickName"></open-data>
<open-data type="userAvatarUrl"></open-data>
<open-data type="userGender"></open-data>

通过除了可以不经过授权直接展示头像、昵称之外,还可以直接展示:

用户性别
用户所在国家
用户所在城市
用户所在省份
用户的语言
群名称(必须是用户曾经分享过小程序的群)

获取用户信息。页面产生点击事件(例如 button 上 bindtap 的回调中)后才可调用,每次请求都会弹出授权窗口,用户同意后返回 userInfo。该接口用于替换 wx.getUserInfo,小程序代码如下:

<template>
    <view>
        <!-- #ifdef MP-WEIXIN -->
        <view>
            <view>
                <view class='header'>
                    <!-- 自己的小程序logo -->
                    <image src='/static/image/logo.png'></image>
                </view>
                <view class='content'>
                    <view>申请获取以下权限</view>
                    <text>获得你的公开信息(昵称,头像、地区等)</text>
                </view>
                <button class='bottom' type='primary' @click="GetUserInfo">
                    授权登录
                </button>
            </view>
        </view>
        <!-- #endif -->
    </view>
</template>

<script>
    import {code2session} from 'api/login' //接口js,自己写
    export default {
        data() {
            return {
                
            }
        },
        methods: {
            GetUserInfo() {
                uni.getUserProfile({
                    lang:'zh_CN',
                    desc: '用户授权登陆', // 声明获取用户个人信息后的用途,后续会展示在弹窗中,请谨慎填写
                    success: (res) => {
                        uni.setStorageSync('userInfo', res.userInfo)	
                        this.Login()//微信登陆
                    }
                })
            },
            Login(){
                uni.login({
                    provider: 'weixin',
                    success: (loginRes) => {
                        let code = loginRes.code;
                        code2session({
                            userinfo:uni.getStorageSync('userInfo'),//向服务端提交用户信息
                            code: code
                        }).then(res=>{
                            uni.setStorageSync('openid',res.data.openid)
                            uni.setStorageSync('unionid',res.data.unionid)
                            uni.setStorageSync('uid',res.data.uid)//uid为可选
                            uni.showModal({
                                content:res.msg,
                                showCancel: false,
                                success: (res) => {
                                    if (res.confirm) {
                                        //回到主页
                                        this.backHome()
                                    }
                                }
                            })
                        })
                    }
                });
            },
            backHome(){
                uni.reLaunch({
                    url:'/pages/index/index'//自己小程序的主页
                })
            },
        }
    }
</script>

<style>
    .header {
        margin: 90rpx 0 90rpx 50rpx;
        border-bottom: 1px solid #ccc;
        text-align: center;
        width: 650rpx;
        height: 300rpx;
        line-height: 450rpx;
    }

    .header image {
        width: 200rpx;
        height: 200rpx;
    }

    .content {
        margin-left: 50rpx;
        margin-bottom: 90rpx;
    }

    .content text {
        display: block;
        color: #9d9d9d;
        margin-top: 40rpx;
    }

    .bottom {
        border-radius: 80rpx;
        margin: 70rpx 50rpx;
        font-size: 35rpx;
    }
</style>

服务端代码参考

<?php

namespace app\wechat\controller;

use app\BaseController;
use EasyWeChat\Factory;
use think\facade\Config;
use think\facade\Db;

class MiniProgram extends BaseController
{
    protected $app;
    protected $config=[];
    protected $table='wx_user';
    public function __construct()
    {
        $this->config= Config::get('apikey');
        $config = [
            // 必要配置
            'app_id'             => $this->config['mp_appid'],
            'secret'             => $this->config['mp_appsecret'],
            'response_type'      => 'array'
        ];
        $this->app = Factory::miniProgram($config);
    }
    public function code2session(){
        $info=input('userinfo');//用户信息
        $code=input('code');
        $res=$this->app->auth->session($code);
        //业务逻辑
        $query=Db::name($this->table)->where('unionid',$res['unionid'])->find();
        //如果unionid已存在
        if($query){
            Db::name($this->table)->where('unionid',$res['unionid'])->update([
                'username'=>$info['nickName'],
                'avatar'=>$info['avatarUrl'],
                'gender'=>$info['gender'],
                'mp_openid'=>$res['openid'],
                'city'=>$info['city'],
                'session_key'=>$res['session_key'],//会话密钥,以后用
                'province'=>$info['province'],
                'update_time'=>date('Y-m-d H:i:s',time())
            ]);
        }
        else{
            Db::name($this->table)->insert([
                'username'=>$info['nickName'],
                'avatar'=>$info['avatarUrl'],
                'gender'=>$info['gender'],
                'mp_openid'=>$res['openid'],
                'city'=>$info['city'],
                'province'=>$info['province'],
                'session_key'=>$res['session_key'],//会话密钥,以后用
                'unionid'=> $res['unionid'],
                'status'=>1,
                'create_time'=>date('Y-m-d H:i:s',time())
            ]);
        }
        if(isset($res['errcode'])){
            return success('登陆失败',$res['errmsg']);
        }
        return success('登陆成功',$res);
    }
}

session_key 的作用

那么,session_key在登录的过程中或者登录完成后起什么作用呢?一起来看一下。

首先来看一下wx.getUserInfo 这个api:

在设置withCredentials 属性为true 的情况下,这个api 可以拿到encryptedData,iv 等敏感信息,encryptedData 需要使用session_key 进行解密,解密后可以拿到的数据如下:

也就是说,session_key的作用之一是将小程序前端从微信服务器获取到的encryptedData 解密出来,获取到openId 和unionId等信息。

但是在1.2登录过程中可以看到开发者服务器是能够直接拿到用户的openId信息,而且unionId 也是有其他获取途径,所以session_key 在这里的作用看起来有点鸡肋。

session_key 更重要的作用大概体现在获取用户手机方面(可能还包含其他敏感信息获取api)。

从文档中可以看到getPhoneNumber 返回的用户数据是加密过的,只有使用session_key才能解密,而小程序前端没有session_key,所以无法获取到用户的手机,只能传到开发者服务器进行处理。

unionId 的作用,有哪些获取途径?

UnionID机制说明

如果公司拥有多个移动应用、网站应用、和公众帐号(包括小程序),可通过unionid来区分用户的唯一性,因为只要是同一个微信开放平台帐号下的移动应用、网站应用和公众帐号(包括小程序),用户的unionid是唯一的。换句话说,同一用户,对同一个微信开放平台下的不同应用,unionid是相同的。

Tip:unionid 用于识别同一主体下不同账号之间的用户。举例说明:就是公司有A订阅号,B服务号,同一个人关注A和B,会得到不同的OPENID,但是会得到相同的unionid。这样就可以识别到相同的用户,用于不同账号之间打通用户关系。

UnionID获取途径

必须有一个微信开放平台账号绑定了至少一个微信公众账号或者网站应用或者小程序,否则UnionID返回null。绑定了开发者帐号的小程序,可以通过下面3种途径获取UnionID。

方法一:调用接口wx.getUserInfo,从解密数据中获取UnionID。注意本接口需要用户授权,请开发者妥善处理用户拒绝授权后的情况。

方法二:如果开发者帐号下存在同主体的公众号,并且该用户已经关注了该公众号。开发者可以直接通过wx.login获取到该用户UnionID,无须用户再次授权。

方法三:如果开发者帐号下存在同主体的公众号或移动应用,并且该用户已经授权登录过该公众号或移动应用。开发者也可以直接通过wx.login获取到该用户UnionID,无须用户再次授权。

code2session接口返回参数如下:

在应用中如何保存用户登录态

保存用户登录态,一直以来都有两种解决方案:前端保存和后端保存。

后端保存

写session的时候可以直接设定过期时间,定期通知小程序前端重新进行登录(wx.login)。

前端保存

因为session_key 存在时效性问题(毕竟是用来查看敏感信息),而小程序前端可以通过wx.checkSession() 来检查session_key 是否过期。所以可以通过这个来作为保存用户登录态的机制,这也是小程序文档中推荐的方法: