uniapp 集成苹果登录Sign in with Apple

记录一下之前上架苹果应用商店遇到的坑。因为App使用微信登录没有接入苹果登陆,被官方审核驳回。苹果登录是 iOS13 新增加的功能,当你的应用使用了第三方登录比如微信登录,同时也需要集成苹果登录,否则提交AppStore审核会被拒绝。 详情参考:App Store 审核指南 - 通过 Apple 登录

一、在 HBuilderX 配置 apple 登录

在项目根目录找到manifest.json文件,点击即可跳转配置页。选择App模块配置,将苹果登陆勾选上。

二、使用苹果登录首先需要在苹果开发者后台开启 App 的 Sign In with Apple 服务。

  1. 登录到苹果开发者后台 编辑对应的 Identifier 勾选 Sign In with Apple 服务并保存
  2. 勾选或取消服务,会导致之前的 profile 描述文件失效,不需要新建,只要点击 Edit 重新编辑对应的 profile文件,然后保存下载使用新的profile文件

三、代码集成

在页面中放置苹果登陆按钮组件。这里苹果对于这个按钮是有要求的。我这里使用苹果官方推荐的圆形按钮。官方提供的基本满足使用条件了。如有特殊需求按官方要求设计按钮即可。如下所示,这里因为我的模拟器没有安装微信和QQ,所以不会出现按钮。

然后这里也要注意一个问题,因为苹果授权登录(Sign in with Apple)是 iOS 13 才有的,所以要调用 uni.getSystemInfo()获取系统信息做下系统版本判断。

这里我是用的是5+App 的api 你也可以使用uniapp 封装的api。uni.login()

	  //#ifdef APP-PLUS
      appleLogin() {
        var appleOauth = null;
        plus.oauth.getServices(function(services) {
          for (var i in services) {
            var service = services[i];
            // 获取苹果授权登录对象,苹果授权登录id 为 'apple' iOS13以下系统,不会返回苹果登录对应的 service    
            if (service.id == 'apple') {
              appleOauth = service;
              break;
            }
          }
          if (!appleOauth) {
            plus.nativeUI.toast('暂不支持apple账户登陆')
            return
          }
          appleOauth.login(function(oauth) {
            // 授权成功,苹果授权返回的信息在 oauth.target.appleInfo 中   
            plus.nativeUI.showWaiting('登陆中...')
            //向后台发送登陆需要的参数
          }, function(err) {
            // 授权失败 error 
            console.log(err);
            plus.nativeUI.toast('授权登陆失败')
          }, {
            // 默认只会请求用户名字信息,如需请求用户邮箱信息,需要设置 scope: 'email'    
            scope: 'email'
          })
        }, function(err) {
          console.log(err);
          // 获取 services 失败  
        })
      },
      //#endif

uni.login()授权成功回调示例

{
    "appleInfo": "getUserInfo:ok",
    "rawData": "json字符串",
    "userInfo": {
        "openId": "xxx.xxxxx.xxx", // 苹果用户唯一标识符,该值在同一个开发者账号下的所有 App 下是一样的,开发者可以用该唯一标识符与自己后台系统的账号体系绑定起来。
        "fullName": {}, // 当且仅当第一次授权才会返回
        "authorizationCode": "12345678xxx", // 服务器验证需要使用的参数
        "identityToken": "header.payload.signature", // 服务器验证需要使用的参数
        "realUserStatus": 1 // 用于判断当前登录的苹果账号是否是一个真实用户
    },
    "signature": ""
}

5+App 的授权成功回调示例

