flutter -- 一键登陆(根据流量获取手机号码)

6,892 阅读3分钟

一键登录彻底简化了用户的操作流程,传统要输入手机号,然后点击发送验证码,再点击登录,使用一键登录就不用输入手机号,直接用本机号码登录。

这里我们使用极光的一键登录【我没收极光的钱哈】

准备材料

申请bundle ID

进入开发者苹果平台,申请开发者,一年需要688人民币,然后申请bundle id,也叫应用id,具体根据提示来,不知道的就搜一下,然后把bundle ID复制下来,粘贴到xcode里面。

申请应用

进入开发者平台极光开发者平台 申请账号后创建应用

审核通过后,点击应用设置
设置安卓和ios信息
保存如下信息

安装环境

插件引用

pubspec.yaml

dependencies:
  jverify: 0.4.0

安卓配置

\android\app\build.gradle

defaultConfig下面添加如下代码

ndk {
            //选择要添加的对应 cpu 类型的 .so 库。
            abiFilters 'armeabi', 'armeabi-v7a', 'x86', 'x86_64', 'mips', 'mips64', 'arm64-v8a'     
        }
        manifestPlaceholders = [
            JPUSH_PKGNAME : applicationId,
            JPUSH_APPKEY : "e579470dae40f090f92c1f3f", // NOTE: JPush 上注册的包名对应的 Appkey.
            JPUSH_CHANNEL : "developer-default", //暂时填写默认值即可.
        ]

IOS配置

无需配置

使用

引入插件

import 'package:jverify/jverify.dart';

初始化

jverify.setDebugMode(true);
jverify.setup(
  appKey: "申请的appkey", 
  channel: "devloper-default"
);

网络校验

jverify.checkVerifyEnable().then((map){
      bool result = map["result"];
      if(result){
        // 当前网络环境支持认证
        print('当前网络环境支持认证');
      }else{
        // 当前网络环境不支持认证
        print('no~~当前网络环境不支持认证');
      }
    });

获取loginAuth

JVUIConfig uiConfig = JVUIConfig();
jverify.setCustomAuthorizationView(false, uiConfig, landscapeConfig: uiConfig, widgets: widgetList);
    /// 步骤 2: 添加 loginAuth 接口回调的监听 (如果想通过 loginAuth 接口异步返回获取接口数据,则忽略此步骤)
    jverify.addLoginAuthCallBackListener((event){
      print("通过添加监听,获取到 loginAuth 接口返回数据,code=${event.code},message = ${event.message},operator = ${event.operator}");
    });
    /// 步骤 3:开始调用一键登录接口        
    jverify.loginAuth(true).then((map){
      /// 步骤 4:获取 loginAuth 接口异步返回数据(如果是通过添加 JVLoginAuthCallBackListener 监听来获取返回数据,则忽略此步骤)
      // print("通过接口异步返回,获取到 loginAuth 接口返回数据,code=$code,message = $content,operator = $operator");
      print("通过接口异步返回,获取到 loginAuth");
      print(map);
    });

(上面widgets,不是必传参数)
在这里,步骤三的数据才是我们需要的。

getToken (目前在一键登陆用不到,做安全校验的时候才用到)

jverify.getToken().then((map){
          int _code = map["code"]; // 返回码,2000代表获取成功,其他为失败,详见错误码描述
          String _token = map["content"]; // 成功时为token,可用于调用验证手机号接口。token有效期为1分钟,超过时效需要重新获取才能使用。失败时为失败信息
          String _operator = map["operator"]; // 成功时为对应运营商,CM代表中国移动,CU代表中国联通,CT代表中国电信。失败时可能为null
          print("一键登录 token code: ${_code} 运营商: ${_operator} _token: ${_token}");
          if(_code == 2000){
                // 请求后台校验
          }else{
            print('没有手机卡,或者异常');
          }
        });

运行逻辑

