业务容灾
确保验证服务异常时不阻塞业务流程
核心原则: 验证服务异常时,建议放行请求,避免阻塞业务流程。宁可放行,不可阻塞。
快速理解
降级方案是什么?
当 Geelab 验证服务暂时不可用时,自动允许用户通过验证,避免阻塞您的业务流程(如登录、注册)。
谁需要关注?
- ✅ 后端开发:必须实现服务端降级逻辑(本文档重点)
- ℹ️ 前端开发:SDK 已自动处理,仅需配置域名白名单
- ℹ️ 运维团队:需要配置监控和告警
容灾流程图
触发条件与异常场景
容灾模式在以下情况下自动触发:
| 触发条件 | 影响范围 | 容灾行为 | 用户体验 |
|---|---|---|---|
| 客户端 load 请求失败 (HTTP 状态码非 200 或超时) | 前端验证码无法加载 | 验证码自动切换为一键通过模式 | 点击即可通过 |
| 服务端 validate 请求失败 (HTTP 状态码非 200 或超时) | 二次验证无法完成 | 服务端返回默认成功结果 | 登录/注册正常进行 |
| 双端同时异常 | 前端和后端都无法正常工作 | 前端一键通过 + 后端默认成功 | 业务完全不受影响 |
容灾模式的触发是自动的,无需手动干预。SDK 和服务端代码会自动处理异常情况。
服务端实现
以下代码应添加到您的二次验证接口中,替换原有的验证逻辑。完整的集成示例请参考 服务端集成文档。
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 秒超时,避免长时间等待影响用户体验。
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 };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
/**
* 二次验证函数(带容灾)
*/
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'];
}
}
?>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
}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(如果有)
这些日志对于问题排查和服务优化非常重要。
如何在开发环境测试降级逻辑是否生效?
无需联系技术支持,可以在本地模拟异常:
# 方法一:设置极短超时触发 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字段,包含fallback或fail说明触发了降级
下一步
- 查看 服务端集成 了解完整的验证实现
- 阅读 常见问题 解决集成问题
- 访问 Geelab 控制台 查看验证数据