本机号码一键登录!推荐 Flutter 极光认证插件

10,486 阅读9分钟

一键登录举例

本机号码一键登录基本成为各个 APP 的标配了。

传统的手机验证码登录方式:

  1. 输入手机号;
  2. 前端判断手机号是否有效;
  3. 后台判断手机号是否有效;
  4. 借助第三方下发手机验证码;
  5. 前端有一个倒计时,防止验证码过期输入;
  6. 手机收到验证码;
  7. 复制粘贴输入验证码校验;
  8. 将信息发给后台接口,验证手机号和验证码是否正确,然后才是注册和登录流程,下发认证 Auth 信息。

这一套流程下来足够费劲了。而上述的「本机号码一键登录」完全无感,只要一点按钮,所有操作一步到位。

今天我们来说一说如何利用「极光」做到「本机号码一键登录」的。

开通「极光认证」

开通「极光认证」,需要先完成实名认证,实名认证后可以领取 1,000 次极光认证礼包。

认证成功后,填入 android 和 iOS 包名、签名等信息。

开通后,即可领取免费使用次数。

注:签名生成工具 apk 包下载链接:res.wx.qq.com/open/zh_CN/…

安装极光认证插件

看过之前文章的朋友应该知道我们公司的 APP 是基于 Flutter 开发的,而且用到极光的推送插件:《推荐一款 Flutter Push 推送功能插件》mp.weixin.qq.com/s/l0_lghnp7…

这里我们还是使用极光认证提供的 Flutter 插件,根据官方提供的安装方法有 github 源代码集成和 pub 集成,这里我推荐 pub 集成方法:

dependencies:
  jverify: 0.6.1

插件功能

有了插件,接下来就可以写功能了。先来看看插件都提供哪些功能,这里我们主要看 Flutter 插件源码,

具体包括:

  1. setup
  2. setDebugMode
  3. isInitSuccess
  4. checkVerifyEnable
  5. getToken
  6. verifyNumber
  7. loginAuth
  8. loginAuthSyncApi
  9. preLogin
  10. dismissLoginAuthView
  11. setCustomUI (注释掉了该功能)
  12. setCustomAuthViewAllWidgets
  13. clearPreLoginCache
  14. setCustomAuthorizationView

极光认证提供了两个方向性的功能:验证手机号和一键登录功能,今天我们主要是用到一键登录功能,整个插件使用流程大致是这样的:

开发决策流程图

主要用到的核心功能有:

  1. setup
  2. setDebugMode
  3. isInitSuccess
  4. checkVerifyEnable
  5. preLogin
  6. setCustomAuthViewAllWidgets
  7. loginAuth

每个功能都挺好理解的,可以过一遍代码。

  /// 统一 key
  final String f_result_key = "result";
  /// 错误码
  final  String  f_code_key = "code";
  /// 回调的提示信息,统一返回 flutter 为 message
  final  String  f_msg_key  = "message";
  /// 运营商信息
  final  String  f_opr_key  = "operator";
  
  final Jverify jverify = new Jverify();

setup

由于我的 APP 是 Flutter 一套开发的,所以在用 setup() 时带上 appKeychannel 属性,Android 则在配置文件中配置。

在 /android/app/build.gradle 中添加极光上创建应用的配置信息,我之前用到极光推送了,用的同一个应用。

// 注册极光认证
jverify.setup(
    appKey: "6592925ae1*****658473",//"你自己应用的 AppKey",
    channel: "devloper-default");

setDebugMode

这个简单,就是是否需要 debug 模式,在开发中建议打开,你也可以创建一个全局变量,在开发中默认打开,在打包 release 模式下是关闭的,避免在 APP 开发过程和打包过程中反复去修改这个状态

jverify.setDebugMode(isDebug); // 是否打开调试模式

isInitSuccess

判断 sdk 初始换是否成功:

