Captcha v4

业务容灾

确保验证服务异常时不阻塞业务流程

核心原则: 验证服务异常时,建议放行请求,避免阻塞业务流程。宁可放行,不可阻塞。

快速理解

降级方案是什么?

当 Geelab 验证服务暂时不可用时,自动允许用户通过验证,避免阻塞您的业务流程(如登录、注册)。

谁需要关注?

  • 后端开发:必须实现服务端降级逻辑(本文档重点)
  • ℹ️ 前端开发:SDK 已自动处理,仅需配置域名白名单
  • ℹ️ 运维团队:需要配置监控和告警

容灾流程图

容灾流程图

触发条件与异常场景

容灾模式在以下情况下自动触发:

触发条件影响范围容灾行为用户体验
客户端 load 请求失败
(HTTP 状态码非 200 或超时)
前端验证码无法加载验证码自动切换为一键通过模式点击即可通过
服务端 validate 请求失败
(HTTP 状态码非 200 或超时)
二次验证无法完成服务端返回默认成功结果登录/注册正常进行
双端同时异常前端和后端都无法正常工作前端一键通过 + 后端默认成功业务完全不受影响

容灾模式的触发是自动的,无需手动干预。SDK 和服务端代码会自动处理异常情况。

服务端实现

以下代码应添加到您的二次验证接口中,替换原有的验证逻辑。完整的集成示例请参考 服务端集成文档

Python 容灾实现
import requests
import logging
from datetime import datetime

def validate_captcha(lot_number, captcha_output, pass_token, gen_time):
    """
    二次验证函数(带容灾)

    参数:
        lot_number: 验证流水号
        captcha_output: 验证输出信息
        pass_token: 验证通过标识
        gen_time: 验证时间戳

    返回:
        dict: {'result': 'success'/'fail', 'reason': '...'}
    """
    query = {
        'lot_number': lot_number,
        'captcha_output': captcha_output,
        'pass_token': pass_token,
        'gen_time': gen_time,
        'captcha_id': 'YOUR_CAPTCHA_ID',       # 从配置中读取
        'sign_token': calculate_sign_token(lot_number),  # 计算签名
    }

    # 验证接口 URL(根据地域选择)
    url = 'https://cap-global.geelabapi.com/validate'

    try:
        # 发起二次验证请求(5 秒超时)
        response = requests.post(url, data=query, timeout=5)

        # 检查 HTTP 状态码
        if response.status_code != 200:
            raise Exception(f'HTTP status code: {response.status_code}')

        return response.json()

    except requests.Timeout:
        logging.warning('Captcha fallback: timeout', extra={
            'lot_number': lot_number,
            'timestamp': datetime.now().isoformat(),
        })
        return {'result': 'success', 'reason': 'request timeout, fallback'}

    except requests.ConnectionError:
        logging.warning('Captcha fallback: connection error', extra={
            'lot_number': lot_number,
            'timestamp': datetime.now().isoformat(),
        })
        return {'result': 'success', 'reason': 'connection error, fallback'}

    except Exception as e:
        logging.warning('Captcha fallback: exception', extra={
            'error_type': type(e).__name__,
            'error_message': str(e),
            'lot_number': lot_number,
            'timestamp': datetime.now().isoformat(),
        })
        return {'result': 'success', 'reason': 'request geelab api fail'}

建议设置 5 秒超时,避免长时间等待影响用户体验。

Node.js 容灾实现
const axios = require('axios');

/**
 * 二次验证函数(带容灾)
 * @param {string} lotNumber - 验证流水号
 * @param {string} captchaOutput - 验证输出信息
 * @param {string} passToken - 验证通过标识
 * @param {string} genTime - 验证时间戳
 * @returns {Promise<Object>} 验证结果
 */
async function validateCaptcha(lotNumber, captchaOutput, passToken, genTime) {
    const query = {
        lot_number: lotNumber,
        captcha_output: captchaOutput,
        pass_token: passToken,
        gen_time: genTime,
        captcha_id: process.env.CAPTCHA_ID,        // 从环境变量读取
        sign_token: calculateSignToken(lotNumber),  // 计算签名
    };

    // 验证接口 URL(根据地域选择)
    const url = 'https://cap-global.geelabapi.com/validate';

    try {
        const response = await axios.post(url, query, { timeout: 5000 });

        if (response.status !== 200) {
            throw new Error(`HTTP status: ${response.status}`);
        }

        return response.data;

    } catch (error) {
        console.warn('Captcha fallback triggered:', {
            error: error.message,
            lotNumber,
            timestamp: new Date().toISOString(),
        });

        return { result: 'success', reason: 'request geelab api fail' };
    }
}