Jverify jverify = new Jverify();
// 是否调试模式
jverify.setDebugMode(true);
// ios使用的参数
jverify.setup(
  appKey: "e57947打码c1f3f", 
  channel: "devloper-default",
  useIDFA: false
);
jverify.checkVerifyEnable().then((map){
      bool result = map["result"];
      if(result){
        // 获取loginAuth
        loginAuth() // 这部分的代码就是上面 获取loginAuth 的代码
      }
})

效果

全部没有报错的话就是成功了

会有一个界面弹出来

点击登录的按钮,就会执行

jverify.loginAuth(true)

这个的异步回调,数据打印出来是如下所示

调用后会自动全屏,不需要引入view到flutter界面里面渲染

异常情况处理

异常1

有一个安卓手机跑起来后是这样,出问题了,QAQ

信号太差了,会有失败的可能性,请在4G+网络下测试。

异常2

2010

原因: takget sdk 版本>22需要动态申请权限。
不过,我们先申请下静态权限 在Android/app/src/main/AndroidManifest.xml加如下代码。(manifest里面,application之前)

<uses-permission android:name="android.permission.READ_PHONE_STATE" />

最好加下基本的权限

<uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.READ_PHONE_STATE" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
    <uses-permission android:name="android.permission.CHANGE_NETWORK_STATE" />
    <uses-permission android:name="android.permission.WRITE_SETTINGS"/>

    <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" /> <!-- 用于开启 debug 版本的应用在6.0 系统上 层叠窗口权限 -->
    <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
    <uses-permission android:name="android.permission.GET_TASKS" />
    <uses-permission android:name="android.permission.VIBRATE" />
    <uses-permission android:name="android.permission.WAKE_LOCK" />

然后修改Android/app/src/build.gradle (这是不能保证完全没问题,最好是使用动态申请权限,不过测试的时候可以这样做)

targetSdkVersion 22

动态申请权限需要写安卓和iOS两个权限的代码然后封装给flutter使用,不过有现成的插件flutter动态申请权限插件

异常3

4033

I/flutter (11895): {code: 4033, message: appkey is not support login, operator: null}

没有权限,有点奇怪,我什么都明明申请了啊

这里忽略了一个细节,在[应用设置] -> [认证设置] -> [一键登陆], 如图,申请RSA加密,把公钥复制上去,之前我们讲了支付宝支付,里面也是一样的逻辑。

然后提交审核,通过之后再试。

IOS bundle Id设置

ios xocde bundle设置

ios跑的时候需要先在xcode输入bundle ID,不过可能会出现如下错误

(这个错误根据提示错误处理,没有iOS经验还真不好处理,其实这个不需要管它,只要保证Xcode登陆的账号和开发者一致就行,然后bundle id没复制错,这时候连接上手机,就可以了)

后端获取手机号

有两个api,一个是验证,一个登录验证。

区别就是,第一个是校验身份,目前不在我们的使用范围呢,也就是安全认证,比如做支付功能,教程目前使用的是是不是本人,就不能发生验证码,直接校验当前使用的手机是不是登录的账号即可。

第二个就是我们要用的 文档上有一个curl

 curl --insecure -X POST -v https://api.verification.jpush.cn/v1/web/loginTokenVerify -H "Content-Type: application/json" -u "7d431e42dfa6a6d693ac2d04:5e987ac6d2e04d95a9d8f0d1" -d '{"loginToken":"STsid0000001542695429579Ob28vB7b0cYTI9w0GGZrv8ujUu05qZvw","exID":"1234566"}'

看不懂curl不要紧,有一个文章可以学习下。 curl教程
还有一个是吧,curl转成语言的网址,直接生成代码 curl转各个语言代码, 目前支持蛮多的,nodejs 、java、go、dart、php等

只有根据上面的获取到了phone的密文才能解析,直接解析loginToken(就是flutter获取到的token),是不行滴,这个要注意,我卡这里好久了QAQ

用nodejs换取手机号密钥

根据点击一键登陆获取到的token,使用CURL执行,会返回9210(提示参数错误),可以用语言代码解析,我放个nodejs换取手机号密文成功的代码

var request = require('request');

var headers = {
    'Content-Type': 'application/json'
};