{
  "target": {
    "id": "apple",
    "description": "Apple",
    "authResult": {
      "access_token": "eyJraWQiOiJlWGF1bm1MIiwiYWxnIjoiUlMyNTYifQ.eyJpc3MiOiJodHRwczovL2FwcGxlaWQuYXBwbGUuY29tIiwiYXVkIjoiY29tLmFsaGVscC53b3JkQXBwIiwiZXhwIjoxNTk2MzA4NzQwLCJpYXQiOjE1OTYzMDgxNDAsInN1YiI6IjAwMTM3My5jZDg4NmJkNWRlZWY0MjI3OGViOTg3ZDgwYTY3YjRlNi4wNDM4IiwiY19oYXNoIjoiX1Z1UzBOZ0RFY1BNanFQRmVWRUJoQSIsImVtYWlsIjoiOTlpd2szZG5iMkBwcml2YXRlcmVsYXkuYXBwbGVpZC5jb20iLCJlbWFpbF92ZXJpZmllZCI6InRydWUiLCJpc19wcml2YXRlX2VtYWlsIjoidHJ1ZSIsImF1dGhfdGltZSI6MTU5NjMwODE0MCwibm9uY2Vfc3VwcG9ydGVkIjp0cnVlfQ.EW6ANPxkiHCQPz297Q_0ZhCQvOJ1tpnQGYG2w82kIwQRiQOZydYzXg2pHDf20ITpGwwSnuXRbuCW4PkpUPunTf8VdM6N9qPVuTszvTQ9C6awwSdt9BoeNrkztaiHTxdzpJOHZyeHFlFRIbTxpqggdFqwLR361F475aN1McdOVsZriOnRdaL7PBd3Ua5di0I2NqAQBn94MCOzX3rWHMpDx2MhuDePMsJo_a_tzWQS6M_4PvtJbP0fa-p8s4Hjezk9uoimIwOD-Qdzg4HdLMwGJzzmU3qEoV6g4nJrcWnaGoywsrXVO85r81v59d4To079OMUJUYcf2LUv0ur0DWO91Q",
      "openid": "001373.cd886bd5deef42278eb987d80a67b4e6.0438" // 苹果用户唯一标识符,该值在同一个开发者账号下的所有 App 下是一样的,开发者可以用该唯一标识符与自己后台系统的账号体系绑定起来。
    },
    "userInfo": {
      "openid": "001373.cd886bd5deef42278eb987d80a67b4e6.0438"
    },
    "appleInfo": {
      "authorizationCode": "c2d2cfbe49e174d5c914ce07c3e2c0119.0.nrtxt.DhS_Ovj6R_LVtM8qvC4yww",
      "fullName": {},
      "identityToken": "eyJraWQiOiJlWGF1bm1MIiwiYWxnIjoiUlMyNTYifQ.eyJpc3MiOiJodHRwczovL2FwcGxlaWQuYXBwbGUuY29tIiwiYXVkIjoiY29tLmFsaGVscC53b3JkQXBwIiwiZXhwIjoxNTk2MzA4NzQwLCJpYXQiOjE1OTYzMDgxNDAsInN1YiI6IjAwMTM3My5jZDg4NmJkNWRlZWY0MjI3OGViOTg3ZDgwYTY3YjRlNi4wNDM4IiwiY19oYXNoIjoiX1Z1UzBOZ0RFY1BNanFQRmVWRUJoQSIsImVtYWlsIjoiOTlpd2szZG5iMkBwcml2YXRlcmVsYXkuYXBwbGVpZC5jb20iLCJlbWFpbF92ZXJpZmllZCI6InRydWUiLCJpc19wcml2YXRlX2VtYWlsIjoidHJ1ZSIsImF1dGhfdGltZSI6MTU5NjMwODE0MCwibm9uY2Vfc3VwcG9ydGVkIjp0cnVlfQ.EW6ANPxkiHCQPz297Q_0ZhCQvOJ1tpnQGYG2w82kIwQRiQOZydYzXg2pHDf20ITpGwwSnuXRbuCW4PkpUPunTf8VdM6N9qPVuTszvTQ9C6awwSdt9BoeNrkztaiHTxdzpJOHZyeHFlFRIbTxpqggdFqwLR361F475aN1McdOVsZriOnRdaL7PBd3Ua5di0I2NqAQBn94MCOzX3rWHMpDx2MhuDePMsJo_a_tzWQS6M_4PvtJbP0fa-p8s4Hjezk9uoimIwOD-Qdzg4HdLMwGJzzmU3qEoV6g4nJrcWnaGoywsrXVO85r81v59d4To079OMUJUYcf2LUv0ur0DWO91Q",// 服务器验证需要使用的参数
      "realUserStatus": 1,
      "user": "001373.cd886bd5deef42278eb987d80a67b4e6.0438"
    }
  }
}

后端从前端接收到 identityToken 字符串之后,由于 identityToken 是一个 JWT,所以这里需要安装如下第三方库对 identityToken 进行解析。

composer require firebase/php-jwt

