返回正文
Main Navigation
  • 博客
  • 归档
  • 万花筒
  • 手册
    • 2026
    • 2025
    • 2024
    • 2023
    • 2022
    • 2021
    • 2020
    • 2019
    • 发布日志
    • 提交 Issue
  • 简体中文
  • English
  • 简体中文
  • English

切换主题

目录

LLM Resources

  • llms.txt

随便看看

  • 深度解析:现代单页应用(SPA)中微信授权登录的高可用架构实现
  • Vue3 项目在render函数中使用自定义指令
  • 跨标签页数据同步完全指南
  • 还在手写JSON调教大模型?.NET 9有新玩法
  • SpringBoot整合MapStruct终极指南
  • 微信小程序订阅消息实战
  • BFF 架构实战
  • Container Queries 的应用
点击前点击后

Cap 是什么 ​

PoW 工作量证明(PoW,Proof of Work)

工作量证明(PoW)是一种加密任务,客户端必须解决该任务以验证其意图并获得对资源的访问权限。这种方法在诸如比特币这样的区块链中被广泛用于验证交易,但也可用于防止服务器过载。在我们的案例中,工作量证明要求客户端在访问服务器之前执行特定计算,从而形成一种“屏障”,使大规模自动化请求变得复杂。

工作量证明(PoW)过程的运作方式如下:服务端向客户端发送一项任务,该任务要求找到一个特定值(例如,具有一定数量前缀零的哈希值)。客户端解决该任务并将其解决方案发送回服务端。如果解决方案正确,服务端将接受该请求,否则就拒绝。

工作量证明(PoW)是一种要求客户端执行特定计算以证明其合法性的方法。PoW 的理念是服务端生成需要耗费计算资源的任务。客户端接收并解决该任务,然后服务端才会接受其请求。这种机制使得诸如分布式拒绝服务(DDoS)之类的攻击成本更高,因为攻击者要完成每个 PoW 任务需要耗费大量计算资源。对于任何服务器而言,主要风险之一就是来自恶意客户端的大量请求,从而导致服务器过载甚至出现宕机情况。实际上,PoW 机制为每次请求设定了“报酬”,而“成本”就是客户端的处理时间。因此,PoW 会过滤掉一些请求,增加了完成这些请求所需的资源,从而减轻了服务器负载。这也会迫使攻击者投入更多资源,从而使得这种攻击在大多数情况下都无法奏效。

A modern, lightning-quick PoW captcha 一种现代的、闪电般快速的工作量证明验证码 Cap is a lightweight, modern open-source CAPTCHA alternative using proof-of-work Cap 是一款轻量级、现代化的开源验证码替代方案,采用工作量证明机制。

与传统验证码不同,Cap:

  • 速度快且不干扰用户
  • 不使用跟踪技术或 cookie
  • 使用工作量证明而非干扰性谜题
  • 完全可访问且可自行托管

Cap 主要由小部件(可以以不可见的方式使用)和服务器(你也可以使用独立服务器)组成。另外,它还支持机器对机器通信,并且有一个类似于 Cloudflare 的检查点中间件。

客户端 ​

以在 Vue3 + ElementPlus 中使用为例

在 index.html 引入 Cap widget:

生产环境请引入固定版本

javascript
<script src="https://cdn.jsdelivr.net/npm/@cap.js/widget"></script>
1

在 ElForm 中使用组件:

html
<el-form-item prop="code">
  <cap-widget
    id="cap"
    :data-cap-api-endpoint="capApi"
    data-cap-i18n-verifying-label="验证中..."
    data-cap-i18n-initial-state="点击验证"
    data-cap-i18n-solved-label="验证通过"
    data-cap-i18n-error-label="验证失败,请重试"
  ></cap-widget>
</el-form-item>
1
2
3
4
5
6
7
8
9
10

其中 data-cap-api-endpoint 为服务端验证 URL 我这里设置为:

typescript
const capApi = ref(`${import.meta.env.VITE_API_URL}/admin/sys/login/`);
1

data-cap-i18n 开头的几个选项为国际化设置。

设置表单,以及校验规则:

typescript
import { type FormInstance, type FormRules } from "element-plus";

const formRef = ref<FormInstance>();

let formData = reactive<
  paths["/admin/sys/login"]["post"]["requestBody"]["content"]["application/json"]