var loginToken = "tg9Z2evKEO/rD7naE1JnCYTbMLirZZgn45FrbAynCA0aukCbC8dzjuWAHI+d9ZjB4vs1ac25U8cJxkGbsBZGXssXpn7aR0vQ0M977qIbkDz/e9DsD+ueQFt/iWHV+ZR4YoDYf9+lMrePqLzTyOx4RiDakHQrLUlUC6G7kznJCSI="
var params = {
    loginToken: loginToken,
    exID:""
}
var dataString = JSON.stringify(params)

var options = {
    url: 'https://api.verification.jpush.cn/v1/web/loginTokenVerify',
    method: 'POST',
    headers: headers,
    body: dataString,
    auth: {
        'user': '这个是应用的key',
        'pass': '这个是应用服务端的密码'
    }
};

function callback(error, response, body) {
    if (!error && response.statusCode == 200) {
        console.log(body);
    }
}

request(options, callback);

手机号密文解密

JAVA 解密

import javax.crypto.Cipher;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;

public class RSADecrypt {
    public static void main(String[] args) throws Exception {
        String encrypted = "iZT省略20字veq0=";
        String prikey = "MII省略20字Kqw==";
        String publicKey = "省略20字";
        
        System.out.println("解密 result 111");
        String result = decrypt(encrypted, prikey);
        System.out.println(result);
    }

    public static String decrypt(String cryptograph, String prikey) throws Exception {
        PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(Base64.getDecoder().decode(prikey));
        PrivateKey privateKey = KeyFactory.getInstance("RSA").generatePrivate(keySpec);

        Cipher cipher=Cipher.getInstance("RSA");
        cipher.init(Cipher.DECRYPT_MODE, privateKey);

        byte [] b = Base64.getDecoder().decode(cryptograph);
        return new String(cipher.doFinal(b));
    }
}

亲测可以,如果报错,应该是密文问题。
如有需要可以下方评论找我要加密的方法,pkcs8 java加解密由皮皮鼠(oops)提供。
(如果用自己的解密方法,报错,如果方法没有问题,那么就是少了一个步骤,解密传入的密文需要base64解码,代码: Base64.getDecoder().decode)

有一个很重要的一点,pkcs8适合java,其他语言可以用工具把pkcs8转pkcs1,支付宝开放平台有一个转换的地方

python安装pycryptodome报错了。

php

 <?php
   $prefix = '-----BEGIN RSA PRIVATE KEY-----';
   $suffix = '-----END RSA PRIVATE KEY-----';
   $result = '';

   $encrypted = null;
   $prikey = null;

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

   echo $result . "\n";

nodejs

const crypto = require('crypto')

var privateStr = `MIIXXXXXXXXXXXXXXKqw==`
var publicStr = `MxxxxXXXXXXXXXXXXXxxxQAB`
var 公钥pem = `-----BEGIN PUBLIC KEY-----\n${publicStr.replace(/(.{64})/g, '$1\n')}\n-----END PUBLIC KEY-----`.toString('ascii')
var 私钥pem = `-----BEGIN PRIVATE KEY-----\n${privateStr}\n-----END PRIVATE KEY-----`
var 一键登陆密文 = "";

//私钥解密
const decodeData = crypto.privateDecrypt({key: 私钥pem, padding: crypto.constants.RSA_PKCS1_PADDING}, Buffer.from(一键登陆密文, 'base64'));
console.log("手机号: ", decodeData.toString())

这里有一个需要注意的地方

Buffer.from(密文, 'base64') 不等于 new Buffer(
    new Buffer(密文.toString('base64'), 'base64').toString(),
    'utf-8'
)

(如果密钥公钥没有换行的话,请直接用上面的代码,如果已经下载就是pem文件,那么直接替换[私钥/公钥]pem)

nodejs这一块我摸索了好久,如果有帮助,请点个赞,谢谢~~

相关链接

github.com/jpush/jveri…
docs.jiguang.cn/jverificati…
github.com/jpush/jveri…
github.com/BaseflowIT/…