module.exports = { validateCaptcha };
Java 容灾实现
import java.net.URI;
import java.net.http.*;
import java.time.Duration;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class CaptchaValidator {
    private static final Logger logger = LoggerFactory.getLogger(CaptchaValidator.class);
    private static final String CAPTCHA_ID = System.getenv("CAPTCHA_ID");
    private static final String VALIDATE_URL = "https://cap-global.geelabapi.com/validate";

    /**
     * 二次验证方法(带容灾)
     */
    public ValidationResult validate(String lotNumber, String captchaOutput,
                                     String passToken, String genTime) {
        try {
            String params = String.format(
                "lot_number=%s&captcha_output=%s&pass_token=%s&gen_time=%s&captcha_id=%s&sign_token=%s",
                lotNumber, captchaOutput, passToken, genTime,
                CAPTCHA_ID, calculateSignToken(lotNumber)
            );

            HttpClient client = HttpClient.newHttpClient();
            HttpRequest httpRequest = HttpRequest.newBuilder()
                .uri(URI.create(VALIDATE_URL))
                .timeout(Duration.ofSeconds(5))
                .header("Content-Type", "application/x-www-form-urlencoded")
                .POST(HttpRequest.BodyPublishers.ofString(params))
                .build();

            HttpResponse<String> response = client.send(
                httpRequest, HttpResponse.BodyHandlers.ofString()
            );

            if (response.statusCode() != 200) {
                throw new Exception("HTTP status: " + response.statusCode());
            }

            return new ObjectMapper().readValue(response.body(), ValidationResult.class);

        } catch (Exception e) {
            logger.warn("Captcha fallback: {}, lotNumber: {}", e.getMessage(), lotNumber);

            ValidationResult result = new ValidationResult();
            result.setResult("success");
            result.setReason("request geelab api fail");
            return result;
        }
    }
}
PHP 容灾实现
<?php
/**
 * 二次验证函数(带容灾)
 */
function validateCaptcha($lotNumber, $captchaOutput, $passToken, $genTime) {
    $query = [
        'lot_number'     => $lotNumber,
        'captcha_output' => $captchaOutput,
        'pass_token'     => $passToken,
        'gen_time'       => $genTime,
        'captcha_id'     => getenv('CAPTCHA_ID'),
        'sign_token'     => calculateSignToken($lotNumber),
    ];

    // 验证接口 URL(根据地域选择)
    $url = 'https://cap-global.geelabapi.com/validate';

    try {
        $context = stream_context_create([
            'http' => [
                'method'  => 'POST',
                'header'  => 'Content-Type: application/x-www-form-urlencoded',
                'content' => http_build_query($query),
                'timeout' => 5,
            ],
        ]);

        $response = @file_get_contents($url, false, $context);

        if ($response === false) {
            throw new Exception('Request failed');
        }

        return json_decode($response, true);

    } catch (Exception $e) {
        error_log(sprintf(
            'Captcha fallback: %s, lotNumber: %s',
            $e->getMessage(), $lotNumber
        ));

        return ['result' => 'success', 'reason' => 'request geelab api fail'];
    }
}
?>
Go 容灾实现
package main

import (
    "encoding/json"
    "log"
    "net/http"
    "net/url"
    "os"
    "strings"
    "time"
)

type ValidationResult struct {
    Result string `json:"result"`
    Reason string `json:"reason"`
}

// ValidateCaptcha 二次验证函数(带容灾)
func ValidateCaptcha(lotNumber, captchaOutput, passToken, genTime string) ValidationResult {
    fallback := ValidationResult{Result: "success", Reason: "request geelab api fail"}

    data := url.Values{
        "lot_number":     {lotNumber},
        "captcha_output": {captchaOutput},
        "pass_token":     {passToken},
        "gen_time":       {genTime},
        "captcha_id":     {os.Getenv("CAPTCHA_ID")},
        "sign_token":     {calculateSignToken(lotNumber)},
    }

    // 验证接口 URL(根据地域选择)
    validateURL := "https://cap-global.geelabapi.com/validate"

    client := &http.Client{Timeout: 5 * time.Second}
    resp, err := client.Post(
        validateURL,
        "application/x-www-form-urlencoded",
        strings.NewReader(data.Encode()),
    )

    if err != nil {
        log.Printf("Captcha fallback: err=%v, lotNumber=%s", err, lotNumber)
        return fallback
    }
    defer resp.Body.Close()

    if resp.StatusCode != 200 {
        log.Printf("Captcha fallback: status=%d, lotNumber=%s", resp.StatusCode, lotNumber)
        return fallback
    }

    var result ValidationResult
    if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
        log.Printf("Captcha fallback: decode err=%v, lotNumber=%s", err, lotNumber)
        return fallback
    }

    return result
}
C# 容灾实现
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;