jverify.isInitSuccess().then((map) {
  bool result = map[f_result_key];
  setState(() {
    if (result) {
      _result = "sdk 初始换成功";
    }else {
      _result = "sdk 初始换失败";
    }
  });

checkVerifyEnable

判断当前的手机网络环境是否可以使用认证。

jverify.checkVerifyEnable().then((map) {
  bool result = map[f_result_key];
  setState(() {
    if (result) {
      _result = "当前网络环境【支持认证】!";
    }else {
      _result = "当前网络环境【不支持认证】!";
    }
  });
});

以上的几个方法基本都是在 sdk 初始化和验证是否可以满足一键登录条件。

preLogin

当环境满足一键登录后,我们就开始进行一键登录操作了,但操作之前,官网提示我们最好先进行 preLogin SDK 一键登录预取号操作,理由是:

sdk会缓存预取号结果,提升之后授权页拉起速度。所以建议拉起授权页前,比如在开屏页或者业务入口页预先调用此接口进行预取号。

请求成功后,不要频繁重复调用。

不要在预取号回调中重复调用预取号或者拉起授权页接口。

jverify.preLogin().then((map) {
  int code = map[f_code_key];
  String message = map[f_msg_key];
  setState(() {
    _loading = false;
    _result = "[$code] message = $message";
  });
});

好了,这时候是万事具备,只欠东风了,但,就如我们开篇的截图,我们必须要有一个界面,用于提示我们的用户,我们需要开始一键登录操作,并得到他们的认可,以及获取个人信息的说明,也就需要下一个功能的定制化实现。

setCustomAuthViewAllWidgets

jverify.setCustomAuthorizationView(true, uiConfig, landscapeConfig: uiConfig);

这里需要注意的是:

Android 横屏的 UI 配置,只有当 isAutorotate=true 时必须传(也就是方法中的第一个参数),并且该配置只生效在 Android,iOS 使用 portraitConfig 的约束适配横屏。

横竖屏的 UI 则需要根据自己的 APP 设计风格来定制,demo 中提供简要的配置:

final screenSize = MediaQuery.of(context).size;
final screenWidth = screenSize.width;
final screenHeight = screenSize.height;
bool isiOS = Platform.isIOS;

/// 自定义授权的 UI 界面,以下设置的图片必须添加到资源文件里,
/// android项目将图片存放至drawable文件夹下,可使用图片选择器的文件名,例如:btn_login.xml,入参为"btn_login"。
/// ios项目存放在 Assets.xcassets。
/// 
JVUIConfig uiConfig = JVUIConfig();
//uiConfig.authBackgroundImage = ;

//uiConfig.navHidden = true;
uiConfig.navColor = Colors.red.value;
uiConfig.navText = "coding01登录";
uiConfig.navTextColor = Colors.blue.value;
uiConfig.navReturnImgPath = "return_bg";//图片必须存在

uiConfig.logoWidth = 100;
uiConfig.logoHeight = 100;
//uiConfig.logoOffsetX = isiOS ? 0 : null;//(screenWidth/2 - uiConfig.logoWidth/2).toInt();
uiConfig.logoOffsetY = 10;
uiConfig.logoVerticalLayoutItem = JVIOSLayoutItem.ItemSuper;
uiConfig.logoHidden = false;
uiConfig.logoImgPath = "logo";

uiConfig.numberFieldWidth = 200;
uiConfig.numberFieldHeight = 40 ;
//uiConfig.numFieldOffsetX = isiOS ? 0 : null;//(screenWidth/2 - uiConfig.numberFieldWidth/2).toInt();
uiConfig.numFieldOffsetY = isiOS ? 20 : 120;
uiConfig.numberVerticalLayoutItem = JVIOSLayoutItem.ItemLogo;
uiConfig.numberColor = Colors.blue.value;
uiConfig.numberSize = 18;

uiConfig.sloganOffsetY = isiOS ? 20 : 160;
uiConfig.sloganVerticalLayoutItem = JVIOSLayoutItem.ItemNumber;
uiConfig.sloganTextColor = Colors.black.value;
uiConfig.sloganTextSize = 15;
//uiConfig.sloganHidden = 0;

uiConfig.logBtnWidth = 220;
uiConfig.logBtnHeight = 50;
//uiConfig.logBtnOffsetX = isiOS ? 0 : null;//(screenWidth/2 - uiConfig.logBtnWidth/2).toInt();
uiConfig.logBtnOffsetY = isiOS ? 20 : 230;
uiConfig.logBtnVerticalLayoutItem = JVIOSLayoutItem.ItemSlogan;
uiConfig.logBtnText = "登录按钮";
uiConfig.logBtnTextColor = Colors.brown.value;
uiConfig.logBtnTextSize = 16;
uiConfig.loginBtnNormalImage = "login_btn_normal";//图片必须存在
uiConfig.loginBtnPressedImage = "login_btn_press";//图片必须存在
uiConfig.loginBtnUnableImage = "login_btn_unable";//图片必须存在


uiConfig.privacyState = true;//设置默认勾选
uiConfig.privacyCheckboxSize = 20;
uiConfig.checkedImgPath = "check_image";//图片必须存在
uiConfig.uncheckedImgPath = "uncheck_image";//图片必须存在
uiConfig.privacyCheckboxInCenter = true;
//uiConfig.privacyCheckboxHidden = false;

//uiConfig.privacyOffsetX = isiOS ? (20 + uiConfig.privacyCheckboxSize) : null;
uiConfig.privacyOffsetY = 15;// 距离底部距离
uiConfig.privacyVerticalLayoutItem = JVIOSLayoutItem.ItemSuper;
uiConfig.clauseName = "协议1";
uiConfig.clauseUrl = "http://www.baidu.com";
uiConfig.clauseBaseColor = Colors.black.value;
uiConfig.clauseNameTwo = "协议二";
uiConfig.clauseUrlTwo = "http://www.hao123.com";
uiConfig.clauseColor = Colors.red.value;
uiConfig.privacyText = ["1极","2光","3认","4证"];
uiConfig.privacyTextSize = 13;
//uiConfig.privacyWithBookTitleMark = true;
//uiConfig.privacyTextCenterGravity = false;


uiConfig.privacyNavColor =  Colors.red.value;;
uiConfig.privacyNavTitleTextColor = Colors.blue.value;
uiConfig.privacyNavTitleTextSize = 16;
uiConfig.privacyNavTitleTitle1 = "协议1 web页标题";
uiConfig.privacyNavTitleTitle2 = "协议2 web页标题";
uiConfig.privacyNavReturnBtnImage = "return_bg";//图片必须存在;

这里就不再对每个参数进行说明了,很好理解。

好了接下来就是我们客户端的最后一步了,调起一键登录界面和逻辑。

loginAuth

这里提供了两种方法:同步和异步,就看自身业务逻辑需要了。

/// 方式一:使用同步接口 (如果想使用异步接口,则忽略此步骤,看方式二)
/// 先,添加 loginAuthSyncApi 接口回调的监听
jverify.addLoginAuthCallBackListener((event){
  setState(() {
    _loading = false;
    _result = "监听获取返回数据:[${event.code}] message = ${event.message}";
  });
  print("通过添加监听,获取到 loginAuthSyncApi 接口返回数据,code=${event.code},message = ${event.message},operator = ${event.operator}");
});
/// 再,执行同步的一键登录接口
jverify.loginAuthSyncApi(autoDismiss: true);


/// 方式二:使用异步接口 (如果想使用异步接口,则忽略此步骤,看方式二)

/// 先,执行异步的一键登录接口
jverify.loginAuth(true).then((map) {

  /// 再,在回调里获取 loginAuth 接口异步返回数据(如果是通过添加 JVLoginAuthCallBackListener 监听来获取返回数据,则忽略此步骤)
  int code = map[f_code_key];
  String content = map[f_msg_key];
  String operator = map[f_opr_key];
  setState(() {
    _loading = false;
    _result = "接口异步返回数据:[$code] message = $content";
  });
  print("通过接口异步返回,获取到 loginAuth 接口返回数据,code=$code,message = $content,operator = $operator");
});

到此,基本完成客户端代码工作了,我们执行下看看效果:

「coding01一键登录」:

如果返回码为:6000,则 message 的值就是我们需要的 loginToken。

下面我们开始后台的接口对接工作了,我们可以将 loginToken 传给我们自己的服务器接口,然后再利用极光提供的REST API 提供的 loginTokenVerify API 获取加密的手机号数据,注册或者登录操作,下发给客户端创建用户或者登录成功后的 Auth 认证信息。

loginTokenVerify API

功能说明:提交loginToken,验证后返回手机号码

调用接口:POST api.verification.jpush.cn/v1/web/logi…

我主要还是基于 Laravel PHP 框架来使用 loginTokenVerify API,这里使用的是 GuzzleHttp 网络请求插件:

/*
* 通过客户端提供的 loginToken,请求极光接口获得加密手机号
*/
public function jiguanVerify($loginToken)
{
    $client = new Client(['base_uri' => $this->loginTokenVerifyUrl]);
    try {
        $response = $client->request('POST', '', [
            'json' => [
                'loginToken' => $loginToken
            ],
            'auth' => [env('JPUSH_APPKEY'), env('JPUSH_MASTERSECRET')],
        ]);

        $contents = json_decode($response->getBody()->getContents(), true);

        // 正确结果
        // {"id":117270465679982592,"code":8000,"content":"get phone success","exID":"1234566","phone":"HpBLIQ/6SkFl0pAq0LMdw1aZ8RHoofgWmaY//LE+0ahkSdHC5oTCnjrR8Tj8y5naKVI03torFU+EzAQnwtVqAoQyYckT0S3Q02TKuAal3VRGiR5Lmp4g2A5Mh4/W5A4o6QFviHuBVJZE/WV0AzU5w4NGhpyQntOeF0UyovYATy4="}
        // 失败结果
        // {"id":268773997490073600,"code":8001,"content":"get phone fail","exID":null,"phone":null}
        if ( $contents['code'] == 8000) {
            return [
                'verify' => true,
                'message' => $this->getPhone($contents['phone']) // 解码获取手机号
            ];
        }

        return [
            'verify' => false,
            'message' => $contents['content']
        ];
    } catch (RequestException $e) {
        return [
            'verify' => false,
            'message' => $e->getMessage()
        ];
    } catch (GuzzleException $e) {
        return [
            'verify' => false,
            'message' => $e->getMessage()
        ];
    }
}

当然根据官网说明,通过 loginTokenVerify API 接口返回的手机号是加密的,需要进行解密,一开始申请认证时,我们在极光后台配置了我们的「RSA 加密公钥」,这时候就派上用场了。

具体看方法 $this->getPhone($contents['phone'])

/**
 * @param $phone
 * @return string 解密的手机号
 */
private function getPhone($phone)
{
    $prefix = '-----BEGIN RSA PRIVATE KEY-----';
    $suffix = '-----END RSA PRIVATE KEY-----';
    $result = '';

    $encrypted = null;
    $prikey = null;

    $key = $prefix . "\n" . $prikey . "\n" . $suffix;
    openssl_private_decrypt(base64_decode($encrypted), $result, openssl_pkey_get_private($key));

    return $result . "\n";
}

拿到手机号后,那剩下的就是和我们业务流程有关的代码了,利用手机号登录用户信息,或者创建用户,然后下发登录成功的 Auth 信息给我们的客户端。

总结

有了极光认证提供的一键登录功能,我们客户端开发就变得很简单,不再需要用户自己手动输入手机号,客户端和接口端去验证手机号的有效性、下发验证码到第三方短信平台、再由短信平台下发给用户,用户再去客户端去输入验证码,然后验证成功,再把信息提交给接口,接口拿着手机号去做认证操作。

所有的操作都不需要了,用户只需点一点「本地手机号一键登录」即可,剩下的都交给我们开发来完成,而且我们开发工作量也变得很少,只需要一个请求接口就可完成登录功能

这就是极光认证功能 —— 一键登录的作用。

参考阅读

  • 推荐一款 Flutter Push 推送功能插件,这是使用极光推送的 Flutter 插件,可以简化推送功能开发,推送一步到位,推荐阅读:mp.weixin.qq.com/s/l0_lghnp7…

  • 跟我一步一步实现 Flutter 视频播放插件,这是一篇早期 Flutter 刚在国内使用时,针对视频播放器 SDK 制作的 Flutter 插件,后来有些大厂工程师们也参考这篇文章去使用和开发插件,强烈推荐一看:mp.weixin.qq.com/s/goMXcCpcq…

  • JPush's officially supported Flutter plugin (Android & iOS). 极光推送官方支持的 Flutter 插件(Android & iOS)。而本文的 demo 代码主要来自此,推荐查看源代码,是学习的最好途径。github.com/jpush/jveri…

下一步我们来解读 Flutter 插件源码,未完待续