验证码接入文档
一.通讯流程
整体介绍验证码服务的客户端与服务端通讯过程:
二.客户端接入
在控制台添加验证场景后,需要在使用验证功能的Web或H5页面中,集成验证码初始化代码,实现客户端接入。
2.1 安装
- 第一步:下载BiocFacade.js,点击下载,将BiocFacade.js部署到服务器本地;
- 第二步:在Web页面引入BiocFacade.js。
<script src="BiocFacade.js"></script>
2.2 快速开始
本段落通过一些简单的示例代码,演示了如何创建和展示验证码。
这个部分的目的是尽快地了解如何使用这个 SDK,因此省略细节,只展示了最基本的使用方式(全默认调用),要深入了解所有的 API 方法和配置参数,以及如何使用更复杂的功能,请参考 2.4 JSAPI 部分。
2.2.1 全局浮窗验证码
全局浮窗验证码:验证码会在一个新的浮动窗口中显示,这个窗口会覆盖在网页的内容之上,这使得业务的前端开发不需要更改页面内容,直接调用JS就可以获取到验证码token。
使用 BiocFacade.createCaptcha 函数来创建全局浮窗验证码,使用返回对象的showPopup进行全局浮窗展示。
1const ak = 'xxx'; // 在控制台中获取
2
3// 创建验证码实例
4const captcha = window.BiocFacade.createCaptcha({ak});
5
6// 监听验证码成功回调函数
7captcha.onSuccess(tokenInfo => {
8 const stk = tokenInfo.stk;
9 // TODO: 处理token,如在接口请求中传递至后端
10});
11
12// 通过全局浮窗弹出验证码
13captcha.showPopup();
2.2.2 嵌入式验证码
嵌入式验证码:验证码内容被嵌入到网页的特定区域中,和其他的内容一起显示。
使用 BiocFacade.createCaptcha 函数来创建嵌入式验证码,使用返回对象的appendTo函数来验证码展示到指定区域。
1const ak = 'xxx'; // 在控制台中获取
2
3// 展示验证码的容器
4const container = document.getElementById('container');
5
6// 创建验证码实例
7const captcha = window.BiocFacade.createCaptcha({ak});
8
9// 监听验证码成功回调函数
10captcha.onSuccess(tokenInfo => {
11 var stk = tokenInfo.stk;
12 // TODO: 处理token,如在接口请求中传递至后端
13});
14
15// 将验证码嵌入到指定容器
16captcha.appendTo(container);
17// 或者类似document.querySelector形式
18// captcha.appendTo('#container');
2.3 验证码Token
当验证码触发onSuccess事件后,获取到验证码Token,数据格式如下:
1export type Token = {
2 pass: boolean; // 验证码通过情况
3 stk: string; // 验证码token数据
4 v: string; // 版本号
5};
对于业务来说,并不需要关心pass字段,只需要将stk传递到至服务器进行验签即可。
2.4 JSAPI
本段提供了更详细的信息,包括所有的 API 方法、配置参数和回调函数。目的是深入了解这个 SDK,因此它包含了更多的细节和更复杂的使用方式。
BiocFacade.createCaptcha
- BiocFacade.createCaptcha:创建验证码,使用返回对象的showPopup进行全局浮窗展示,使用返回对象的appendTo来嵌入到指定容器。
1// 创建验证码
2const captcha = BiocFacade.createCaptcha(options);
3// 嵌入式验证码
4captcha.appendTo(container);
1// 创建验证码
2const captcha = BiocFacade.createCaptcha(config);
3// 浮窗验证码
4captcha.showPopup();
options 配置参数说明
| 参数名 | 类型 | 是否必填 | 描述 |
|---|---|---|---|
| ak | string | 是 | 用户app key |
| timeout | number | 否 | 静态资源加载超时时间,默认8s |
| closeable | boolean | 否 | 验证码是否可关闭(嵌入式强制不可关闭),默认为true |
| showFeedback | boolean | 否 | 是否展示反馈链接按钮 |
| ctype | string | 否 | 前端控制的验证码类型,支持如下b_track_match:轨迹滑块匹配b_click:数字点选b_word_click:文字点选b_track_draw:轨迹绘制 |
| biocOrigin | string | 否 | 服务器源,主备数据源用逗号分隔,默认值如下https://sec-captcha-cloud.baidu.com,https://sec-captcha-cloud-1.baidu.com |
返回对象实例事件API
| 事件名 | 事件类型 | 描述 |
|---|---|---|
| close | () => void | 关闭验证码 |
| reset | () => void | 刷新验证码 |
| appendTo | ( selector: string | HTMLElement, callback?: () => void) => void |
| showPopup | (callback?: () => void) => void | 将验证码全局全局浮窗展示,参数说明callback:验证码展示后执行的回调 |
| onReady | () => void | 验证码初始化完成回调,等同于appendTo与showPopup的第二个参数 |
| onSuccess | (data: object) => void | 验证码验证成功回调 |
| onError | (e: Error) => void | 验证码发生错误回调(包含初始化阶段和验证阶段) |
| onFail | () => void | 验证码验证失败回调 |
| onClose | () => void | 验证码关闭时回调 |
三.服务端接入
完成客户端接入后,还需要在服务端调用verifystk接口,发起验证请求。
接口调用流程
- 前端验证通过拿到 token数据 stk;
- 前端将 stk 传给后端(可通过 query参数 、header 等方式,业务自定义);
- 后端拿到 stk,进行 aes 加密,生成加密数据 ;
- 调用验证码平台 /v1/webapi/verint/verifystk 接口 ;
- 获取到接口返回的结果,进行解密,根据解密后的 pass 字段,来判断是否校验通过。
3.1 接口地址
3.2 接口参数
- 二次校验 stk 合法性
11) stk 有效期为 1 分钟;
22) 只能验证一次, 从而防止重放攻击,超过1次认为校验不通过。
- 请求 URI
**/v1/webapi/verint/verifystk - 请求方法
* POST - 请求 query 参数
| 参数名称 | 参数类型 | 可选/必选 | 参数说明 | 示例 |
|---|---|---|---|---|
| ak | string | 必选 | 业务申请的 AK | ak 申请的公钥 |
| ts | string | 必选 | 1668065435 | 时间戳(秒,不是毫秒)有有效期,要求1min内,否则校验失败 |
| tk | string | 必选 | tk=crc32($ak$ts$sk) | 133 |
- 请求 body 参数
| 参数名称 | 参数类型 | 可选/必选 | 参数说明 |
|---|---|---|---|
| data | string | 必选 | 加密数据 参数的加解密过程使用AES算法,详细说明请参考: 3.3 加解密示例 。 加密前的数据格式1)stk 必选2)scene 可选3)data 可选 加密前后数据见示例: |
1//加密前数据:
2{
3 "stk": "1_A_O7T2uUxS45dO5_f3Eddu8WGuBTUZ2K8RaxIRcjDHuwAW9kA0xI6yRovIbPSnTGPgCLDKVekfnIY-OalpD8EWTbLUyw8aT1_7HUssr7WT3DWjENytgGC_rYcocG68_v2Mn49q35gjmpWsCtZ7-93y0LHL_p-HGBRYuIx1wJvK0Y7nohBPIJ0JfdB_330AdqsgLcbsIkpPAc-OQ8IUeQLw==", // 必选
4 "scene": "login", // 可选
5 "data": {} // 可选data是请求的补充参数
6}
7
8//加密后数据:
9"snzeVu9Zm4uodH/Z+DI9iUu8li317Eig/AdfQtEGRwv6v4Foa9lU24n1avH5qPM/Z0ZnAPgryXbldpYP1jLuAppe2YvKbH+Oz+qyJQAxv1mKQXh4HNRxUC02E79KEaytwiDB3txaHoimDogGPsk5RtdIV8VKZJqCmgo0xIahMBTFPyUmO7OS2mUwLDjNuTRAqHD4QMqB4xI/V2SmmS6P/LCQTSdKPkB9/ptr6kuFT+ke/K81UJWPD42PdHlarbytkBG1fzyWaM9r03lmsBacorIQppXzUsP5G6i/0V2BhVwc3aMympAz8VNeYe5oxdxO7QHGr93TBR5jJURrOr6JUYrlmYB6eYXe7h707FjCE26z19HjPpQjJMvSNU+ALX9w3KJKL1vxvJMUxJflO2kPq+Q=="
- 返回参数
| 参数名称 | 参数类型 | 参数说明 | 返回示例 |
|---|---|---|---|
| code | int | 错误码:0 正常,非 0 异常 | 0 |
| msg | string | 错误信息简要说明 | "" |
| data | string | 加密数据 参数的加解密过程使用AES算法,详细说明请参考: 3.3 加解密示例。 关注解密后的 pass 字段即可:1)pass 字段对应值为 true,则认为验证通过; 2)pass 字段对应值为 false, 则认为验证不通过。解密后格式如下: | "45SUFHDISFGH7832YEOFDS" |
1{
2 "pass": true, // 是否验证通过。 pass 为 true 的底层逻辑是 is_cap 为 true,并且 is_spam 为 false
3 "is_spam": false, // 是否作弊
4 "is_cap": true, // 一次验证是否成功
5}
| code 错误码 | 异常描述 |
|---|---|
| 80000 | 访问密钥(ak)不存在或不合法,请检查请求参数中的 ak 是否正确传入。 |
| 80001 | 时间戳参数 ts 格式不合法,需传入整型时间戳。 |
| 80002 | 时间戳参数 ts 已过期,请使用当前有效时间戳重新发起请求。 |
| 80003 | 参数 tk 校验失败,请参考接入文档确认生成逻辑及传参方式是否正确。 |
| 80004 | 请求体中 data 参数缺失或格式不合法,请检查请求参数结构。 |
| 80005 | 请求体中 data 参数解密失败,请确认加密方式、密钥及数据完整性是否正确。 |
| 80007 | 服务内部异常,请稍后重试;如问题持续,请联系技术支持。 |
- 请求正常示例
1//模拟请求
2curl -X POST /v1/webapi/verint/verifystk?ak=ZNyko6NVysDXy5AxERQLblqNnOcdGrSY&ts=1695730061&tk=4065780838
3-d '
4{
5 "data": "Pp6Kl5Ab8tgybbrvDO23/I/w37jBLYiPpSG9iZmKliMVWSV/kQ0FO0JO6NJHt5oXMvP10hdlsuuE/v/QT3dq6QiALzBqdg6bnhLYcaOkBlrUx7dgDNSmNMeZF/Zjp6GoIV7o0Z3HtP13Ycf+Y+TWwt1R0QLMFPqhqIIX7mYiYgfQhhNbUYlIP3i6uklG8GGkJ8jodfUOZ7QPRM9sB8k4Qs63kXlflPVg/El2ibxFu9k="
6}
7'
8//返回
9{
10 "data": "9FZsQuhB9fl+BmCWZaz3C2v+DrFlTsj0V0WWsqXGwS6lO9zwtw1grN7uKR8YhuD+",
11 "code": 0,
12 "msg":""
13}
14//data解密之后的数据:
15{
16 "pass": true, // 是否验证通过。 pass 为 true 的底层逻辑是 is_cap 为 true,并且 is_spam 为 false
17 "is_spam": false, // 是否作弊
18 "is_cap": true //一次验证是否成功
19}
20
21//非作弊
22{
23 "pass": true, // 是否验证通过。 pass 为 true 的底层逻辑是 is_cap 为 true,并且 is_spam 为 false
24 "is_spam": false, // 是否作弊
25 "is_cap": true //一次验证是否成功
26}
27
28//是作弊
29{
30 "pass": false, // 二次验证是否验证通过。 pass 为 true 的底层逻辑是 is_cap 为 true,并且 is_spam 为 false
31 "is_spam": true, // 是否作弊
32 "is_cap": true // 一次验证是否成功
33}
- 请求异常示例
1curl -X POST /v1/webapi/verint/verifystk?ak=ZNyko6NVysDXy5AxERQLblqNnOcdGrSY&ts=1695730061&tk=4065780838
2-d '
3{
4 "data": "11"
5}
6'
7异常返回
8{
9 "data": null,
10 "code": 80005,
11 "msg": "DataDecodeFalied",
12 "log_id": "91ce771bf9424a5b5104c23b5b310451"
13}
3.3 加解密示例
Go 版本
1package main
2
3import (
4 "encoding/base64"
5 "encoding/json"
6 "fmt"
7
8 "github.com/forgoer/openssl"
9)
10
11var (
12 // 分配的 ak
13 appkey = "********************************"
14 // 分配的 sk
15 secretKey = "********************************"
16)
17
18type VerifyStkDataInfo struct {
19 Stk string `json:"stk"` // stoken
20 Scene string `json:"scene"` // 场景
21 Data map[string]any `json:"data"` // 补充参数
22}
23
24func main() {
25 checkData := VerifyStkDataInfo{
26 Stk: "1_MdZSn67LRFth2xgszhZlOo48tNmALNS-dWgPXjhZsEOIsI2so0UAiPstmgncypxE9oLyVTCbjIzcXZNuYhR_Fniwm-rFhalsKuQgi3MaNhSVziFbfwYv-TRgjqeC04R-C4hVgezfRffC0_XZ4Ibj1FL5egwObRbt4ETJvDtGDynjNZJxfdYsT30Pm3k_VT9ESiV4r9B6IQSt8ZG6P7OfchFRLCrKBB1VByWsRtOHk1s=",
27 Scene: "login", // 可选
28 Data: map[string]any{"uid": 111}, // 可选
29 }
30
31 // post 接口 body 数据
32 // {
33 // "data": "nzeVu9Zm4uodH/Z+DI9iUu8li317Eig/AdfQtEGRwv6v4Foa9lU24n1avH5qPM/Z0ZnAPgryXbldpYP1jLuAppe2YvKbH+Oz+qyJQAxv1mKQXh4HNRxUC02E79KEaytwiDB3txaHoimDogGPsk5RtdIV8VKZJqCmgo0xIahMBTFPyUmO7OS2mUwLDjNuTRAqHD4QMqB4xI/V2SmmS6P/LCQTSdKPkB9/ptr6kuFT+ke/K81UJWPD42PdHlarbytkBG1fzyWaM9r03lmsBacorIQppXzUsP5G6i/0V2BhVwc3aMympAz8VNeYe5oxdxO7QHGr93TBR5jJURrOr6JUYrlmYB6eYXe7h707FjCE26z19HjPpQjJMvSNU+ALX9w3KJKL1vxvJMUxJflO2kPq+Q=="
34 // }
35 data, err := assembleData(checkData)
36 fmt.Println("加密后 Post Body 的 data:", data)
37 fmt.Println("err:", err)
38
39 /////////////////////////////////////////////
40 // 接口返回 data 解密
41 // {
42 // "data": "+avB7i2I+4LxPZHZt0qm7KhWvvwmUt+sFbcEWE4MANB3LzIdjAbhGvZngYk86aOrsBGZ10I0nfJZTJg6Wmkxbg==",
43 // "code": 0,
44 // "msg": ""
45 // }
46 decData, _ := aesDecode("+avB7i2I+4LxPZHZt0qm7KhWvvwmUt+sFbcEWE4MANB3LzIdjAbhGvZngYk86aOrsBGZ10I0nfJZTJg6Wmkxbg==", secretKey)
47 // 接口返回 data 解密 decData: {"pass":false,"is_spam":true,"is_cap":true,"detail":["b"]}
48 fmt.Println("接口返回 data 解密 decData:", decData)
49}
50
51// assembleData 组装data
52func assembleData(checkData VerifyStkDataInfo) (string, error) {
53 encodeCheckData, errEncodeCheckData := json.Marshal(checkData)
54 if errEncodeCheckData != nil {
55
56 return "", errEncodeCheckData
57 }
58 data, errAesEncode := aesEncode(encodeCheckData, secretKey)
59 if errAesEncode != nil {
60 return "", errAesEncode
61 }
62
63 return data, nil
64}
65
66// aesEncode Aes数据加密
67func aesEncode(data []byte, sk string) (string, error) {
68 encryptData, err := openssl.AesECBEncrypt(data, []byte(sk), openssl.ZEROS_PADDING)
69 if err != nil {
70 return "", err
71 }
72 return base64.StdEncoding.EncodeToString(encryptData), nil
73}
74
75// aesDecode Aes数据解密
76func aesDecode(data string, sk string) (string, error) {
77 resBase64Decode, err := base64.StdEncoding.DecodeString(data)
78 if err != nil {
79 return "", err
80 }
81 key := []byte(sk)
82 res, err := openssl.AesECBDecrypt(resBase64Decode, key, openssl.ZEROS_PADDING)
83 if err != nil {
84 return "", err
85 }
86
87 return string(res), nil
88}
Java版本
- pom 文件
1<?xml version="1.0" encoding="UTF-8"?>
2<project xmlns="http://maven.apache.org/POM/4.0.0"
3 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
5 <modelVersion>4.0.0</modelVersion>
6
7 <groupId>com.baidu.seccaptcha</groupId>
8 <artifactId>demo</artifactId>
9 <version>1.0-SNAPSHOT</version>
10
11 <properties>
12 <maven.compiler.source>17</maven.compiler.source>
13 <maven.compiler.target>17</maven.compiler.target>
14 </properties>
15 <dependencies>
16 <dependency>
17 <groupId>commons-codec</groupId>
18 <artifactId>commons-codec</artifactId>
19 <version>1.15</version>
20 </dependency>
21 <dependency>
22 <groupId>com.alibaba</groupId>
23 <artifactId>fastjson</artifactId>
24 <version>1.2.83</version>
25 </dependency>
26 </dependencies>
27</project>
- java 加密、解密示例
1package com.baidu.seccaptcha;
2
3import com.alibaba.fastjson.JSONObject;
4import java.util.Base64;
5import java.util.HashMap;
6import java.util.Map;
7import javax.crypto.Cipher;
8import javax.crypto.spec.SecretKeySpec;
9
10public class AESUtils {
11 public static void main(String[] args) {
12 // 分配的 sk
13 String sk = "****************";
14
15 JSONObject jsonObject = new JSONObject();
16 jsonObject.put("stk",
17 "1_quExwipacpBC4PukBSLtv5jUUi5CiKB3ZawBBie0ARfnwO9joJEUyG_GF6fJWxfd7tbM_z18crl8_k0Jq5WWLLEZ9f1tvACGv-rGSn-9MOftEf2VkOGDnGtptUjqMvqRL2YYgGTMdTNyMdjKD8461XMxaW7_EsSsP0yJEuZIKSGyrAZ_zKA2ZiY4ChktjobN");
18 jsonObject.put("scene", "login");
19 Map<String, String> data = new HashMap<>();
20 data.put("uid", "12345");
21 jsonObject.put("data", data);
22
23 String jsonString = jsonObject.toString();
24 System.out.println("jsonString: " + jsonString);
25
26 // 生成 接口 post body 加密数据
27 // {
28 // "data":"AfcZ8tUOXFcYzHF/jqSfyr47KjcZ1UWCp6nE0cOPAkMppbb+TYbPliKgBwII8UAd2P9mHgAyLRb59szFLl4BoocpwBiQJ6X4uVZWIhJuUDaFb0VT4Cey7DxnIS0CEWPddtGm6WKZMJQb1tYAHVeqIelFKOaWHkF0RNJG0CDhfKi2CQ+4F5Kf/IZF1Wv42BGjyFCtyVBaHRMPpYPXmQcvsNPRhaCTgkSwMoK2cc8REw01ahNO0iO1tqR2BYrdJHd6g8r/G8ioK4i2UbRCqAs+yskN9N/VDVH8XvGWwRkmF34S0PruH8DoDM3FNkAv9xmBy5F3J4SgYLVcdou1oOk+6w=="
29 // }
30 String encryptedData = AESUtils.AESEncode(jsonString, sk);
31 System.out.println("encryptedData: " + encryptedData);
32
33 // 接口 response 解密 示例
34 // {
35 // "data":
36 // "fzWbphJiHJIWLTihYn2wGZA5Er9/HSktAf4ycc0DRYNSW+9gN7vRlEzG+8DUjezdyF0RyMktwwURInZsDRwaUg==",
37 // "code": 0,
38 // "msg": ""
39 // }
40 String decryptedData = AESUtils.AESDecode(
41 "fzWbphJiHJIWLTihYn2wGZA5Er9/HSktAf4ycc0DRYNSW+9gN7vRlEzG+8DUjezdyF0RyMktwwURInZsDRwaUg==", sk);
42 System.out.println("decryptedData: " + decryptedData);
43
44 }
45
46
47 private static String AESEncode(String str, String key) {
48 String ret = "";
49 if (str != null && str.length() > 0) {
50 try {
51 Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding");
52 SecretKeySpec keyspec = new SecretKeySpec(key.getBytes("UTF-8"), "AES");
53 cipher.init(Cipher.ENCRYPT_MODE, keyspec);
54 //待加密内容的长度必须是16的倍数,如果不是16的倍数,就抛出IllegalBlockSizeException,补全加密内容。
55 //因此解密的时候,明文最后有可能出现一段特殊字符,这就是aes用于补全用的字符。用json解析使用明文前,
56 //trim掉它。这个特殊字符是杠铃:\0,这个就是返回数据时,使用的aes加密时,使用的补全字符
57 byte[] dataBytes = str.getBytes("UTF-8");
58 int dataLength = dataBytes.length;
59 if (dataLength % 16 != 0) {
60 dataLength = dataLength + (16 - (dataLength % 16));
61 }
62 byte[] paddingBytes = new byte[dataLength];
63 System.arraycopy(dataBytes, 0, paddingBytes, 0, dataBytes.length);
64
65 byte[] encrypted = cipher.doFinal(paddingBytes);
66 ret = Base64.getEncoder().encodeToString(encrypted);
67 return ret;
68 } catch (Exception e) {
69 // ...
70 }
71 }
72 return ret;
73 }
74
75 private static String AESDecode(String str, String key) {
76 String ret = "";
77 if (str != null && str.length() > 0) {
78 try {
79 byte[] dataBytes = Base64.getDecoder().decode(str);
80 Cipher cipher = Cipher.getInstance("AES/ECB/NoPadding");
81 SecretKeySpec keyspec = new SecretKeySpec(key.getBytes("UTF-8"), "AES");
82 cipher.init(Cipher.DECRYPT_MODE, keyspec);
83 byte[] retBytes = cipher.doFinal(dataBytes);
84 ret = new String(retBytes, "UTF-8");
85 return ret;
86 } catch (Exception e) {
87 //e.printStackTrace();
88 }
89
90 }
91 return ret;
92 }
93}
PHP 版本
1class Encrypt
2{
3 /**
4 * 用配置中的密钥,aes解密
5 * @param $str 加密前
6 * @param $key 密钥
7 * @return string 加密后
8 */
9 public static function aes_encode($str,$key)
10 {
11
12 $paddedstr = $str;
13 $method = 'AES-256-ECB';
14 //aes block size is fixed 16.
15 if (strlen($str) % 16 != 0) {
16 $paddedstr = str_pad($str, strlen($str) + 16 - strlen($str) % 16, "\0");
17 }
18 $ciphertext = openssl_encrypt($paddedstr, $method, $key, $options=OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING);
19 return base64_encode($ciphertext);
20 }
21 /**
22 * 用配置中的密钥,aes解密
23 * @param $str 反解前
24 * @param $key 密钥
25 * @return string 反解后
26 */
27 public static function aes_decode($str,$key)
28 {
29 $method = "AES-256-ECB";
30 $cipherText = base64_decode($str);
31 $ret = openssl_decrypt($cipherText, $method, $key, $options=OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING);
32 return rtrim(rtrim($ret, chr(0)), chr(7));
33 }
34}
Python 版本
1 # -*- coding: utf-8 -*-
2from Crypto.Cipher import AES
3from base64 import b64encode, b64decode
4from urllib import quote_plus
5import hashlib
6import time
7import requests
8
9class MyCrypt():
10 def __init__(self, key):
11 self.key = key
12 self.mode = AES.MODE_ECB
13 self.padding = '\0'
14
15 def encrypt(self, text):
16 """
17 aes加密
18 """
19 cryptor = AES.new(self.key, self.mode)
20 length = 16
21 count = text.count('')
22 if count < length:
23 add = (length - count) + 1
24 text += (self.padding * add)
25 elif count > length:
26 add = (length - (count % length)) + 1
27 text += (self.padding * add)
28 self.ciphertext = cryptor.encrypt(text)
29 return self.ciphertext
30
31 def decrypt(self, text):
32 """
33 aes解密
34 """
35 cryptor = AES.new(self.key, self.mode)
36 plain_text = cryptor.decrypt(text)
37 return plain_text.rstrip("\0")
38
39def md5(src):
40 """
41 md5加密
42 """
43 m = hashlib.md5()
44 m.update(src)
45 return m.hexdigest()
46
47if __name__ == '__main__':
48 # 用户申请公钥
49 appid = '*************'
50 # 分给用户的私钥
51 appsecret = '*************'
52 # 用户传递的数据
53 uid = '1957883890'
54 data = '{"cmd":"get","uid":"' + uid + '","scence":"post","data":"{\\"ip\\":\\"127.0.0.1\\",\\"client_type\\":\\"pc\\"}"}'
55 ec = MyCrypt(appsecret)
56 encrpt_data = ec.encrypt(data)
57 decrpt_data = ec.decrypt(encrpt_data)
58 # print encrpt_data, decrpt_data, decrpt_data == data
59 # print b64encode(encrpt_data)
60 # 合成url
61 appkey_url = quote_plus(appid)
62 data_url = quote_plus(b64encode(encrpt_data))
63 timestamp_url = quote_plus(str(int(time.time())))
64 data_str = "appkey=" + appkey_url + "&data=" + data_url + "×tamp=" + timestamp_url
65 signature = md5(data_str + appsecret)
66 param_str = data_str + '&signature=' + signature
67 url="http://group.passanti.passport.all.serv:8898/spam/account?" + param_str;
68 r = requests.post(url)
69 print ec.decrypt(b64decode(r.text))
70
71 # 调试成功后将会输出:{"code":0,"ret":{"is_spam":true,"detail":["a","c"],"update":20150907}}
Node.js 版本
1const axios = require('axios');
2const crypto = require('crypto');
3const assert = require('assert');
4
5// 通信双方约定的算法, aes-256-ecb + base64
6// AES加密处理
7const encrypt = (plain, key) => {
8 // aes加密 ecb 模式不需要初始化向量,这里留空
9 const iv = '';
10 const cipher = crypto.createCipheriv('aes-256-ecb', key, iv);
11 // 关闭自动补齐
12 cipher.setAutoPadding(false);
13 return cipher.update(plain.toString(), 'utf8', 'base64') + cipher.final('base64');
14}
15
16// AES解密处理
17const decrypt = (encrypted, key) => {
18 // aes加密 ecb 模式不需要初始化向量,这里留空
19 const iv = '';
20 const decipher = crypto.createDecipheriv('aes-256-ecb', key, iv);
21 // 关闭自动补齐
22 decipher.setAutoPadding(false);
23 return decipher.update(encrypted, 'base64', 'utf8') + decipher.final('utf8');
24}
25
26// 按约定规则,AES组块要补零处理,注意标准是有很多种的,必须交互双方一致
27const zeroPad = (input) => {
28 let inputData = Buffer.from(input, 'utf8');
29 let bufResult = inputData;
30 // 计算缺失需要填补的字节数量,必须使用buffer填充\0,而不是字符串拼接'0',在js中无效
31 const blockSize = 16;
32 const bitLength = inputData.length;
33 if (bitLength < blockSize) {
34 // 填补到分块的整数
35 const padLen = blockSize - bitLength;
36 const bufPad = Buffer.alloc(padLen, '\0');
37 // concat
38 bufResult = Buffer.concat([inputData, bufPad], blockSize);
39 } else if (bitLength > blockSize) {
40 // 计算要补足的字节数量
41 const remain = bitLength % blockSize;
42 // 不能整除
43 if (remain !== 0) {
44 const padLen = blockSize - bitLength % blockSize;
45 const bufPad = Buffer.alloc(padLen, '\0');
46 // concat
47 bufResult = Buffer.concat([inputData, bufPad], bitLength + padLen);
48 }
49 }
50
51 return bufResult;
52}
53
54// 剔除后面的补0
55const removePad = (input) => {
56 const i = input.indexOf('\0');
57 if (i !== -1) {
58 return input.substr(0, i);
59 }
60 return input;
61}
62
63// 加解密测试
64const runEncryptTest = (key) => {
65 const randomNum = (minNum, maxNum) => {
66 return parseInt(Math.random() * (maxNum - minNum + 1) + minNum, 10);
67 }
68
69 for (let i = 0; i < 100000; i++) {
70 let plain = `${randomNum(1, 99)}`;
71 // random string process
72 const padNum = randomNum(1, 10000);
73 plain = plain.repeat(padNum);
74 const plainBuff = zeroPad(plain);
75
76 const encrypted = encrypt(plainBuff, appsecret);
77 // console.log(`Encrypted: ${encrypted}`);
78
79 const decrypted = decrypt(encrypted, appsecret);
80 const cmpPlain = removePad(decrypted);
81 // console.log(`Decrypted: ${cmpPlain}`);
82
83 if (plain !== cmpPlain) {
84 console.log(`data comparison error. test count: ${i}, test string: ${plain}`);
85 assert(false);
86 break;
87 }
88 }
89}
90
91// ak/sk
92const appid = 'd1fa6b3c2********90074660c';
93const appsecret='ca6e45f76*********397b4fb8b';
94
95// runEncryptTest(appsecret);
96
97// test data
98const stk = '1_quExwipacpBC4PukBSLtv5jUUi5CiKB3ZawBBie0ARfnwO';
99
100/*
101// 禁止使用这种模式生成字符串,与PHP不兼容
102const data = {
103 "stk": uid,
104 "scence": "post",
105 "other" : {
106 "ip": "127.0.0.1",
107 "client_type": "pc"
108 }
109};
110const dataStr = JSON.stringify(data);
111*/
112// 最简单的测试模式
113const dataStr = '{"stk:"' + stk + '","scence":"post","data":"{\\"ip\\":\\"127.0.0.1\\",\\"client_type\\":\\"pc\\"}"}';
114//const dataStr = '{"cmd":"get","uid":"' + uid + '","scence":"post","token":"aFwIZxjtECrMj6al3bEe4aDLYI6TFhlIiY","ds":"owVehTPfB/rdv+4PQv0gn2O0eazxjCdQKvyFJjtAKbsTSKo/...","other":"{\\"ip\\":\\"127.0.0.1\\",\\"client_type\\":\\"pc\\", \\"ua\\":\\"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.121 Safari/537.36\\"}"}';
115const dataPadding = zeroPad(dataStr);
116const encryptedData = encrypt(dataPadding, appsecret);
117const decryptedData = decrypt(encryptedData, appsecret);
118
119console.log(`encrypt data: ${encodeURIComponent(encryptedData)}`);
120
121const result = removePad(decryptedData);
122if (dataStr !== result) {
123 assert(false);
124}
125
126// use fixed value for test
127// const timestamp = 1630308003
128const timestamp = Math.floor(Date.now()/1000);
129
130// 进行数据组装和签名
131const postData = encodeURIComponent(encryptedData);
132let paramStr = `ak=${appid}&ts=${timestamp}`;
133const md5Signature = crypto.createHash('md5').update(paramStr+appsecret).digest('hex');
134
135paramStr += `&tk=${md5Signature}`;
136
137console.log(`param string: ${paramStr}`);
138
139// 服务地址
140//const testUrl = "seccaptcha.baidu.com/v1/webapi/verint/verifystk?" + paramStr;
141
142console.log(`testUrl: ${testUrl}`);
143
144// test request
145axios(testUrl).then(function (resp) {
146 const result = resp.data;
147 console.log(`server response raw data: ${result}`);
148 // 解密
149 try {
150 const data = decrypt(result, appsecret);
151 const cleanData = removePad(data);
152 console.log(`decrypt data: ${cleanData}`);
153 const parseData = JSON.parse(cleanData);
154 // 获取内部数据
155 const {code} = parseData;
156 if (code === 0) {
157 // 检测成功,查看是否作弊
158 const {ret: { is_spam }} = parseData;
159 console.log(`is_spam result: ${is_spam}`);
160
161 } else if (code === 1) {
162 // 出现调用的错误
163 const {msg} = parseData;
164 console.log(`response error msg: ${msg}`);
165 }
166
167 } catch (err) {
168 console.log(`error : ${err.message}`);
169 }
170}).catch(function (error) {
171 console.log(error.message);
172});
四. 关于容灾处理
4.1 客户端容灾
安全验证码自带了资源&请求的主备切换、自动重试机制,提升整体服务的可用性,同时BiocFacade内置了一个脱离服务器的本地验证码token生成机制,这代表着当安全验证码在服务器出现宕机时,客户端会自动切换到离线验证的机制,保证业务方页面可以正常运行不受影响。
当安全验证码的容灾机制被触发时,弹窗的验证码将变成容灾验证码,展示如下:

用户点击区域即可完成验证生成验证码token,业务前端直接获取到。
4.2 服务端容灾
服务端容灾部署需要客户在服务端集成时二次校验流程时,注意对二次校验的接口进行异常处理。 当请求极验二次验证接口异常或响应状态非200时做出相应异常处理,保证不会因为接口请求超时或服务未响应而阻碍业务流程。
