极验(Geetest)验证码功能集成
h5端演示


app端演示
app端极验验证码
极验验证码前后端交互详细分析
一、整体架构概览
极验服务器
后端 (Flask)
前端 (uni-app)
H5环境
App环境
GET
返回captchaId+challenge
验证成功
携带验证参数
调用
POST
验证结果
通过/拒绝
登录页面 login.vue
极验组件 GeetestCaptcha.vue
H5环境: 内嵌渲染
App环境: WebView弹窗
geetest.html
初始化接口 /geetest/init
验证接口 verify_geetest
业务接口 登录/注册/发短信
SDK加载 gt4.js
二次验证 /validate
二、完整交互流程(时序图)
E 极验服务器 后端服务器 极验SDK 极验组件 登录页面 用户 E 极验服务器 后端服务器 极验SDK 极验组件 登录页面 用户 阶段1: 初始化 alt [H5环境] [App环境] 阶段2: 用户验证 阶段3: 业务提交 打开登录页面 挂载组件 GET /geetest/init {captchaId, challenge} 加载gt4.js SDK SDK加载完成 initGeetest4(captchaId, challenge) 渲染验证按钮 创建WebView弹窗 加载geetest.html?captcha_id=&challenge= 加载gt4.js SDK SDK加载完成 initGeetest4(captchaId, challenge) 渲染验证按钮 点击验证按钮 显示滑块/拼图 完成滑动操作 本地行为分析 onSuccess({lot_number, captcha_output, pass_token, gen_time}) 点击登录按钮 POST /login {username, password, lot_number, captcha_output, pass_token, gen_time} verify_geetest() POST /validate {lot_number, captcha_output, pass_token, gen_time, sign_token} {result: "success", reason: ""} 验证通过 {access_token, user_id, username} 登录成功,跳转首页
三、H5与App环境差异对比

四、关键数据流详解
4.1 初始化阶段
PlainText
前端请求: GET /geetest/init
↓
后端响应: {
"captchaId": "你的极验ID",
"challenge": "随机UUID"
}
↓
前端使用: captchaId + challenge 初始化极验SDK
python
import hmac
import hashlib
import requests
import json
class GeetestV4:
"""极验行为验证第四代 SDK"""
# 极验4 二次校验接口地址(注意:与极验3的地址不同)
VALIDATE_URL = "https://gcaptcha4.geetest.com/validate"
def __init__(self, captcha_id, captcha_key):
"""
初始化极验4 SDK
:param captcha_id: 验证 ID(公钥)
:param captcha_key: 验证密钥(私钥)
"""
self.captcha_id = captcha_id
self.captcha_key = captcha_key
def generate_sign_token(self, lot_number):
"""
生成签名 token
极验4 要求使用标准 HMAC-SHA256 算法生成签名:
- 原文(message):lot_number
- 密钥(key):captcha_key
- 哈希算法:SHA256
- 输出格式:hex digest(十六进制小写字符串)
不兼容旧极验3生成MD5签名的方法,不要复用旧方法。
:param lot_number: 验证流水号,由前端验证成功后返回
:return: sign_token 签名字符串
"""
lot_number_bytes = lot_number.encode()
prikey_bytes = self.captcha_key.encode()
sign_token = hmac.new(
prikey_bytes, lot_number_bytes, digestmod=hashlib.sha256
).hexdigest()
return sign_token
def validate(self, lot_number, captcha_output, pass_token, gen_time):
"""
二次校验:向极验服务器确认用户验证结果是否有效
:param lot_number: 验证流水号
:param captcha_output: 验证输出信息
:param pass_token: 验证通过令牌
:param gen_time: 验证时间戳
:return: dict 包含 result(success/fail)、reason 等字段
"""
# 1. 生成签名
sign_token = self.generate_sign_token(lot_number)
# 2. 组装请求参数
params = {
"lot_number": lot_number,
"captcha_output": captcha_output,
"pass_token": pass_token,
"gen_time": gen_time,
"sign_token": sign_token,
}
# 3. 发送二次校验请求
# captcha_id 放在 URL 参数中,便于日志排查
url = f"{self.VALIDATE_URL}?captcha_id={self.captcha_id}"
try:
# 必须使用 application/x-www-form-urlencoded 格式
resp = requests.post(url, data=params, timeout=5)
assert resp.status_code == 200
result = resp.json()
return result
except Exception as e:
print(f"极验4 二次校验请求异常: {e}")
# 宕机模式:请求异常时放行,保证业务可用
return {"result": "success", "reason": "request geetest api fail"}
python
@auth_bp.route('/geetest/init', methods=['GET'])
def geetest_init():
"""初始化极验验证"""
config = get_geetest_config()
captcha_id = config['geetest_id']
current_app.logger.info(f"极验初始化 - captcha_id={captcha_id}")
# 返回captchaId和随机challenge
return jsonify({
'captchaId': captcha_id,
'challenge': str(uuid.uuid4())
})
4.2 验证成功回调参数
用户完成验证后,极验SDK返回4个关键参数:

