波波技术栈
article

在自有服务器上完整部署 Cap 验证码(含 Go 后端验证)

本文介绍在自有服务器上完整部署 Cap 验证码的流程。Cap 是基于工作量证明的自托管 CAPTCHA 替代方案,无需图片谜题,也不向第三方发送数据。教程涵盖使用 Docker 与 Valkey 部署服务端、配置 Nginx 反向代理实现 HTTPS、前端 Web Component 集成,以及 Go 后端调用 siteverify 接口验证 token 的完整步骤。

概述

Cap 是一个自托管的 CAPTCHA 替代方案,通过工作量证明在浏览器后台静默验证用户,无需图片谜题,不向第三方发送数据。本教程指导你从部署服务端到 Go 后端集成的完整流程。

Cap 需要 Docker 和 Redis/Valkey 来运行服务端,验证组件通过 Web Component 嵌入网站。服务端提供与 reCAPTCHA 兼容的 /siteverify API,使得后端集成非常直接。

步骤一:部署 Cap 服务端

1.1 准备 docker-compose.yml

推荐使用官方 Cap Standalone 镜像,单个容器即可运行 REST API 和密钥管理控制台。

yaml

services:
  cap:
    image: tiago2/cap:latest
    container_name: cap
    ports:
      - "3000:3000"
    environment:
      ADMIN_KEY: your_strong_admin_password    # 控制台登录密码,至少 32 字符
      REDIS_URL: redis://valkey:6379
    depends_on:
      valkey:
        condition: service_healthy
    restart: unless-stopped

  valkey:
    image: valkey/valkey:9-alpine
    container_name: cap-valkey
    volumes:
      - valkey-data:/data
    command: valkey-server --save 60 1 --loglevel warning --maxmemory-policy noeviction
    healthcheck:
      test: ["CMD", "valkey-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 5
    restart: unless-stopped

volumes:
  valkey-data:

关键配置说明:

  • ADMIN_KEY:控制台密码,务必设置为强密码
  • 如 3000 端口被占用,可改为 "3001:3000"
  • 如果控制台无法访问,可在 cap 服务下添加 network_mode: "host"

1.2 启动并获取密钥

bash

docker compose up -d

打开 http://你的服务器IP:3000,用 ADMIN_KEY 登录控制台,创建一个站点密钥。你会获得:

  • Site Key:用于前端嵌入,可以公开
  • Secret Key:用于后端验证,必须保密

两个密钥都保存好,后续步骤需要用到。

步骤二:配置反向代理(HTTPS)

浏览器不允许 HTTPS 页面加载 HTTP 资源。如果你的网站是 HTTPS,Cap 服务端也必须通过 HTTPS 暴露。

Nginx 配置示例

nginx

server {
    listen 443 ssl;
    server_name cap.yourdomain.com;

    ssl_certificate /path/to/cert.pem;
    ssl_certificate_key /path/to/key.pem;

    location / {
        proxy_pass http://127.0.0.1:3000;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $remote_addr;  # 转发真实客户端 IP
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

关键点:X-Forwarded-For 必须转发真实客户端 IP,否则所有用户会共用同一个速率限制桶。如果在 Cloudflare 后面,设置环境变量 RATELIMIT_IP_HEADER=cf-connecting-ip。

步骤三:集成前端验证组件

3.1 引入脚本

html

<script src="https://cdn.jsdelivr.net/npm/cap-widget@<version>"></script>

建议固定具体版本号(如 cap-widget@0.1.58),而非使用 latest。高安全性场景可自托管该文件。

3.2 表单集成(最简单)

将 <cap-widget> 放入 <form> 内,Cap 会自动注入隐藏的 cap-token 字段,随表单一起提交,无需 JavaScript。

html

<form action="/submit" method="POST">
  <!-- 你的表单字段 -->
  <cap-widget data-cap-api-endpoint="https://cap.yourdomain.com/<site-key>/"></cap-widget>
  <button type="submit">提交</button>
</form>
  • data-cap-api-endpoint 格式:https://<你的Cap域名>/<站点密钥>/
  • 提交时,你的服务器会收到 cap-token 字段

3.3 JavaScript 集成(SPA/自定义流程)

监听 solve 事件获取 token:

html

<cap-widget id="cap" data-cap-api-endpoint="https://cap.yourdomain.com/<site-key>/"></cap-widget>

<script>
  document.getElementById("cap").addEventListener("solve", (e) => {
    const token = e.detail.token;
    // 将 token 发送到你的后端进行验证
    fetch("/api/verify", {
      method: "POST",
      body: JSON.stringify({ token })
    });
  });
</script>

步骤四:Go 后端验证 Token

用户完成验证后,你的 Go 服务器必须调用 Cap 的 /siteverify 端点来验证 token 的真实性。

4.1 验证请求

bash

curl "https://<your-instance>/<site-key>/siteverify" \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{ "secret": "<secret-key>", "response": "<captcha-token>" }'

4.2 响应格式

成功响应:

json

{ "success": true }

success 为 true 表示验证通过。

4.3 Go 实现

go

package cap

import (
    "bytes"
    "encoding/json"
    "fmt"
    "net/http"
    "time"
)

// Config 保存 Cap 验证所需配置
type Config struct {
    InstanceURL string // 如 https://cap.yourdomain.com
    SiteKey     string // 控制台获取的站点密钥
    SecretKey   string // 控制台获取的秘密密钥
}

// VerifyResponse 对应 Cap 的 siteverify 响应
type VerifyResponse struct {
    Success bool   `json:"success"`
    Error   string `json:"error,omitempty"`
}

// VerifyToken 验证 Cap token 是否有效
func VerifyToken(cfg Config, token string) (bool, error) {
    endpoint := fmt.Sprintf("%s/%s/siteverify", cfg.InstanceURL, cfg.SiteKey)

    payload := map[string]string{
        "secret":   cfg.SecretKey,
        "response": token,
    }

    body, err := json.Marshal(payload)
    if err != nil {
        return false, fmt.Errorf("marshal payload: %w", err)
    }

    client := &http.Client{Timeout: 5 * time.Second}

    req, err := http.NewRequest("POST", endpoint, bytes.NewReader(body))
    if err != nil {
        return false, fmt.Errorf("create request: %w", err)
    }
    req.Header.Set("Content-Type", "application/json")

    resp, err := client.Do(req)
    if err != nil {
        return false, fmt.Errorf("request failed: %w", err)
    }
    defer resp.Body.Close()

    if resp.StatusCode != http.StatusOK {
        return false, fmt.Errorf("unexpected status: %d", resp.StatusCode)
    }

    var result VerifyResponse
    if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
        return false, fmt.Errorf("decode response: %w", err)
    }

    return result.Success, nil
}

4.4 HTTP Handler 使用示例

go

package main

import (
    "encoding/json"
    "log"
    "net/http"
)

var capCfg = cap.Config{
    InstanceURL: "https://cap.yourdomain.com",
    SiteKey:     "your-site-key",
    SecretKey:   "your-secret-key",
}

func submitHandler(w http.ResponseWriter, r *http.Request) {
    if r.Method != http.MethodPost {
        http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
        return
    }

    if err := r.ParseForm(); err != nil {
        http.Error(w, "Bad request", http.StatusBadRequest)
        return
    }

    token := r.FormValue("cap-token")
    if token == "" {
        http.Error(w, "Missing cap-token", http.StatusBadRequest)
        return
    }

    ok, err := cap.VerifyToken(capCfg, token)
    if err != nil {
        log.Printf("Cap verification error: %v", err)
        // Fail-open: 验证服务不可用时放行(可选)
        // 或者选择 fail-closed: http.Error(w, "Verification service unavailable", http.StatusServiceUnavailable)
        http.Error(w, "Verification failed", http.StatusForbidden)
        return
    }

    if !ok {
        http.Error(w, "CAPTCHA validation failed", http.StatusForbidden)
        return
    }

    // 验证通过,处理业务逻辑
    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(map[string]string{"status": "ok"})
}

4.5 环境变量配置(推荐)

生产环境建议通过环境变量传入配置:

go

import "os"

func loadCapConfig() cap.Config {
    return cap.Config{
        InstanceURL: os.Getenv("CAP_INSTANCE_URL"),
        SiteKey:     os.Getenv("CAP_SITE_KEY"),
        SecretKey:   os.Getenv("CAP_SECRET_KEY"),
    }
}

生产环境注意事项

安全要点

项目 说明
Secret Key 保密 仅在后端使用,绝不能暴露给前端
token 一次性 每个 token 只能验证一次,不能重复使用
超时处理 建议设置 5 秒超时,Cap 服务不可达时可考虑 fail-open
反向代理转发真实 IP X-Forwarded-For必须正确转发,否则速率限制失效

速率限制

Cap 默认对验证端点进行速率限制(每 5 秒 30 次/IP)。可在控制台的 Settings 中调整,或为单个站点密钥单独覆盖。

Fail-Open 与 Fail-Closed

当 Cap 服务不可达时,你可以选择:

  • Fail-open:放行请求(用户体验优先,但可能被绕过)
  • Fail-closed:拒绝请求(安全性优先,但 Cap 故障时用户无法提交)

根据业务场景选择合适策略。Cap 的 XenForo 集成默认采用 fail-open,并记录错误日志。

迁移提示

Cap 的 /siteverify 接口与 reCAPTCHA 兼容。如果你已有 reCAPTCHA 验证代码,只需修改验证 URL 即可将流量指向 Cap,无需重写逻辑,可并行运行后平滑切换