本机号码一键登录基本成为各个 APP 的标配了。
传统的手机验证码登录方式:
- 输入手机号;
- 前端判断手机号是否有效;
- 后台判断手机号是否有效;
- 借助第三方下发手机验证码;
- 前端有一个倒计时,防止验证码过期输入;
- 手机收到验证码;
- 复制粘贴输入验证码校验;
- 将信息发给后台接口,验证手机号和验证码是否正确,然后才是注册和登录流程,下发认证 Auth 信息。
这一套流程下来足够费劲了。而上述的「本机号码一键登录」完全无感,只要一点按钮,所有操作一步到位。
今天我们来说一说如何利用「极光」做到「本机号码一键登录」的。
开通「极光认证」
开通「极光认证」,需要先完成实名认证,实名认证后可以领取 1,000 次极光认证礼包。
认证成功后,填入 android 和 iOS 包名、签名等信息。
开通后,即可领取免费使用次数。
注:签名生成工具 apk 包下载链接:https://res.wx.qq.com/open/zh_CN/htmledition/res/dev/download/sdk/Gen_Signature_Android2.apk
安装极光认证插件
看过之前文章的朋友应该知道我们公司的 APP 是基于 Flutter 开发的,而且用到极光的推送插件:《推荐一款 Flutter Push 推送功能插件》https://mp.weixin.qq.com/s/l0_lghnp7ECRRO9YswWWSw。
这里我们还是使用极光认证提供的 Flutter 插件,根据官方提供的安装方法有 github 源代码集成和 pub 集成,这里我推荐 pub 集成方法:
dependencies:
jverify: 0.6.1
插件功能
有了插件,接下来就可以写功能了。先来看看插件都提供哪些功能,这里我们主要看 Flutter 插件源码,
具体包括:
- setup
- setDebugMode
- isInitSuccess
- checkVerifyEnable
- getToken
- verifyNumber
- loginAuth
- loginAuthSyncApi
- preLogin
- dismissLoginAuthView
- setCustomUI (注释掉了该功能)
- setCustomAuthViewAllWidgets
- clearPreLoginCache
- setCustomAuthorizationView
极光认证提供了两个方向性的功能:验证手机号和一键登录功能,今天我们主要是用到一键登录功能,整个插件使用流程大致是这样的:
主要用到的核心功能有:
- setup
- setDebugMode
- isInitSuccess
- checkVerifyEnable
- preLogin
- setCustomAuthViewAllWidgets
- 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()
时带上 appKey
和 channel
属性,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 https://api.verification.jpush.cn/v1/web/loginTokenVerify
我主要还是基于 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 插件,可以简化推送功能开发,推送一步到位,推荐阅读:https://mp.weixin.qq.com/s/l0_lghnp7ECRRO9YswWWSw
-
跟我一步一步实现 Flutter 视频播放插件,这是一篇早期 Flutter 刚在国内使用时,针对视频播放器 SDK 制作的 Flutter 插件,后来有些大厂工程师们也参考这篇文章去使用和开发插件,强烈推荐一看:https://mp.weixin.qq.com/s/goMXcCpcqo7Tpb5TE3fCIQ
-
JPush’s officially supported Flutter plugin (Android & iOS). 极光推送官方支持的 Flutter 插件(Android & iOS)。而本文的 demo 代码主要来自此,推荐查看源代码,是学习的最好途径。https://github.com/jpush/jverify-flutter-plugin
下一步我们来解读 Flutter 插件源码,未完待续