Skip to content

Commit da13c22

Browse files
authored
Merge pull request #36 from EdiWang/feature/stateless-captcha
Add Stateless Captcha Support
2 parents e0fc07c + f51fe13 commit da13c22

File tree

11 files changed

+521
-32
lines changed

11 files changed

+521
-32
lines changed

README.md

Lines changed: 178 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Edi.Captcha.AspNetCore
1+
# Edi.Captcha.AspNetCore
22
The Captcha module used in my blog
33

44
[![.NET](https://github.com/EdiWang/Edi.Captcha.AspNetCore/actions/workflows/dotnet.yml/badge.svg)](https://github.com/EdiWang/Edi.Captcha.AspNetCore/actions/workflows/dotnet.yml)
@@ -8,9 +8,7 @@ The Captcha module used in my blog
88
[main-nuget]: https://www.nuget.org/packages/Edi.Captcha/
99
[main-nuget-badge]: https://img.shields.io/nuget/v/Edi.Captcha.svg?style=flat-square&label=nuget
1010

11-
## Usage
12-
13-
### 0. Install from NuGet
11+
## Install
1412

1513
NuGet Package Manager
1614
```
@@ -23,6 +21,8 @@ or .NET CLI
2321
dotnet add package Edi.Captcha
2422
```
2523

24+
## Session-Based Captcha (Traditional Approach)
25+
2626
### 1. Register in DI
2727

2828
```csharp
@@ -186,18 +186,184 @@ public async Task<IActionResult> SomeAction(YourModelWithCaptchaCode model)
186186
}
187187
```
188188

189-
Refer to https://edi.wang/post/2018/10/13/generate-captcha-code-aspnet-core
190189

191-
## 免责申明
190+
## Stateless Captcha (Recommended for Scalable Applications)
191+
192+
**Advantages of Stateless Captcha:**
193+
- ✅ Works in clustered/load-balanced environments
194+
- ✅ No server-side session storage required
195+
- ✅ Built-in expiration through encryption
196+
- ✅ Secure token-based validation
197+
- ✅ Better scalability
198+
- ✅ Single API call for both token and image
199+
200+
### 1. Register in DI
201+
202+
```csharp
203+
services.AddStatelessCaptcha();
204+
```
205+
206+
or with custom options:
207+
208+
```csharp
209+
services.AddStatelessCaptcha(options =>
210+
{
211+
options.Letters = "2346789ABCDGHKMNPRUVWXYZ";
212+
options.CodeLength = 4;
213+
options.TokenExpiration = TimeSpan.FromMinutes(5);
214+
});
215+
```
216+
217+
### 2. Create Model with Token Support
192218

193-
此项目(Edi.Captcha.AspNetCore)及其配套组件均为免费开源的产品,仅用于学习交流,并且不直接向中国提供服务,中国访客请于下载后立即删除。
219+
```csharp
220+
public class StatelessHomeModel
221+
{
222+
[Required]
223+
[StringLength(4)]
224+
public string CaptchaCode { get; set; }
225+
226+
public string CaptchaToken { get; set; }
227+
}
228+
```
194229

195-
任何中国境内的组织及个人不得使用此项目(Edi.Captcha.AspNetCore)及其配套组件构建任何形式的面向中国境内访客的网站或服务。
230+
### 3. Example Controller
196231

197-
不可用于任何违反中华人民共和国(含台湾省)或使用者所在地区法律法规的用途。
232+
```csharp
233+
using Edi.Captcha.SampleApp.Models;
234+
using Microsoft.AspNetCore.Mvc;
235+
using System;
236+
using System.Diagnostics;
198237

199-
因为作者即本人仅完成代码的开发和开源活动(开源即任何人都可以下载使用),从未参与访客的任何运营和盈利活动。
238+
namespace Edi.Captcha.SampleApp.Controllers;
200239

201-
且不知晓访客后续将程序源代码用于何种用途,故访客使用过程中所带来的任何法律责任即由访客自己承担。
240+
public class StatelessController(IStatelessCaptcha captcha) : Controller
241+
{
242+
public IActionResult Index()
243+
{
244+
return View(new StatelessHomeModel());
245+
}
202246

203-
[《开源软件有漏洞,作者需要负责吗?是的!》](https://go.edi.wang/aka/os251)
247+
[HttpPost]
248+
public IActionResult Index(StatelessHomeModel model)
249+
{
250+
if (ModelState.IsValid)
251+
{
252+
bool isValidCaptcha = captcha.Validate(model.CaptchaCode, model.CaptchaToken);
253+
return Content(isValidCaptcha ? "Success - Stateless captcha validated!" : "Invalid captcha code");
254+
}
255+
256+
return BadRequest();
257+
}
258+
259+
[Route("get-stateless-captcha")]
260+
public IActionResult GetStatelessCaptcha()
261+
{
262+
var result = captcha.GenerateCaptcha(100, 36);
263+
264+
return Json(new {
265+
token = result.Token,
266+
imageBase64 = Convert.ToBase64String(result.ImageBytes)
267+
});
268+
}
269+
270+
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
271+
public IActionResult Error()
272+
{
273+
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
274+
}
275+
}
276+
```
277+
278+
### 4. Example View
279+
280+
```razor
281+
@model StatelessHomeModel
282+
@{
283+
ViewData["Title"] = "Stateless Captcha Example";
284+
}
285+
286+
<div class="text-center">
287+
<h1 class="display-4">Stateless Captcha Example</h1>
288+
<p>This example shows how to use stateless captcha that works in clustered environments.</p>
289+
</div>
290+
291+
<div class="row">
292+
<div class="col-md-6 offset-md-3">
293+
<div class="card">
294+
<div class="card-header">
295+
<h5>Stateless Captcha Form</h5>
296+
</div>
297+
<div class="card-body">
298+
<form asp-action="Index" method="post" id="stateless-form">
299+
<div class="form-group mb-3">
300+
<label>Captcha Image:</label>
301+
<div class="d-flex align-items-center">
302+
<img id="captcha-image" src="" alt="Captcha" class="me-2" style="border: 1px solid #ccc;" />
303+
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="refreshCaptcha()">
304+
🔄 Refresh
305+
</button>
306+
</div>
307+
<small class="form-text text-muted">Click refresh to get a new captcha</small>
308+
</div>
309+
310+
<div class="form-group mb-3">
311+
<label asp-for="CaptchaCode">Enter Captcha Code:</label>
312+
<input asp-for="CaptchaCode" class="form-control" placeholder="Enter the code from image" autocomplete="off" />
313+
<span asp-validation-for="CaptchaCode" class="text-danger"></span>
314+
</div>
315+
316+
<input type="hidden" asp-for="CaptchaToken" id="captcha-token" />
317+
318+
<div class="form-group">
319+
<button type="submit" class="btn btn-primary">Submit</button>
320+
<a asp-controller="Home" asp-action="Index" class="btn btn-secondary">Session-based Example</a>
321+
</div>
322+
</form>
323+
324+
<div class="mt-4">
325+
<h6>Advantages of Stateless Captcha:</h6>
326+
<ul class="small">
327+
<li>✅ Works in clustered/load-balanced environments</li>
328+
<li>✅ No server-side session storage required</li>
329+
<li>✅ Built-in expiration through encryption</li>
330+
<li>✅ Secure token-based validation</li>
331+
<li>✅ Better scalability</li>
332+
<li>✅ Single API call for both token and image</li>
333+
</ul>
334+
</div>
335+
</div>
336+
</div>
337+
</div>
338+
</div>
339+
340+
<script>
341+
async function refreshCaptcha() {
342+
try {
343+
const response = await fetch('/get-stateless-captcha');
344+
const data = await response.json();
345+
346+
// Set the token for validation
347+
document.getElementById('captcha-token').value = data.token;
348+
349+
// Set the image source using base64 data
350+
document.getElementById('captcha-image').src = `data:image/png;base64,${data.imageBase64}`;
351+
352+
// Clear the input
353+
document.getElementById('CaptchaCode').value = '';
354+
} catch (error) {
355+
console.error('Error refreshing captcha:', error);
356+
alert('Failed to load captcha. Please try again.');
357+
}
358+
}
359+
360+
// Initialize captcha on page load
361+
document.addEventListener('DOMContentLoaded', function() {
362+
refreshCaptcha();
363+
});
364+
</script>
365+
366+
@section Scripts {
367+
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
368+
}
369+
```
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
using Edi.Captcha.SampleApp.Models;
2+
using Microsoft.AspNetCore.Mvc;
3+
using System;
4+
using System.Diagnostics;
5+
6+
namespace Edi.Captcha.SampleApp.Controllers;
7+
8+
public class StatelessController(IStatelessCaptcha captcha) : Controller
9+
{
10+
public IActionResult Index()
11+
{
12+
return View(new StatelessHomeModel());
13+
}
14+
15+
[HttpPost]
16+
public IActionResult Index(StatelessHomeModel model)
17+
{
18+
if (ModelState.IsValid)
19+
{
20+
bool isValidCaptcha = captcha.Validate(model.CaptchaCode, model.CaptchaToken);
21+
return Content(isValidCaptcha ? "Success - Stateless captcha validated!" : "Invalid captcha code");
22+
}
23+
24+
return BadRequest();
25+
}
26+
27+
[Route("get-stateless-captcha")]
28+
public IActionResult GetStatelessCaptcha()
29+
{
30+
var result = captcha.GenerateCaptcha(100, 36);
31+
32+
return Json(new {
33+
token = result.Token,
34+
imageBase64 = Convert.ToBase64String(result.ImageBytes)
35+
});
36+
}
37+
38+
[ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)]
39+
public IActionResult Error()
40+
{
41+
return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier });
42+
}
43+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
using System.ComponentModel.DataAnnotations;
2+
3+
namespace Edi.Captcha.SampleApp.Models;
4+
5+
public class StatelessHomeModel
6+
{
7+
[Required]
8+
[StringLength(4)]
9+
public string CaptchaCode { get; set; }
10+
11+
public string CaptchaToken { get; set; }
12+
}

