unipp+vue3+python h5+app极验验证码集成全流程解析

极验(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 页面

双端兼容实现

验证流程概览

一个标准的验证流程涉及三方:前端(客户端)你的业务后端极验服务端。四个地址的职能如下:

地址(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_idcaptcha_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进行签名,然后调用此接口获取最终、权威的验证结果。

  • 请求方式GETPOST

  • 请求参数

    参数名 类型 说明
    lot_number string 验证序列号
    captcha_output string 验证输出信息
    pass_token string 验证通过标识
    gen_time string 验证通过时间戳
    captcha_id string 验证ID
    sign_token string 签名,用于验证请求合法性
  • 返回参数

    参数名 类型 说明
    result string 二次验证结果:"success"(成功)或 "fail"(失败)
    reason string result的补充说明
    captcha_args dict 验证输出参数

📊 完整流程详解

具体步骤说明
  1. 初始化 (/api/geetest/init) :前端请求你的后端接口,获取极验的captcha_id等必要信息。
  2. 加载验证码 (/load) :前端JS SDK使用captcha_id向极验服务器请求验证码资源,极验返回此次验证的唯一标识lot_number等信息。
  3. 前端验证 (/verify) :用户完成验证操作后,前端JS SDK将行为数据和lot_number等一同发往极验进行分析。极验会返回一个初步结果,以及lot_number, captcha_output, pass_token, gen_time等参数。
  4. 服务端校验 (/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"
  5. 业务处理 :你的后端根据result的结果,决定后续的业务逻辑(如允许登录或拒绝操作)。

⚠️ 注意事项

  • 签名生成 :调用/validate接口时,sign_token的签名算法是使用captcha_keylot_number进行HMAC-SHA256运算。务必确保签名正确,否则验证会失败。
  • 容灾处理 :你的后端在调用/validate接口时,必须做好异常处理 。若因网络波动或极验服务暂时不可用导致请求失败,你应当根据业务需求,设置一个默认的验证结果(如success),避免影响用户正常使用。
  • 备用域名 :极验提供了备用域名(如gcaptcha4.geevisit.com),建议在生产环境中配置域名故障切换逻辑,以保障服务的连续性。

💎 总结

总的来说,这四个地址分工明确,共同构成了极验验证码的安全防线。其中,loadverify这两个前端API由极验JS SDK自动处理,而服务端API validate的安全性则依赖于你的后端服务对密钥的保护和正确签名。

官方提供了从Java, Python, PHP, Golang, Node.js等多种语言的服务端Demo,你可以参考这些示例代码来集成服务端逻辑。

相关推荐
陈随易1 小时前
为什么今天还会有新语言?MoonBit 想解决什么问题?
前端·后端·程序员
ZC跨境爬虫1 小时前
跟着 MDN 学 HTML day_15:(媒体缓冲、拖动与时间范围控制)
前端·笔记·ui·html·edge浏览器·媒体
李白的天不白1 小时前
webpack 与 webpack-cli 版本匹配问题
前端·webpack·node.js
tool1 小时前
Hermes Agent 从安装到生产:我的完整踩坑记录
前端
kyriewen112 小时前
奥特曼借GPT-5.5干杯,而你的Copilot正按Token收钱
前端·gpt·ai·copilot
空中海2 小时前
05 React架构设计、项目实践与专家清单
前端·react.js·前端框架
人生鹿呀2 小时前
AI 情绪陪伴助手:从 0 到 1 的 PWA + 跨端应用实战
前端
人生鹿呀2 小时前
从零打造滑板文化社区平台:React 19 + Node.js + AI 微服务全栈实战
前端
网络工程小王3 小时前
【LCEL 链式调用详解】调用篇-2
java·服务器·前端·数据库·人工智能