4.3 二次验证流程(后端)
python
# 1. 前端提交登录请求,携带验证参数
POST /login {
"username": "xxx",
"password": "xxx",
"lot_number": "xxx",
"captcha_output": "xxx",
"pass_token": "xxx",
"gen_time": "xxx"
}
# 2. 后端调用verify_geetest()
# 位置: auth.py:L73-100
# 3. 生成sign_token签名
sign_token = HMAC-SHA256(
key=geetest_key,
message=lot_number
)
# 4. 向极验服务器发起二次验证
POST https://gcaptcha4.geetest.com/validate?captcha_id=xxx
Content-Type: application/x-www-form-urlencoded
lot_number=xxx&captcha_output=xxx&pass_token=xxx&gen_time=xxx&sign_token=xxx
# 5. 极验服务器返回验证结果
{
"result": "success", # 或 "fail"
"reason": ""
}
核心代码:
- SDK实现: geetest_v4.py
- 验证调用: auth.py:L73-100
python
def verify_geetest(lot_number, captcha_output, pass_token, gen_time):
"""
验证极验结果 - 使用官方二次验证流程
参数说明:
- lot_number: 验证流水号
- captcha_output: 验证输出信息
- pass_token: 验证通过标识
- gen_time: 验证通过时间戳
"""
config = get_geetest_config()
captcha_id = config['geetest_id']
captcha_key = config['geetest_key']
current_app.logger.info(f"极验验证 - 开始验证, lot_number={lot_number[:10]}...")
# 如果没有配置极验ID和KEY,则跳过验证(用于开发测试)
if not captcha_id or captcha_id == 'your_geetest_id':
current_app.logger.info("极验验证 - 未配置有效ID,跳过验证")
return True
try:
# 使用封装的GeetestV4 SDK
geetest = GeetestV4(captcha_id, captcha_key)
result = geetest.validate(lot_number, captcha_output, pass_token, gen_time)
if result.get('result') == 'success':
return True
else:
current_app.logger.warning(f"极验验证失败: {result.get('reason', '未知原因')}")
return False
except Exception as e:
# 任何异常都允许通过,避免阻塞业务流程
current_app.logger.error(f"极验验证请求异常: {str(e)}")
return True
五、App端WebView通信机制
创建WebView
用户验证成功
window.location.href
overrideUrlLoading拦截
handleCaptchaSuccess
关闭WebView
GeetestCaptcha.vue
geetest.html
触发onSuccess回调
geetest-callback://success?lot_number=xxx&...
解析参数
通知登录页面
关键实现:
- WebView创建: GeetestCaptcha.vue:L211-273
- URL拦截:使用 overrideUrlLoading({mode: 'reject'}) 拦截自定义协议
- 回调解析: GeetestCaptcha.vue:L285-299
/hybrid/html
html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>安全验证</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body {
width: 100%;
height: 100%;
overflow: hidden;
}
body {
background: transparent;
display: flex;
align-items: center;
justify-content: center;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
}
#captcha-wrapper {
width: 100%;
display: flex;
justify-content: center;
}
.error-text {
text-align: center;
color: #f56c6c;
font-size: 14px;
padding: 20px 0;
}
.retry-btn {
display: block;
margin: 16px auto 0;
padding: 8px 24px;
background: #5b6bf8;
color: #fff;
border: none;
border-radius: 20px;
font-size: 14px;
cursor: pointer;
}
</style>
</head>
<body>
<div id="captcha-wrapper"></div>
<script src="https://static.geetest.com/v4/gt4.js"></script>
<script>
console.log('极验页面 - 开始加载')
console.log('极验页面 - 当前URL:', window.location.href)
let captchaObj = null
function getQueryParams() {
const fullUrl = window.location.href
const questionIndex = fullUrl.indexOf('?')
const queryString = questionIndex !== -1 ? fullUrl.substring(questionIndex + 1) : ''
console.log('极验页面 - QueryString:', queryString)
const params = {
captchaId: '',
challenge: ''
}
if (queryString) {
const pairs = queryString.split('&')
for (let i = 0; i < pairs.length; i++) {
const pair = pairs[i].split('=')
const key = decodeURIComponent(pair[0])
const value = pair.length > 1 ? decodeURIComponent(pair[1] || '') : ''
if (key === 'captcha_id') {
params.captchaId = value
} else if (key === 'challenge') {
params.challenge = value
}
}
}
console.log('极验页面 - 解析结果:', params)
return params
}
const queryParams = getQueryParams()
const captchaId = queryParams.captchaId
const challenge = queryParams.challenge
function showError(text, showRetry) {
const box = document.getElementById('captcha-wrapper')
let html = `<div class="error-text">${text}</div>`
if (showRetry) {
html += `<button class="retry-btn" onclick="initCaptcha()">重试</button>`
}
box.innerHTML = html
}
function initCaptcha() {
console.log('极验页面 - 开始初始化验证码')
console.log('极验页面 - captchaId:', captchaId)
if (!captchaId) {
const errorMsg = '缺少 captcha_id 参数'
console.error('极验页面 - ' + errorMsg)
showError(errorMsg, false)
setTimeout(() => {
window.location.href = 'geetest-callback://error?msg=' + encodeURIComponent(errorMsg)
}, 1500)
return
}
if (typeof window.initGeetest4 !== 'function') {
const errorMsg = '极验 SDK 未加载'
console.error('极验页面 - ' + errorMsg)
showError(errorMsg, true)
return
}
setTimeout(() => {
initGeetest4({
captchaId: captchaId,
challenge: challenge,
language: 'zh-cn',
protocol: 'https:',
timeout: 10000,
retry: 2,
product: 'bind'
}, function (captcha) {
console.log('极验页面 - 验证码实例创建成功')
captchaObj = captcha
// 清空容器
const wrapper = document.getElementById('captcha-wrapper')
wrapper.innerHTML = ''
captcha.appendTo('#captcha-wrapper')
captcha.onReady(function () {
console.log('极验页面 - 验证码就绪,直接显示滑块')
setTimeout(() => {
captcha.showCaptcha()
}, 200)
})
captcha.onSuccess(function () {
console.log('极验页面 - 验证成功')
const validate = captcha.getValidate()
console.log('极验页面 - 验证结果:', validate)
const callbackUrl = 'geetest-callback://success?' +
'lot_number=' + encodeURIComponent(validate.lot_number || '') +
'&captcha_output=' + encodeURIComponent(validate.captcha_output || '') +
'&pass_token=' + encodeURIComponent(validate.pass_token || '') + '&gen_time=' + encodeURIComponent(validate.gen_time || '')
console.log('极验页面 - 回调 URL:', callbackUrl)
window.location.href = callbackUrl
})
captcha.onError(function (error) {
console.error('极验页面 - 验证错误:', error)
const errorMsg = error.msg || '验证失败'
showError(errorMsg, true)
})
captcha.onClose(function () {
console.log('极验页面 - 验证已关闭')
window.location.href = 'geetest-callback://error?msg=用户取消验证'
})
})
}, 300)
}
document.addEventListener('DOMContentLoaded', function () {
console.log('极验页面 - DOM 加载完成')
initCaptcha()
})
</script>
</body>
</html>
/components/GeetestCaptcha.vue
vue
<template>
<view class="geetest-container">
<!-- H5端:直接渲染极验组件 -->
<view v-if="isH5" class="geetest-h5-wrapper">
<!-- 加载中 -->
<view v-if="isLoading" class="geetest-loading">
<view class="loading-spinner"></view>
<text class="loading-text">加载中...</text>
</view>
<!-- 验证成功 -->
<view v-else-if="captchaData.lot_number" class="geetest-success">
<text class="captcha-success-icon fa-solid fa-check-circle"></text>
<text class="captcha-success-text">验证成功</text>
<text class="captcha-retry" @click="resetCaptcha">重新验证</text>
</view>
<!-- 极验容器 -->
<view v-else id="geetest-h5-box"></view>
</view>
<!-- App端:点击触发,显示webview -->
<view v-else class="geetest-app-container">
<!-- 加载中 -->
<view v-if="isLoading" class="geetest-loading">
<view class="loading-spinner"></view>
<text class="loading-text">加载中...</text>
</view>
<!-- 验证成功 -->
<view v-else-if="captchaData.lot_number" class="geetest-success">
<text class="captcha-success-icon fa-solid fa-check-circle"></text>
<text class="captcha-success-text">验证成功</text>
<text class="captcha-retry" @click="resetCaptcha">重新验证</text>
</view>
<!-- 触发按钮 -->
<view v-else class="geetest-trigger" @click="showCaptcha">
<text class="captcha-icon fa-solid fa-shield-halved"></text>
<text class="captcha-text">点击进行安全验证</text>
<text class="captcha-arrow fa-solid fa-chevron-right"></text>
</view>
</view>
</view>
</template>
<script setup>
import { ref, onMounted, onUnmounted, nextTick } from 'vue';
import { userApi } from '@/apis/index';
import { onUnload } from '@dcloudio/uni-app';
const emit = defineEmits(['success', 'error']);
const isH5 = ref(false);
const isLoading = ref(false);
const captchaData = ref({
lot_number: '',
captcha_output: '',
pass_token: '',
gen_time: ''
});
let captchaInstance = null;
let captchaScript = null;
let captchaWebview = null;
// 检查当前环境
const checkEnvironment = () => {
// #ifdef H5
isH5.value = true;
console.log('极验组件 - H5环境');
// #endif
// #ifndef H5
isH5.value = false;
console.log('极验组件 - App环境');
// #endif
};
// 初始化验证码
const initCaptcha = async () => {
console.log('极验组件 - 开始初始化');
isLoading.value = true;
try {
console.log('极验组件 - 调用初始化接口');
const res = await userApi.geetestInit();
console.log('极验组件 - 接口返回:', res);
if (!res || !res.captchaId) {
throw new Error('获取验证码参数失败');
}
const challenge = res.challenge || '';
console.log('极验组件 - challenge:', challenge);
if (isH5.value) {
await loadGeetestSDK();
await initH5Captcha(res.captchaId, challenge);
} else {
const url = `/hybrid/html/geetest.html?captcha_id=${res.captchaId}&challenge=${encodeURIComponent(challenge)}`;
createCaptchaWebview(url);
}
isLoading.value = false;
} catch (error) {
console.error('极验组件 - 初始化失败:', error);
isLoading.value = false;
emit('error', error);
}
};
// 加载极验SDK
const loadGeetestSDK = () => {
return new Promise((resolve, reject) => {
console.log('极验组件 - 开始加载SDK');
if (typeof window.initGeetest4 === 'function') {
console.log('极验组件 - SDK已加载');
resolve();
return;
}
if (window.geetestLoading) {
const checkInterval = setInterval(() => {
if (typeof window.initGeetest4 === 'function') {
clearInterval(checkInterval);
resolve();
}
}, 100);
setTimeout(() => {
clearInterval(checkInterval);
reject(new Error('SDK加载超时'));
}, 10000);
return;
}
window.geetestLoading = true;
captchaScript = document.createElement('script');
captchaScript.src = 'https://static.geetest.com/v4/gt4.js';
captchaScript.async = true;
captchaScript.onload = function () {
window.geetestLoading = false;
resolve();
};
captchaScript.onerror = function () {
window.geetestLoading = false;
reject(new Error('SDK加载失败'));
};
document.head.appendChild(captchaScript);
});
};
// 初始化H5验证码
const initH5Captcha = async (captchaId, challenge) => {
console.log('极验组件 - 初始化H5验证码,ID:', captchaId, 'challenge:', challenge);
await nextTick();
if (typeof window.initGeetest4 === 'function') {
window.initGeetest4(
{
captchaId: captchaId,
challenge: challenge,
language: 'zh-cn',
protocol: 'https:',
timeout: 10000,
retry: 2,
product: 'float',
width: '100%'
},
function (captcha) {
console.log('极验组件 - 验证码实例创建成功');
captchaInstance = captcha;
captcha.appendTo('#geetest-h5-box');
captcha.onSuccess(function () {
const validate = captcha.getValidate();
if (validate) {
handleCaptchaSuccess(validate);
}
});
captcha.onError(function (error) {
console.error('极验组件 - 验证错误:', error);
emit('error', error);
});
captcha.onClose(function () {
console.log('极验组件 - 验证已关闭');
});
}
);
} else {
throw new Error('验证码SDK未加载');
}
};
// 处理验证成功
const handleCaptchaSuccess = (validate) => {
console.log('极验组件 - 处理验证成功:', validate);
captchaData.value = {
lot_number: validate.lot_number,
captcha_output: validate.captcha_output,
pass_token: validate.pass_token,
gen_time: validate.gen_time
};
emit('success', captchaData.value);
};
// 创建App端Webview
const createCaptchaWebview = (url) => {
console.log('极验组件 - 创建WebView,URL:', url);
// 先关闭旧的WebView
if (captchaWebview) {
try {
captchaWebview.close();
} catch (e) {
console.error('极验组件 - 关闭旧WebView异常:', e);
}
captchaWebview = null;
}
const [urlPath, urlQuery] = url.split('?');
let absoluteUrl = '';
try {
const localFilePath = plus.io.convertLocalFileSystemURL('_www/' + urlPath.replace(/^\//, ''));
absoluteUrl = `file://${localFilePath}${urlQuery ? '?' + urlQuery : ''}`;
console.log('极验组件 - 绝对URL:', absoluteUrl);
} catch (err) {
console.error('极验组件 - 转换路径失败:', err);
return;
}
try {
const webviewId = 'geetest-captcha-' + Date.now();
const wv = plus.webview.open(absoluteUrl, webviewId, {
'uni-app': 'none',
background: 'transparent',
webviewBGTransparent: true,
top: '0px',
left: '0px',
height: '100%',
width: '100%',
scrollIndicator: 'none',
popGesture: 'none',
zindex: 99999
});
captchaWebview = wv;
wv.overrideUrlLoading({ mode: 'reject' }, (e) => {
const urlStr = e.url;
if (urlStr.indexOf('geetest-callback://success') === 0) {
const paramsResult = parseUrlParams(urlStr);
handleCaptchaSuccess({
lot_number: paramsResult.lot_number || '',
captcha_output: paramsResult.captcha_output || '',
pass_token: paramsResult.pass_token || '',
gen_time: paramsResult.gen_time || ''
});
closeCaptchaWebview();
} else if (urlStr.indexOf('geetest-callback://error') === 0) {
const paramsResult = parseUrlParams(urlStr);
const errorMsg = paramsResult.msg || '未知错误';
console.error('极验组件 - 验证失败:', errorMsg);
closeCaptchaWebview();
}
});
wv.addEventListener('loaded', () => {
console.log('极验组件 - WebView加载完成');
});
wv.addEventListener('error', (e) => {
console.error('极验组件 - WebView加载错误:', e);
closeCaptchaWebview();
});
} catch (error) {
console.error('极验组件 - 创建WebView失败:', error);
captchaWebview = null;
}
};
// 关闭Webview
const closeCaptchaWebview = () => {
if (captchaWebview) {
const wv = captchaWebview;
captchaWebview = null;
setTimeout(() => {
try {
if (wv) {
wv.close();
}
} catch (e) {
console.error('极验组件 - 关闭WebView异常:', e);
}
}, 400);
}
};
// 显示验证码(App端)
const showCaptcha = () => {
initCaptcha();
};
// 解析URL参数
const parseUrlParams = (url) => {
const queryString = url.split('?')[1];
if (!queryString) return {};
const params = {};
const pairs = queryString.split('&');
pairs.forEach((pair) => {
const [key, value] = pair.split('=');
params[decodeURIComponent(key)] = decodeURIComponent(value || '');
});
return params;
};
// 重置验证码
const resetCaptcha = () => {
console.log('极验组件 - 重置');
captchaData.value = {
lot_number: '',
captcha_output: '',
pass_token: '',
gen_time: ''
};
if (captchaInstance && typeof captchaInstance.reset === 'function') {
captchaInstance.reset();
}
closeCaptchaWebview();
// 重新初始化验证码
isLoading.value = true;
setTimeout(() => {
initCaptcha();
}, 300);
};
// H5端挂载时自动初始化
onMounted(() => {
console.log('极验组件 - 挂载');
checkEnvironment();
// H5端自动初始化并显示极验按钮
if (isH5.value) {
initCaptcha();
}
});
onUnmounted(() => {
console.log('极验组件 - 卸载');
if (captchaScript && captchaScript.parentNode) {
captchaScript.parentNode.removeChild(captchaScript);
}
if (captchaInstance && typeof captchaInstance.reset === 'function') {
captchaInstance.reset();
}
closeCaptchaWebview();
});
onUnload(() => {
console.log('极验组件 - 页面卸载');
closeCaptchaWebview();
});
defineExpose({
reset: resetCaptcha,
initCaptcha,
showCaptcha
});
</script>
<style lang="scss" scoped>
.geetest-container {
width: 100%;
display: block;
}
.geetest-h5-wrapper {
width: 100%;
}
#geetest-h5-box {
width: 100% !important;
}
.geetest-app-container {
width: 100%;
}
.geetest-loading {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40rpx;
gap: 16rpx;
}
.loading-spinner {
width: 48rpx;
height: 48rpx;
border: 4rpx solid #e6e6e6;
border-top-color: #5b6bf8;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.loading-text {
font-size: 28rpx;
color: #909399;
}
.geetest-trigger {
display: flex;
align-items: center;
padding: 24rpx;
background-color: #f0f9ff;
border: 2rpx solid #d4e6ff;
border-radius: 16rpx;
.captcha-icon {
font-size: 32rpx;
color: #5b6bf8;
margin-right: 16rpx;
}
.captcha-text {
flex: 1;
font-size: 28rpx;
color: #333;
}
.captcha-arrow {
font-size: 24rpx;
color: #999;
}
}
.geetest-success {
display: flex;
align-items: center;
padding: 24rpx;
background-color: #f0fff4;
border: 2rpx solid #9ae6b4;
border-radius: 16rpx;
.captcha-success-icon {
font-size: 32rpx;
color: #48bb78;
margin-right: 16rpx;
}
.captcha-success-text {
flex: 1;
font-size: 28rpx;
color: #48bb78;
}
.captcha-retry {
font-size: 24rpx;
color: #5b6bf8;
}
}
/* 极验组件宽度撑满 */
.geetest-h5-wrapper :deep(.geetest_captcha),
.geetest-h5-wrapper :deep(.geetest_holder),
.geetest-h5-wrapper :deep(.geetest_popup_wrap),
.geetest-h5-wrapper :deep(.geetest_btn),
.geetest-h5-wrapper :deep(.geetest_btn_click),
.geetest-h5-wrapper :deep(.geetest_btn_text),
.geetest-h5-wrapper :deep(.geetest_radar_tip),
.geetest-h5-wrapper :deep(.geetest_item) {
width: 100% !important;
min-width: 100% !important;
max-width: 100% !important;
}
.geetest-h5-wrapper :deep(.geetest_radar) {
width: 100% !important;
}
</style>
六、业务集成点
极验验证已集成到以下业务接口:

前端调用示例(登录页面):
js
// login.vue:L184-203
await userApi.login({
username: form.value.username,
password: form.value.password,
lot_number: captchaData.value.lot_number, // 验证流水号
captcha_output: captchaData.value.captcha_output, // 验证输出
pass_token: captchaData.value.pass_token, // 验证令牌
gen_time: captchaData.value.gen_time // 时间戳
})
七、安全机制说明
- HMAC-SHA256签名 :使用 lot_number 和 geetest_key 生成签名,防止参数篡改
- 二次验证 :前端验证通过后,后端再次向极验服务器验证,确保真实性
- 超时保护 :SDK设置 timeout: 10000 ,防止长时间挂起
- 重试机制 :SDK设置 retry: 2 ,网络异常自动重试
- 降级策略 :极验服务异常时,后端默认放行( return True ),保证业务可用性
总结: 该系统采用极验V4版本,通过前端SDK收集用户行为数据,后端进行二次验证的架构。H5和App端采用不同的渲染策略,但都遵循相同的验证流程。关键是要确保 captchaId 、 challenge 和四个验证参数的正确传递。
平台兼容性
✅ H5 端 :直接加载 SDK(保持原逻辑)
✅ App 端(Android/iOS) :通过 web-view 加载 H5 页面
✅ 微信小程序 :通过 web-view 加载 H5 页面
✅ 支付宝小程序 :通过 web-view 加载 H5 页面
双端兼容实现
- H5环境 :动态加载 https://static.geetest.com/v4/gt4.js ,直接初始化极验验证码
- App环境 :通过 WebView 加载 /hybrid/html/geetest.html
验证流程概览
一个标准的验证流程涉及三方:前端(客户端) 、你的业务后端 和极验服务端。四个地址的职能如下:
| 地址(URL) | 请求方 | 作用 |
|---|---|---|
http://localhost:5555/api/geetest/init |
你的前端 | 业务方自定义API,用于获取验证配置参数 |
https://gcaptcha4.geetest.com/load |
极验JS (前端发起) | 加载验证码资源,获取验证ID |
https://gcaptcha4.geetest.com/verify |
极验JS (前端发起) | 上报前端行为数据,生成校验参数 |
https://gcaptcha4.geetest.com/validate |
你的业务后端 | 验证前端生成的校验参数是否有效 |
🔗 各接口/地址详解
1. 业务方自定义:http://localhost:5555/api/geetest/init
严格来说,这不是极验的标准API,而是你项目中的一个自定义后端接口。它的典型作用是:
- 接收前端的初始化请求。
- 从环境变量或配置中读取你的极验
captcha_id和captcha_key。 - 安全地 将
captcha_id返回给前端,但绝不暴露captcha_key。 - 返回给前端的JSON通常包含
captcha_id,有时也包含后端生成的lot_number等信息的签名。
安全第一 :极验官方强调,
captcha_key(私钥)必须且只能保存在你的服务端,绝不能以任何形式泄露给前端,否则验证机制将失效。
2. 加载验证码资源:https://gcaptcha4.geetest.com/load
这是极验服务端提供的前端API,由极验JS SDK自动调用,你无需手动处理。
- 请求方:浏览器(通过极验JS发起)。
- 作用 :根据
captcha_id加载验证码所需的静态资源与动态配置。 - 主要请求参数 :
captcha_id。 - 返回数据 :包含
lot_number,payload,process_token等用于后续验证流程的关键信息。 - 注意 :此过程受极验客户端文档所述初始化配置的影响。
3. 上报前端行为数据:https://gcaptcha4.geetest.com/verify
同为极验的前端API,在用户完成验证(如点击、滑动等)后,由JS SDK自动调用。
- 请求方:浏览器。
- 作用 :将用户的行为数据和前一阶段获取的
lot_number等信息发送给极验,进行初步分析。 - 主要请求参数 :
lot_number,captcha_output,pass_token,gen_time等。 - 返回数据 :验证是否成功的初始判断,以及用于服务端二次验证的参数(如
lot_number,captcha_output,pass_token,gen_time)。
4. 服务端二次验证:https://gcaptcha4.geetest.com/validate
这是验证流程中最关键的一环 ,由你的业务后端服务器发起。
-
请求方:你的服务端。
-
作用 :接收前端传来的验证参数,并结合你的
captcha_key进行签名,然后调用此接口获取最终、权威的验证结果。 -
请求方式 :
GET或POST。 -
请求参数:
参数名 类型 说明 lot_numberstring 验证序列号 captcha_outputstring 验证输出信息 pass_tokenstring 验证通过标识 gen_timestring 验证通过时间戳 captcha_idstring 验证ID sign_tokenstring 签名,用于验证请求合法性 -
返回参数:
参数名 类型 说明 resultstring 二次验证结果:"success"(成功)或 "fail"(失败) reasonstring 对 result的补充说明captcha_argsdict 验证输出参数
📊 完整流程详解
具体步骤说明
- 初始化 (
/api/geetest/init) :前端请求你的后端接口,获取极验的captcha_id等必要信息。 - 加载验证码 (
/load) :前端JS SDK使用captcha_id向极验服务器请求验证码资源,极验返回此次验证的唯一标识lot_number等信息。 - 前端验证 (
/verify) :用户完成验证操作后,前端JS SDK将行为数据和lot_number等一同发往极验进行分析。极验会返回一个初步结果,以及lot_number,captcha_output,pass_token,gen_time等参数。 - 服务端校验 (
/validate) :- 你的后端接收到前端提交的业务数据及上述验证参数。
- 后端以
lot_number为消息,以你的captcha_key为密钥,使用HMAC-SHA256算法生成签名sign_token。 - 后端调用极验的
/validate接口,传入lot_number,captcha_output,pass_token,gen_time,captcha_id以及刚刚生成的sign_token。 - 极验服务端返回最终验证结果
result: "success"或result: "fail"。
- 业务处理 :你的后端根据
result的结果,决定后续的业务逻辑(如允许登录或拒绝操作)。
⚠️ 注意事项
- 签名生成 :调用
/validate接口时,sign_token的签名算法是使用captcha_key对lot_number进行HMAC-SHA256运算。务必确保签名正确,否则验证会失败。 - 容灾处理 :你的后端在调用
/validate接口时,必须做好异常处理 。若因网络波动或极验服务暂时不可用导致请求失败,你应当根据业务需求,设置一个默认的验证结果(如success),避免影响用户正常使用。 - 备用域名 :极验提供了备用域名(如
gcaptcha4.geevisit.com),建议在生产环境中配置域名故障切换逻辑,以保障服务的连续性。
💎 总结
总的来说,这四个地址分工明确,共同构成了极验验证码的安全防线。其中,load和verify这两个前端API由极验JS SDK自动处理,而服务端API validate的安全性则依赖于你的后端服务对密钥的保护和正确签名。
官方提供了从Java, Python, PHP, Golang, Node.js等多种语言的服务端Demo,你可以参考这些示例代码来集成服务端逻辑。