src/Edi.Captcha.SampleApp/Startup.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,14 @@ public void ConfigureServices(IServiceCollection services)
3838
//option.DrawLines = false;
3939
option.BlockedCodes = [magic1, magic2];
4040
});
41+
42+
// For stateless approach (recommended)
43+
services.AddStatelessCaptcha(options =>
44+
{
45+
options.Letters = "2346789ABCDGHKMNPRUVWXYZ";
46+
options.CodeLength = 4;
47+
options.TokenExpiration = TimeSpan.FromMinutes(5);
48+
});
4149
}
4250

4351
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
@model StatelessHomeModel
2+
@{
3+
ViewData["Title"] = "Stateless Captcha Example";
4+
}
5+
6+
<div class="text-center">
7+
<h1 class="display-4">Stateless Captcha Example</h1>
8+
<p>This example shows how to use stateless captcha that works in clustered environments.</p>
9+
</div>
10+
11+
<div class="row">
12+
<div class="col-md-6 offset-md-3">
13+
<div class="card">
14+
<div class="card-header">
15+
<h5>Stateless Captcha Form</h5>
16+
</div>
17+
<div class="card-body">
18+
<form asp-action="Index" method="post" id="stateless-form">
19+
<div class="form-group mb-3">
20+
<label>Captcha Image:</label>
21+
<div class="d-flex align-items-center">
22+
<img id="captcha-image" src="" alt="Captcha" class="me-2" style="border: 1px solid #ccc;" />
23+
<button type="button" class="btn btn-sm btn-outline-secondary" onclick="refreshCaptcha()">
24+
🔄 Refresh
25+
</button>
26+
</div>
27+
<small class="form-text text-muted">Click refresh to get a new captcha</small>
28+
</div>
29+
30+
<div class="form-group mb-3">
31+
<label asp-for="CaptchaCode">Enter Captcha Code:</label>
32+
<input asp-for="CaptchaCode" class="form-control" placeholder="Enter the code from image" autocomplete="off" />
33+
<span asp-validation-for="CaptchaCode" class="text-danger"></span>
34+
</div>
35+
36+
<input type="hidden" asp-for="CaptchaToken" id="captcha-token" />
37+
38+
<div class="form-group">
39+
<button type="submit" class="btn btn-primary">Submit</button>
40+
<a asp-controller="Home" asp-action="Index" class="btn btn-secondary">Session-based Example</a>
41+
</div>
42+
</form>
43+
44+
<div class="mt-4">
45+
<h6>Advantages of Stateless Captcha:</h6>
46+
<ul class="small">
47+
<li>✅ Works in clustered/load-balanced environments</li>
48+
<li>✅ No server-side session storage required</li>
49+
<li>✅ Built-in expiration through encryption</li>
50+
<li>✅ Secure token-based validation</li>
51+
<li>✅ Better scalability</li>
52+
<li>✅ Single API call for both token and image</li>
53+
</ul>
54+
</div>
55+
</div>
56+
</div>
57+
</div>
58+
</div>
59+
60+
<script>
61+
async function refreshCaptcha() {
62+
try {
63+
const response = await fetch('/get-stateless-captcha');
64+
const data = await response.json();
65+
66+
// Set the token for validation
67+
document.getElementById('captcha-token').value = data.token;
68+
69+
// Set the image source using base64 data
70+
document.getElementById('captcha-image').src = `data:image/png;base64,${data.imageBase64}`;
71+
72+
// Clear the input
73+
document.getElementById('CaptchaCode').value = '';
74+
} catch (error) {
75+
console.error('Error refreshing captcha:', error);
76+
alert('Failed to load captcha. Please try again.');
77+
}
78+
}
79+
80+
// Initialize captcha on page load
81+
document.addEventListener('DOMContentLoaded', function() {
82+
refreshCaptcha();
83+
});
84+
</script>
85+
86+
@section Scripts {
87+
@{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
88+
}

src/Edi.Captcha.Tests/Edi.Captcha.Tests.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
<PackageReference Include="NUnit" Version="4.4.0" />
88
<PackageReference Include="Moq" Version="4.20.72" />
99
<PackageReference Include="NUnit3TestAdapter" Version="5.1.0" />
10-
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
10+
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.0" />
1111
</ItemGroup>
1212
<ItemGroup>
1313
<ProjectReference Include="..\Edi.Captcha\Edi.Captcha.csproj" />

0 commit comments

Comments
 (0)