public class CaptchaValidator
{
    private readonly ILogger<CaptchaValidator> _logger;
    private static readonly HttpClient _client = new HttpClient
    {
        Timeout = TimeSpan.FromSeconds(5)
    };
    private const string ValidateUrl = "https://cap-global.geelabapi.com/validate";

    public CaptchaValidator(ILogger<CaptchaValidator> logger)
    {
        _logger = logger;
    }

    /// <summary>
    /// 二次验证方法(带容灾)
    /// </summary>
    public async Task<ValidationResult> ValidateCaptcha(
        string lotNumber, string captchaOutput, string passToken, string genTime)
    {
        try
        {
            var content = new FormUrlEncodedContent(new Dictionary<string, string>
            {
                { "lot_number",     lotNumber },
                { "captcha_output", captchaOutput },
                { "pass_token",     passToken },
                { "gen_time",       genTime },
                { "captcha_id",     Environment.GetEnvironmentVariable("CAPTCHA_ID") },
                { "sign_token",     CalculateSignToken(lotNumber) },
            });

            var response = await _client.PostAsync(ValidateUrl, content);
            response.EnsureSuccessStatusCode();

            var body = await response.Content.ReadAsStringAsync();
            return JsonConvert.DeserializeObject<ValidationResult>(body);
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "Captcha fallback triggered, lotNumber: {LotNumber}", lotNumber);

            return new ValidationResult { Result = "success", Reason = "request geelab api fail" };
        }
    }
}

public class ValidationResult
{
    public string Result { get; set; }
    public string Reason { get; set; }
}

容灾测试

在完成所有接入工作后,请一定要进行全面的业务容灾测试。

联系技术支持开启容灾测试

提供 captcha_id,联系技术支持开启容灾测试模式。

测试客户端异常场景

测试接口: load

预期表现: 前端验证直接加载一键通过

测试服务端异常场景

测试接口: validate

预期表现: 服务端返回成功,业务登录成功

测试双端异常场景

测试接口: load + validate

预期表现: 前端验证直接加载一键通过后,业务登录成功

常见问题

什么场景下会触发容灾模式?

客户端交互请求(load)和服务端交互请求(validate)HTTP Status Code 非 200 时,默认走 Geelab 容灾流程,触发对应场景的容灾模式。

容灾模式的触发是自动的,无需手动干预。

为什么部分请求二次校验失败返回 pass_token error?

如客户端异常场景下,直接加载一键通过,此时一键通过的 pass_token 为本地生成。在服务端交互正常时进行二次校验将验证失败返回 pass_token error,这是为了防止伪造客户端异常场景而绕过验证。

真实的客户端异常多由于网络问题造成,可通过切换/刷新网络等自助手段解决。部分极端场景下 Geelab 可开启流量放行保障业务连续。

如何设置合理的超时时间?

建议设置 5 秒超时时间,这是在用户体验和服务稳定性之间的最佳平衡点。

是否需要记录降级日志?

强烈建议记录详细的降级日志,包括:

  • 降级触发时间
  • 错误类型和详细信息
  • 用户 IP 和请求参数
  • lot_number(如果有)

这些日志对于问题排查和服务优化非常重要。

如何在开发环境测试降级逻辑是否生效?

无需联系技术支持,可以在本地模拟异常:

Python 本地测试示例
# 方法一:设置极短超时触发 Timeout
response = requests.post(url, data=query, timeout=0.001)

# 方法二:使用不存在的域名触发 ConnectionError
url = 'https://invalid-domain.geelabapi.com/validate'

# 方法三:使用 mock 单元测试
from unittest.mock import patch
with patch('requests.post', side_effect=requests.Timeout):
    result = validate_captcha(lot_number, ...)
    assert result['result'] == 'success'

降级期间对安全性有什么影响?

降级模式下验证强度有所降低,但内置机制可防止主动伪造:

  • 客户端异常时:本地生成的 token 在服务端正常时会被拒绝
  • 服务端异常时:请求被放行,无法拦截机器行为

建议在降级率较高时,对高风险业务(如批量注册)额外增加频控或人工审核。

如何判断当前是否处于降级状态?

  • 客户端: 验证码显示"一键通过"按钮 = 客户端降级
  • 服务端: 查看日志中的 reason 字段,包含 fallbackfail 说明触发了降级

下一步