接下来还需要获取解密 JWT 的 JWK。可以通过访问 https://appleid.apple.com/auth/keys 得到一个 keys 列表,也就是 JWK 列表。这也就意味着客户端向服务器提交的 identityToken 可能是用 keys 里面的特定某个 JWK 来进行加密的。 获取到keys 后,需要确定到底是使用哪个JWK加密的

$tks = explode('.', $identityToken);
list($headb64, $bodyb64, $cryptob64) = $tks;
$header = JWT::jsonDecode(JWT::urlsafeB64Decode($headb64));
$key_used = $keys_map[$header->kid];

确定了 JWK 之后,还需要安装如下第三方库将 JWK 转换为 PEM。

composer require codercat/jwk-to-pem

PHP代码示例

/**
     * 苹果登陆
     * auth_time: 签名时间
     * @param $params
     * @return array|bool
     */
    public function apple_login($params)
    {
        try {
            $apple_info = $params['target']['appleInfo'];

            $identityToken = $apple_info['identityToken'];

            // 获取 JWK 列表
            $keys_map = $this->get_apple_auth_keys();

            if (!$keys_map) {
                exception('获取JWK列表');
            }

            // 定位用于加密当前 identityToken 的 JWK
            $tks = explode('.', $identityToken);
            list($headb64, $bodyb64, $cryptob64) = $tks;
            $header = JWT::jsonDecode(JWT::urlsafeB64Decode($headb64));
            $key_used = $keys_map[$header->kid];

            //获取用户的数据
            $jwkConverter = new JWKConverter();
            $publicKey = $jwkConverter->toPEM($key_used);

            $userinfo = JWT::decode($identityToken, $publicKey, ['RS256']);

            if (!$userinfo) {
                exception('获取用户信息失败');
            }      
			// $userinfo里的sub 参数即可用于绑定用户信息,相当于weixin 的unionid。这里只有用户第一次授权登陆时能获取到用户的邮箱信息,第二次就获取不到了
            }

            return $data;
        } catch (Exception $e) {
            $this->setError($e->getMessage());
            return false;
        } catch (ExpiredException $e) {
            $this->setError($e->getMessage());
            return false;
        }
    }
	
	/**
     * Apple公钥
     * @return array|bool|mixed
     */
    public function get_apple_auth_keys()
    {
        $keys_map = Redis::instance()->hGetJson('apple_auth', 'apple_auth_keys');
        if (!$keys_map) {
            $request_url = 'https://appleid.apple.com/auth/keys';
            $response = Http::get($request_url);
            $resData = json_decode($response, true);

            $keys = $resData['keys'];
            $keys_map = [];
            foreach ($keys as $key) {
                $keys_map[$key['kid']] = $key;
            }
            Redis::instance()->hSetJson('apple_auth', 'apple_auth_keys', $keys_map, 86400 * 30);
        }
        return $keys_map;
    }

获取到的用户信息示例

{
  "data": {
    "code": 1,
    "msg": "操作成功",
    "data": {
      "iss": "https://appleid.apple.com",//签发机构,苹果
      "aud": "com.xxx.xxx",//接收者,目标app,对应Apple开发者账户中的client_id
      "exp": 1596308740,//过期时间
      "iat": 1596308140,//签发时间
      "sub": "001373.cd886bd5deef42278eb987d80a67b4e6.0438",//它是用户的唯一标识符,很重要,和微信unionid 一样
      "c_hash": "_VuS0NgDEcPMjqPFeVEBhA",//authorizationCode的hash值,用于验证authorizationCode
      "email": "99iwk3dnb2@privaterelay.appleid.com",//用户邮箱
      "email_verified": "true",//邮箱是否经过验证
      "is_private_email": "true",//是否是加密邮箱
      "auth_time": 1596308140,//请求授权的时间
      "nonce_supported": true //指示对待nonce的方式, true: 如果授权请求时有给nonce,但返回的token不含nonce,则说明此次请求失败.。false: 不支持nonce,忽略nonce
    }
  },
  "statusCode": 200,
  "errMsg": "request:ok"
}

参考文章:https://blog.csdn.net/zou8944/article/details/105167965/

    • CAIException
    • 沉默纪年灬
蔡关荣博客
请先登录后发表评论
  • latest comments
  • 总共0条评论