>({
  username: "",
  password: "",
  code: "",
});

const rules = reactive<FormRules<typeof formData>>({
  username: [{ required: true, message: "请输入用户名" }],
  password: [{ required: true, message: "请输入密码" }],
  code: [{ required: true, message: "请点击验证" }],
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

监听 Cap 校验结果:

tsx
onMounted(() => {
  const widget = document.querySelector("#cap");

  widget?.addEventListener("solve", function (e: any) {
    formData.code = e.detail.token;
  });
});
1
2
3
4
5
6
7

服务端 ​

以在 Nestjs 中使用为例

安装 @cap.js/server

sh
npm i @cap.js/server
1
sh
yarn add @cap.js/server
1
sh
pnpm add @cap.js/server
1
sh
bun add @cap.js/server
1

在 Service 中创建 Cap 实例:

typescript
import { InjectRepository } from "@nestjs/typeorm";
import Cap from "@cap.js/server";

@Injectable()
export class LoginService {
  // ...
  cap: Cap = new Cap({ tokens_store_path: ".data/tokensList.json" });
  //...
}
1
2
3
4
5
6
7
8
9

Cap 默认使用内存和文件存储 token,你可以将 noFSState 设置为 true,仅使用内存存储 token。你可以将此与设置 config.state 结合使用,以使用诸如 Redis 之类来存储令牌。可以参考这个 Pull requests。

在 Controller 中创建接口:

typescript
import { BadRequestException, Body, Controller, Post } from "@nestjs/common";
import { LoginService } from "./login.service";

@Controller("login")
export class LoginController {
  constructor(private readonly loginService: LoginService) {}

  @Post("/challenge")
  async challenge() {
    return this.loginService.cap.createChallenge();
  }

  @Post("/redeem")
  async redeem(
    @Body() body: { token: string; solutions: Array<[string, string, string]> }
  ) {
    const { token, solutions } = body;
    if (!token || !solutions) {
      return new BadRequestException("人机验证失败");
    }
    return this.loginService.cap.redeemChallenge({ token, solutions });
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

当用户点击客户端 Cap 组件时,将请求 /challenge 和 /redeem 获取 token。

最后在登录接口的 Service 内添加 token 验证:

typescript
// ...
const result = await this.cap.validateToken(loginDto.code);
if (!result.success) {
  throw new BadRequestException("人机验证失败");
}
// ...
1
2
3
4
5
6

What is PowCapServer ​

This is a .NET Core server implementation of the open-source project tiagozip/cap. based on the Proof-of-Work (PoW) mechanism. It provides a lightweight and non-intrusive CAPTCHA solution that can be used to prevent bot abuse and spam submissions.

📦 NuGet Packages ​

The project is split into two main libraries, which will be published to NuGet:

  • PowCapServer.Core – Core logic and services for CAPTCHA generation and validation.
  • PowCapServer.AspNetCore – ASP.NET Core integration for exposing CAPTCHA endpoints as HTTP APIs.

You can install them via:

bash
dotnet add package PowCapServer.Core
dotnet add package PowCapServer.AspNetCore
1
2

🧩 Features ​

  • ✅ Challenge generation (/api/captcha/challenge or /api/captcha/{useCase}/challenge)
  • ✅ Challenge redemption (/api/captcha/redeem or /api/captcha/{useCase}/redeem)
  • ✅ Token-based CAPTCHA validation
  • ✅ Configurable difficulty, expiration times, and endpoint paths
  • ✅ Built-in token cleanup for expired challenges
  • ✅ ASP.NET Core middleware and endpoint integration

🛠️ Usage ​

Install the NuGet packages

bash
dotnet add package PowCapServer.AspNetCore
1

Register services

cs
builder.Services.AddPowCapServer(options =>
{
    // Default configuration for CAPTCHAs without specific use case
    options.Default.ChallengeCount = 1000;
    options.Default.ChallengeSize = 32;
    options.Default.ChallengeDifficulty = 4;
    options.Default.ChallengeTokenExpiresMs = 60000;
    options.Default.CaptchaTokenExpiresMs = 120000;

    // Configuration for specific use case CAPTCHA
    options.UseCaseConfigs = new Dictionary<string, PowCapConfig>()
    {
        ["login"] = new PowCapConfig
        {
            ChallengeCount = 1000,
            ChallengeSize = 32,
            ChallengeDifficulty = 5,
            ChallengeTokenExpiresMs = 60000,
            CaptchaTokenExpiresMs = 120000
        },
        ["form"] = new PowCapConfig
        {
            ChallengeCount = 100,
            ChallengeSize = 16,
            ChallengeDifficulty = 3,
            ChallengeTokenExpiresMs = 120000,
            CaptchaTokenExpiresMs = 600000
        }
    };
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

Map CAPTCHA endpoints

cs
app.MapPowCapServer();
1

This will expose the following endpoints:

  • POST /api/captcha/challenge – Generate a new CAPTCHA challenge with default configuration.
  • POST /api/captcha/{useCase}/challenge – Generate a new CAPTCHA challenge with configuration specific to the use case.
  • POST /api/captcha/redeem – Redeem a solved CAPTCHA challenge with default configuration.
  • POST /api/captcha/{useCase}/redeem – Redeem a solved CAPTCHA challenge with configuration specific to the use case.

🗃️ Storage and Caching ​

By default, PowCapServer uses the Microsoft.Extensions.Caching.Memory implementation of IDistributedCache to store CAPTCHA-related data in memory. This provides a lightweight, in-memory storage solution that's perfect for single-instance deployments.

For more robust scenarios such as multi-instance deployments or when persistence is required, you can replace the default in-memory cache with other IDistributedCache implementations.

Popular alternatives include:

  • Redis Distributed Cache
  • SQL Server Distributed Cache

To use Redis as an example, first install the required package:

bash
dotnet add package Microsoft.Extensions.Caching.StackExchangeRedis
1

Then configure it in your service registration:

cs
builder.Services.AddPowCapServer();
builder.Services.AddStackExchangeRedisCache(options =>
{
    options.Configuration = "localhost:6379"; // Redis server configuration
    options.InstanceName = "PowCapServer:";
});
1
2
3
4
5
6

For more information on available IDistributedCache implementations, please refer to the Microsoft Documentation.

📐 Integration with Frontend ​

please refer to the official documentation of @cap.js/widget for instructions on how to embed and configure the CAPTCHA widget in your web application.

Example for default CAPTCHA:

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

<cap-widget id="cap" data-cap-api-endpoint="/api/captcha/"></cap-widget>
1
2
3

Example for specific use case CAPTCHA (e.g. login):

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

<cap-widget id="cap" data-cap-api-endpoint="/api/captcha/login/"></cap-widget>
1
2
3
Vue SFC
<template>
  <a-spin :spinning="spinning" class="login-form-body-spin" tip="登录中...">
    <a-form scrolltofirsterror hiderequiredmark :layout="'vertical'">
      <a-form-item name="username" :validateFirst="true" v-bind="validateInfos.username">
        <a-input
          autocomplete="off"
          has-feedback
          v-model:value="modelRef.username"
          placeholder="登录用户名"
        >
          <template #prefix>
            <user-outlined />
          </template>
          <template #addonAfter> </template>
        </a-input>
      </a-form-item>

      <a-form-item has-feedback name="password" v-bind="validateInfos.password">
        <a-input-password
          autocomplete="off"
          v-model:value="modelRef.password"
          @paste.capture.prevent="false"
          placeholder="登录密码"
        >
          <template #prefix>
            <key-outlined />
          </template>
        </a-input-password>
      </a-form-item>

      <a-form-item has-feedback name="captchaCode" v-bind="validateInfos.captchaCode">
        <a-input v-model:value="modelRef.captchaCode">
          <template #addonAfter>
            <captcha-code :client-id="modelRef.clientId" />
          </template>
          <template #prefix>
            <safety-outlined />
          </template>
        </a-input>
      </a-form-item>

      <a-form-item has-feedback name="captchaCode" v-bind="validateInfos.captchaCode">
        <a-input v-model:value="modelRef.captchaCode">
          <template #addonAfter>
            <cap-widget
              ref="cap"
              onsolve="console.log(`Token: ${event.detail.token}`)"
              :data-cap-api-endpoint="capApi"
              data-cap-i18n-verifying-label="验证中..."
              data-cap-i18n-initial-state="点击验证"
              data-cap-i18n-solved-label="验证通过"
              data-cap-i18n-error-label="验证失败,请重试"
            ></cap-widget>
          </template>
          <template #prefix>
            <safety-outlined />
          </template>
        </a-input>
      </a-form-item>

      <a-form-item>
        <a-space style="width: 100%" direction="vertical">
          <a-button type="primary" block @click="onSubmit">登录</a-button>
        </a-space>
      </a-form-item>
    </a-form>
  </a-spin>
</template>

<script setup lang="ts">
import { useForm } from 'ant-design-vue/lib/form'
import { JSEncrypt } from 'jsencrypt'
import { v4 as uuidv4 } from 'uuid'
import { onMounted, reactive, ref, toRaw, useTemplateRef } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import useHotkey, { type HotKey } from 'vue3-hotkey'

import { jwtAuth } from '@/apis/authentication/account.api'
import { defaultHomePath } from '@/router'
import { useAuthStore } from '@/stores/auth.store'
import type { ValidateErrorEntity } from 'ant-design-vue/es/form/interface'

const hotkeys = ref<HotKey[]>([
  {
    keys: ['enter'],
    preventDefault: true,
    handler(keys) {
      console.log(keys)
      onSubmit()
    },
  },
])
useHotkey(hotkeys.value)

const capApi = ref(`http://localhost:5075/api/captcha/form`)

interface LoginFormState {
  username: string
  password: string
  captchaCode: string
  clientId: string
}

const modelRef = reactive({
  username: '',
  password: '',
  captchaCode: '',
  clientId: uuidv4(),
})
const spinning = ref<boolean>(false)

const rulesRef = reactive({
  username: [
    {
      required: true,
      trigger: ['blur', 'change'],
      message: '请输入登录用户名',
    },
  ],
  password: [
    {
      required: true,
      message: '请输入登录密码',
      trigger: ['blur', 'change'],
    },
  ],
})

const { validate, validateInfos } = useForm(modelRef, rulesRef)

const router = useRouter()
const route = useRoute()

const captchaRef = ref()

const onReady = (handle: any) => {
  console.log('Captcha is ready', handle)
}

const onError = (error: any) => {
  console.error('Captcha error:', error)
}

const capRef = useTemplateRef('cap')

onMounted(() => {
  console.log('cap', capRef.value)
  capRef.value.addEventListener('solve', function (e: any) {
    const token = e.detail.token
    console.log('solve token', token)
    // handle the token as needed
  })
  capRef.value.addEventListener('error', (e: any) => console.error('Cap错误:', e.detail))
  capRef.value.addEventListener('progress', (e: any) => console.log('Cap progress:', e.detail))
  capRef.value.addEventListener('reset', (e: any) => console.warn('Cap reset:', e.detail))
})

const onSubmit = async () => {
  // const cap = new window.Cap({
  //   apiEndpoint: 'http://localhost:5075/api/captcha/form/',
  // })
  // cap.addEventListener('solve', (e) => console.log('成功:', e.detail.token))
  // cap.addEventListener('error', (e) => console.error('Cap错误:', e.detail))
  // try {
  //   const solution = await cap.solve()
  //   alert(solution.token)
  // } catch (e) {
  //   console.error('调用失败:', e)
  // }

  validate()
    .then(() => {
      console.log(toRaw(modelRef))
      data.spinning = true

      // 新建一个JSEncrypt对象
      const encryptor = new JSEncrypt()
      // 设置公钥 (这是后端直接给我的,看你们项目情况是需要调接口获得,还是程序中直接写死)
      const publicKey =
        'MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDlOJu6TyygqxfWT7eLtGDwajtNFOb9I5XRb6khyfD1Yt3YiCgQWMNW649887VGJiGr/L5i2osbl8C9+WJTeucF+S76xFxdU6jE0NQ+Z+zEdhUTooNRaY5nZiu5PgDB0ED/ZKBUSLKL7eibMxZtMlUDHjm4gwQco1KRMDSmXSMkDwIDAQAB'

      encryptor.setPublicKey(publicKey) // publicKey为公钥
      // 加密数据
      const password = encryptor.encrypt(modelRef.password)

      jwtAuth({
        platform: import.meta.env.VITE_APP_NAME,
        username: modelRef.username,
        password: password.toString(),
        clientId: modelRef.clientId,
        captchaCode: modelRef.captchaCode,
      }).then((token) => {
        console.log(token)
        useAuthStore().setJsonWebToken(token)

        Promise.all([useAuthStore().getProfile(), useAuthStore().getBehavior()]).then((jsons) => {
          console.log(jsons)
          let path = defaultHomePath

          const { redirect } = route.query
          console.log('redirect', redirect)
          if (redirect && redirect !== '') {
            path = redirect + ''
          }

          router.replace({
            path: path,
          })
        })
      })
    })

    .catch((error: ValidateErrorEntity<LoginFormState>) => {
      console.log('error', error)
    })
  // .finally(() => { })
}

const data = reactive({
  spinning: false,
  verifyCodeLoading: false,
  disableSendButton: true,
  formattedCellPhone: '',
})

onMounted(() => {
  const widget = document.querySelector('#cap')

  widget?.addEventListener('solve', function (e) {
    console.log(e)

    // handle the token as needed
  })
})
</script>

<style lang="less">
cap-widget {
  --cap-background: transparent;
  --cap-border-color: transparent;
  --cap-border-radius: 14px;
  --cap-widget-height: 30px;
  --cap-widget-width: 200px;
  --cap-widget-padding: 14px;
  --cap-gap: 15px;
  --cap-color: #212121;
  --cap-checkbox-size: 25px;
  --cap-checkbox-border: 1px solid #aaaaaad1;
  --cap-checkbox-border-radius: 6px;
  --cap-checkbox-background: #fafafa91;
  --cap-checkbox-margin: 2px;
  --cap-font:
    AlibabaPuHuiTi, HYCuJianHeiJ, PingFangSC, HYZhongJianHeiJ, Georgia, Avenir, Helvetica,
    'BlinkMacSystemFont', '.SFNSText-Regular', 'San Francisco', 'Roboto', 'Segoe UI',
    'Helvetica Neue', 'Lucida Grande', 'Ubuntu', 'arial', sans-serif;
  --cap-spinner-color: #000;
  --cap-spinner-background-color: #eee;
  --cap-spinner-thickness: 5px;
  // --cap-checkmark: url('data:image/svg+xml,...);
  // --cap-error-cross: url('data:image/svg+xml,...');
}
</style>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
效果预览

You can listen to the solve event to obtain the generated token and proceed with your form submission or API calls.

🧪 Validate the CAPTCHA Token in a Controller ​

To use the CAPTCHA token validation in a real-world scenario, you can inject ICaptchaService into any controller (e.g., a LoginController) and verify the token submitted by the client.

Example: Validate Token in LoginController

cs
[ApiController]
[Route("[controller]")]
public class LoginController : ControllerBase
{
    private readonly ICaptchaService _captchaService;

    public LoginController(ICaptchaService captchaService)
    {
        _captchaService = captchaService;
    }

    [HttpPost]
    public async Task<IActionResult> Login([FromBody] LoginRequest request, CancellationToken ct = default)
    {
        if (string.IsNullOrEmpty(request.CaptchaToken))
        {
            return BadRequest("CAPTCHA token is required.");
        }

        var isValid = await _captchaService.ValidateCaptchaTokenAsync(request.CaptchaToken, ct);

        if (!isValid)
        {
            return BadRequest("Invalid or expired CAPTCHA token.");
        }

        // Proceed with login logic
        return Ok(new { message = "Login successful" });
    }
}

public class LoginRequest
{
    public string Username { get; set; }
    public string Password { get; set; }
    public string CaptchaToken { get; set; }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
  • The ICaptchaService is injected via constructor injection.
  • The ValidateCaptchaTokenAsync method is used to verify the token submitted by the client.
  • This helps prevent bot abuse on critical endpoints such as login, registration, or form submission.

📚 View Sample Project ​

Please check the samples/WebApplication1 folder in the source code. It includes:

✅ A full ASP.NET Core web application integrated with PowCapServer ✅ Frontend usage with the @cap.js/widget ✅ Example controller usage for token validation

/zh-CN/gallery/PowCap.html 的头像
您觉得这篇文章
怎么样?
编辑本页面

上次更新:

V 0.13.12 |
基于 MIT Licensed版权所有 © 2009- 2026 CMONO.NET
本站访客数
--次
𝓒𝓜𝓞𝓝𝓞.𝓝𝓔𝓣
本站总访问量
--人次