闪验一键登录H5
<p>[TOC]</p>
<h3>概述</h3>
<h4>产品说明</h4>
<p>本产品以H5的方式接入运营商闪验功能,适用于移动端网页一键登录功能</p>
<h4>注意点</h4>
<ul>
<li>
<p>同一号码有输入错误次数限制
移动:输入错误三次就会被锁定24小时
电信:连续输错三次锁定24小时,不连续输错不锁定
联通:没有限制</p>
</li>
<li>闪验功能需要识别运营商,因此<strong>不能在Wifi环境下</strong>进行,需要<strong>关闭Wifi</strong></li>
</ul>
<h4>准备工作</h4>
<p>开通闪验功能需要创建应用,运营商审核通过后方能对接。请联系客服提供以下资料:</p>
<ol>
<li>应用名称</li>
<li>应用类型</li>
<li>应用介绍</li>
<li>所属行业</li>
<li>应用ICON(最大:128 X 128 )</li>
<li>登录页地址和域名。地址和域名必须按照实际填写,否则会报referer校验不通过的错误
> 登录页地址和域名务必填写正确,审核通过后不能修改。建议直接提供生产环境的地址,开发测试可通过配置hosts解析到本地或测试环境</li>
</ol>
<p>运营商审核通过后,接入者可获取到应用的<strong>flashValidateAppId</strong>和<strong>flashValidateKey</strong>,后续集成对接中会使用</p>
<h4>业务流程</h4>
<p>本产品接入需要<strong>客户端(浏览器)</strong>和<strong>服务端</strong>均接入才能实现</p>
<p><img src="https://file.jumdata.com/api-document/flash-validate/flash-validate-h5.jpg" alt="" /></p>
<p>演示:<a href="https://api-h5.jumdata.com/flash-validate/demo">https://api-h5.jumdata.com/flash-validate/demo</a></p>
<hr />
<h3>客户端接入</h3>
<h4>准备工作</h4>
<p>Web 页面需引入js和css</p>
<pre><code class="language-html">&lt;script type=&quot;text/javascript&quot; src=&quot;https://api-h5.jumdata.com/flash-validate/flash-validate.min.js&quot;&gt;&lt;/script&gt;
&lt;link type=&quot;text/css&quot; href=&quot;https://www.cmpassport.com/h5/js/jssdk_auth/css/ydrz-layer.css&quot;/&gt;</code></pre>
<p>创建JumeiFlashValidate实例</p>
<pre><code class="language-javascript">// flashValidateAppId开通后服务商提供
// 实例一个页面只需要创建一次,建议在页面加载完成后创建
var jumeiFlashValidate = new JumeiFlashValidate(flashValidateAppId) </code></pre>
<h4>界面配置(可选)</h4>
<ul>
<li>该方法为可选方法,本产品内置有默认界面配置,可以使用默认配置,降低开发及适配难度。</li>
<li>如确需修改界面配置,须在初始化之前通过setUIConfig方法配置。</li>
<li>支持授权页面弹窗/全屏模式、标题、应用logo,隐私协议等属性修改,开发者可选择修改其中一项或多项进行配置。</li>
<li>授权页面弹窗/全屏模式三网均可通过api修改。</li>
<li>标题、应用logo,隐私协议仅移动、联通支持通过api修改,电信需要发对接人员报备,运营商审核通过后方可生效。</li>
</ul>
<h5>全屏模式</h5>
<p><img src="https://file.jumdata.com/api-document/flash-validate/%E5%85%A8%E5%B1%8F.png" alt="" /></p>
<h5>弹窗模式</h5>
<p><img src="https://file.jumdata.com/api-document/flash-validate/%E5%BC%B9%E7%AA%97.png" alt="" /></p>
<h5>配置说明</h5>
<ul>
<li>页面标题:支持配置文案,限制10字以内,默认显示“本机号码登录”,默认水平居中显示。(仅支持全屏模式下)</li>
<li>应用logo:支持配置应用logo,默认水平居中显示</li>
<li>服务协议:在运营商协议后,支持增加配置协议。最多新增2个,总限制20字以内,默认水平居中显示</li>
</ul>
<p>> 注意:该配置仅对移动、联通卡立即生效,电信配置需要发对接人员报备,运营商审核通过后方可生效</p>
<h5>方法说明</h5>
<pre><code class="language-javascript">var config={
setPageType,
setLoginTitle,
setLoginLogo,
setPrivacyOne,
setPrivacyTwo,
}
// jumeiFlashValidate为页面加载后,创建的JumeiFlashValidate实例
jumeiFlashValidate.setUIConfig(config, function (result) {
});</code></pre>
<p>config参数说明</p>
<table>
<thead>
<tr>
<th>参数</th>
<th>类型</th>
<th><div style="width:420px;">说明</div></th>
</tr>
</thead>
<tbody>
<tr>
<td>setPageType</td>
<td>Boolean</td>
<td>设置页面登录模式,全屏:false,弹窗:true,不填默认全屏</td>
</tr>
<tr>
<td>setLoginTitle</td>
<td>string</td>
<td>页面标题</td>
</tr>
<tr>
<td>setLoginLogo</td>
<td>string</td>
<td>平台logo的url,尺寸建议:80x80</td>
</tr>
<tr>
<td>setPrivacyOne</td>
<td>[]</td>
<td>格式:['协议名称','链接地址']协议 1 名称,最多20个字符</td>
</tr>
<tr>
<td>setPrivacyTwo</td>
<td>[]</td>
<td>格式:['协议名称','链接地址']协议 2 名称,最多 20个字符</td>
</tr>
</tbody>
</table>
<h4>初始化</h4>
<p>需要在启动授权页前调用,一般在H5页面加载完成,并实例化JumeiFlashValidate后调用</p>
<h5>方法说明</h5>
<pre><code class="language-javascript">// jumeiFlashValidate为页面加载后,创建的JumeiFlashValidate实例
jumeiFlashValidate.init(function(result){
})</code></pre>
<p>回调函数参数说明</p>
<table>
<thead>
<tr>
<th>参数</th>
<th><div style="width:420px;">说明</div></th>
</tr>
</thead>
<tbody>
<tr>
<td>code</td>
<td>"000000"代表成功,其他代表失败,详见客户端返回码说明</td>
</tr>
<tr>
<td>message</td>
<td>code对应的说明</td>
</tr>
</tbody>
</table>
<h4>启动授权页</h4>
<p>在需要启动一键登录的页面,调用此方法会启动闪验H5内部的授权页面</p>
<h5>方法说明</h5>
<pre><code class="language-javascript">// jumeiFlashValidate为页面加载后,创建的JumeiFlashValidate实例
jumeiFlashValidate.open(function(result){
})</code></pre>
<p>回调函数参数说明 </p>
<table>
<thead>
<tr>
<th>参数</th>
<th>说明</th>
</tr>
</thead>
<tbody>
<tr>
<td>code</td>
<td>"200000"代表成功,其他状态码为失败,详见客户端返回码说明</td>
</tr>
<tr>
<td>message</td>
<td>code对应的说明</td>
</tr>
<tr>
<td>token</td>
<td>可获取置换手机号所需的token,调用服务端接口时需要用到此参数</td>
</tr>
</tbody>
</table>
<h5>注意事项</h5>
<p>触发启动授权页函数的按钮必须设置<strong>id="j-get-code"</strong></p>
<pre><code class="language-html">&lt;button id=&quot;j-get-code&quot;&gt;本机号码登录&lt;/button&gt; </code></pre>
<p>通过监听的方式添加事件,直接在按钮加点击事件,如果是电信号,会被电信的sdk所吞掉事件</p>
<pre><code class="language-javascript">var button = document.getElementById(&#039;j-get-code&#039;)
button.addEventListener(&#039;click&#039;, function () {
})</code></pre>
<p>需要与初始化在同一界面调用,否则电信卡可能会启动不了授权页</p>
<p>如果采用Vue开发,只需把触发启动授权页函数设置<strong>id="j-get-code"</strong>即可,无需执行button.addEventListener</p>
<pre><code class="language-html">&lt;button id=&quot;j-get-code&quot; @click=&quot;open()&quot;&gt;本机号码登录&lt;/button&gt;</code></pre>
<h4>客户端返回码说明</h4>
<table>
<thead>
<tr>
<th>返回码</th>
<th><div style="width:420px;">说明</div></th>
</tr>
</thead>
<tbody>
<tr>
<td>000000</td>
<td>初始化成功</td>
</tr>
<tr>
<td>000400</td>
<td>初始化失败</td>
</tr>
<tr>
<td>000500</td>
<td>自定义配置失败,协议长度不能超过二十字符</td>
</tr>
<tr>
<td>000510</td>
<td>参数错误</td>
</tr>
<tr>
<td>000600</td>
<td>Sdk加载失败</td>
</tr>
<tr>
<td>000700</td>
<td>自定义配置成功</td>
</tr>
<tr>
<td>100001</td>
<td>未知运营商,可能的原因:未关闭wifi、没有信号、移动校验号码错误超限</td>
</tr>
<tr>
<td>100101</td>
<td>用户拒绝授权</td>
</tr>
<tr>
<td>100102</td>
<td>电信号码不匹配(仅电信号返回)</td>
</tr>
<tr>
<td>100103</td>
<td>电信校验错误次数超限(仅电信号返回)</td>
</tr>
<tr>
<td>200000</td>
<td>获取token成功</td>
</tr>
</tbody>
</table>
<hr />
<h3>服务端接入</h3>
<p>客户端授权成功之后,需要由接入者的服务端再调用服务商的获取手机号接口获取手机号,本接口即完成该功能
> 服务端接口需要采用appId、appSecret生成并且传入sign,请不要在客户端直接调用服务商的服务端接口,以免暴露appSecret。应该由接入者的客户端调用接入者的服务端,再由接入者的服务端调用服务商的服务端接口</p>
<h4>请求地址</h4>
<p><code>https://api.jumdata.com/flash-validate/mobile/get</code></p>
<h4>请求方式</h4>
<ul>
<li>POST</li>
</ul>
<h4>请求格式</h4>
<ul>
<li>application/x-www-form-urlencoded</li>
</ul>
<h4>请求参数</h4>
<table>
<thead>
<tr>
<th>名称</th>
<th>类型</th>
<th>必须</th>
<th><div style="width:420px;">说明</div></th>
</tr>
</thead>
<tbody>
<tr>
<td>appId</td>
<td>String</td>
<td>是</td>
<td>服务商分配的唯一标识</td>
</tr>
<tr>
<td>timestamp</td>
<td>Long</td>
<td>是</td>
<td>当前时间戳(毫秒)</td>
</tr>
<tr>
<td>sign</td>
<td>String</td>
<td>是</td>
<td>签名,详见签名算法说明</td>
</tr>
<tr>
<td>flashValidateAppId</td>
<td>String</td>
<td>是</td>
<td>开通后服务商提供,和客户端一致</td>
</tr>
<tr>
<td>token</td>
<td>String</td>
<td>是</td>
<td>闪验客户端启动授权页返回的token</td>
</tr>
<tr>
<td>flashValidateSign</td>
<td>String</td>
<td>是</td>
<td>闪验签名,详见闪验签名说明</td>
</tr>
<tr>
<td>clientIp</td>
<td>String</td>
<td>否</td>
<td>客户端IP,由客户服务端获取的前端APP的IP,如需要使用反欺诈核验功能则传入,否则可以不传</td>
</tr>
</tbody>
</table>
<h4>接口签名算法说明</h4>
<pre><code>sign = sha256(appId + appSecret + timestamp)</code></pre>
<p>用服务商分配的 <strong>appId</strong>、服务商分配的 <strong>appSecret</strong>,当前时间戳(毫秒) <strong>timestamp</strong>,按上述顺序拼接成字符串,再进行 <strong>sha256</strong> 哈希得到。如下:</p>
<pre><code class="language-java">String appId = &quot;xyzxy2121zxyz&quot;;
String timestamp = &quot;1555378976238&quot;;
String appSecret = &quot;efcefcef1121cefcefc1212121&quot;;
String str = appId + appSecret + timestamp;
String sign = sha256(str);</code></pre>
<h4>闪验签名说明</h4>
<pre><code class="language-java">sign = sha256(flashValidateAppId + flashValidateAppKey + token)</code></pre>
<p>闪验应用的 <strong>flashValidateAppId</strong>、<strong>flashValidateAppKey</strong>,闪验授权页返回的 <strong>token</strong>,按上述顺序拼接成字符串,再进行 <strong>sha256</strong> 哈希得到。如下:</p>
<pre><code class="language-java">String flashValidateAppId = &quot;124324343&quot;;
String flashValidateAppKey = &quot;xyzxy21121rewrer12yuyu1zxyz&quot;;
String token = &quot;xyzxy21121rewrer12yuyu1zxyz23123xyzxy21121rewrer12yuyu1zx321321&quot;;
String str = flashValidateAppId + flashValidateAppKey + token;
String sign = sha256(str);</code></pre>
<h4>获取号码成功返回样例</h4>
<pre><code class="language-json">{
&quot;code&quot;: 200,
&quot;msg&quot;: &quot;成功&quot;,
&quot;taskNo&quot;: &quot;987272522132093927999832&quot;,
&quot;charge&quot;: true,
&quot;data&quot;: {
&quot;mobile&quot;: &quot;BB9736648935DDD2F67C784126AEB7E2&quot; // 手机号码,AES加密,详见手机号解密说明
}
}</code></pre>
<h4>获取号码失败返回样例</h4>
<pre><code class="language-json">{
&quot;code&quot;: 301,
&quot;msg&quot;: &quot;取号失败,号码补填不正确&quot;,
&quot;taskNo&quot;: &quot;987272522132093927999832&quot;,
&quot;charge&quot;: true
}</code></pre>
<h4>返回字段说明</h4>
<table>
<thead>
<tr>
<th>字段名</th>
<th><div style="width:420px">说明</div></th>
</tr>
</thead>
<tbody>
<tr>
<td>code</td>
<td>返回码,详见:服务端返回码说明</td>
</tr>
<tr>
<td>msg</td>
<td>code对应的描述</td>
</tr>
<tr>
<td>charge</td>
<td>计费标志 true为计费,false为不计费</td>
</tr>
<tr>
<td>taskNo</td>
<td>本次请求号</td>
</tr>
<tr>
<td>data</td>
<td>返回具体结果,object类型,详见data返回字段描述</td>
</tr>
</tbody>
</table>
<h4>服务端返回码说明</h4>
<table>
<thead>
<tr>
<th>code</th>
<th><div style="width:420px;">说明</div></th>
</tr>
</thead>
<tbody>
<tr>
<td>200</td>
<td>成功(计费)</td>
</tr>
<tr>
<td>301</td>
<td>取号失败,号码补填不正确(计费)</td>
</tr>
<tr>
<td>400</td>
<td>参数错误</td>
</tr>
<tr>
<td>404</td>
<td>接口地址不正确</td>
</tr>
<tr>
<td>500</td>
<td>系统维护,请稍候再试</td>
</tr>
<tr>
<td>601</td>
<td>接口未开通</td>
</tr>
<tr>
<td>602</td>
<td>账号停用</td>
</tr>
<tr>
<td>604</td>
<td>接口停用</td>
</tr>
<tr>
<td>606</td>
<td>调用超限,请稍候再试</td>
</tr>
<tr>
<td>607</td>
<td>ip不在白名单</td>
</tr>
<tr>
<td>609</td>
<td>请求过于频繁,请稍候再试</td>
</tr>
<tr>
<td>610</td>
<td>请求超时</td>
</tr>
<tr>
<td>999</td>
<td>其他,以实际返回为准</td>
</tr>
</tbody>
</table>
<h4>手机号解密说明</h4>
<p>mobile字段采用加密返回,加密算法/分组加密模式/分组填充方式为<strong>AES/CBC/PKCS7Padding</strong>,密文为<strong>16进制</strong>字符串,解密时需要做对应转换。秘钥为<strong>md5(flashValidateKey)前16位字符串</strong>,初始化向量为<strong>md5(flashValidateKey)后16位字符密串</strong></p>
<p>示例代码</p>
<ul>
<li>解密</li>
</ul>
<pre><code class="language-java">String tmp = DigestUtils.md5Hex(flashValidateKey);
String aesKey = StringUtil.substring(tmp, 0, 16); // 密钥
String aesIv = StringUtil.substring(tmp, 16); // 初始化向量
Striing mobile = AES256Util.decrypt(mobileCipher, aesKey, aesIv); // mobileCipher为接口返回的手机号密文</code></pre>
<ul>
<li>AES256Util.java</li>
</ul>
<pre><code class="language-java">package com.anq.jumeidata.openapi.util;
import lombok.extern.slf4j.Slf4j;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.util.encoders.Hex;
import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.io.UnsupportedEncodingException;
import java.security.Security;
/**
* AES加密工具
*/
@Slf4j
public class AES256Util {
private static final String ENCODING = &quot;UTF-8&quot;;
private static final String ALGORITHM = &quot;AES&quot;;
/**
* 加密算法/分组加密模式/分组填充方式
*/
private static final String ALGORITHM_CBC_PADDING = &quot;AES/CBC/PKCS7Padding&quot;;
/**
* 使用PKCS7Padding填充必须添加一个支持PKCS7Padding的Provider
* 类加载的时候就判断是否已经有支持256位的Provider,如果没有则添加进去
*/
static {
if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
Security.addProvider(new BouncyCastleProvider());
}
}
/**
* 加密
*
* @param content 需要加密的内容
* @param secret 密钥
* @param iv 向量
* @return
*/
public static String encrypt(String content, String secret, String iv) throws AES256Exception {
byte[] plainByte;
try {
plainByte = content.getBytes(ENCODING);
} catch (UnsupportedEncodingException e) {
throw new AES256Exception(e);
}
byte[] encryptByte = encrypt(plainByte, secret, iv);
return Hex.toHexString(encryptByte);
}
/**
* 加密
*
* @param content 需要加密的内容
* @param secret 密钥
* @param iv 向量
* @return
*/
public static byte[] encrypt(byte[] content, String secret, String iv) throws AES256Exception {
try {
byte[] secretByte = secret.getBytes();
SecretKeySpec skey = new SecretKeySpec(secretByte, ALGORITHM);
IvParameterSpec ivParameterSpec = new IvParameterSpec(iv.getBytes());
Cipher cipher = Cipher.getInstance(ALGORITHM_CBC_PADDING);// &quot;算法/加密/填充&quot;
cipher.init(Cipher.ENCRYPT_MODE, skey, ivParameterSpec); //初始化加密器
return cipher.doFinal(content); // 加密
} catch (Exception e) {
throw new AES256Exception(e);
}
}
/**
* @param content 需要解密的内容
* @param secret 密钥
* @param iv 向量
* @return
*/
public static String decrypt(String content, String secret, String iv) throws AES256Exception {
byte[] encryptByte;
try {
encryptByte = Hex.decodeStrict(content);
} catch (Exception e) {
throw new AES256Exception(e);
}
byte[] plaintextByte = decrypt(encryptByte, secret, iv);
if (plaintextByte == null) {
return null;
}
try {
return new String(plaintextByte, ENCODING);
} catch (UnsupportedEncodingException e) {
log.warn(e.getMessage(), e);
return null;
}
}
/**
* @param content 需要解密的内容
* @param secret 密钥
* @param iv 向量
*/
public static byte[] decrypt(byte[] content, String secret, String iv) throws AES256Exception {
try {
SecretKeySpec skey = new SecretKeySpec(secret.getBytes(), ALGORITHM);
IvParameterSpec ivParameterSpec = new IvParameterSpec(iv.getBytes(ENCODING));
Cipher cipher = Cipher.getInstance(ALGORITHM_CBC_PADDING);// 创建密码器
cipher.init(Cipher.DECRYPT_MODE, skey, ivParameterSpec);// 初始化解密器
return cipher.doFinal(content);
} catch (Exception e) {
throw new AES256Exception(e);
}
}
public static class AES256Exception extends Exception {
public AES256Exception(String message, Throwable cause) {
super(message, cause);
}
public AES256Exception(Throwable cause) {
super(cause);
}
}
}
</code></pre>
<p>需要依赖</p>
<pre><code class="language-xml">&lt;dependency&gt;
&lt;groupId&gt;org.bouncycastle&lt;/groupId&gt;
&lt;artifactId&gt;bcprov-jdk15to18&lt;/artifactId&gt;
&lt;version&gt;1.77&lt;/version&gt;
&lt;/dependency&gt;</code></pre>