在.NET Core 3.0+或.NET 5+中,System.Drawing因跨平台限制和GDI+资源泄漏易出错;应改用SkiaSharp实现验证码生成,并通过Session绑定随机token、禁用缓存、统一大小写/全半角处理确保校验可靠。

为什么直接用 System.Drawing 生成验证码容易出错
在 .NET Core 3.0+ 或 .NET 5+ 环境下,System.Drawing 默认不可用(尤其在 Linux 容器中会抛 System.TypeInitializationException),即使 Windows 上能跑,也因 GDI+ 资源未释放导致内存泄漏。很多“可用源码”直接照搬旧版 WinForms 写法,上线后 CPU 持续飙高或图片返回空白。
真正可行的路径只有两条:降级回 .NET Framework(不推荐),或改用跨平台图像库。目前最稳妥的是 SkiaSharp —— 它不依赖系统图形子系统,API 清晰,且噪点、扭曲、干扰线都能可控实现。
用 SkiaSharp 生成带噪点+字符扭曲的验证码图
核心思路是:先画背景 → 逐字符绘制(加随机旋转/偏移)→ 叠加噪点 → 加干扰线 → 输出 PNG 流。关键不是“画得像不像”,而是“机器难 OCR、人眼易识别”。
- 安装包:
SkiaSharp和SkiaSharp.NativeAssets.Linux(如需 Linux 部署) - 字符集避免
0/O/l/1/I等易混淆字母数字,建议用"23456789ABCDEFGHJKLMNPQRSTUVWXYZ" - 噪点用随机小矩形(非单像素点),尺寸设为
1x1到2x2,数量控制在 100–300 个,太多反而降低可读性 - 每个字符旋转角度建议 ±15° 内,X/Y 偏移 ≤ 3px,过度扭曲会触发部分 OCR 引擎的容错机制
public static byte[] GenerateCaptcha(int width = 120, int height = 40)
{
using var surface = SKSurface.Create(new SKImageInfo(width, height));
using var canvas = surface.Canvas;
canvas.Clear(SKColors.White);
<pre class="brush:php;toolbar:false;">var paint = new SKPaint { Color = SKColors.Black, IsAntialias = true };
var random = new Random();
string code = new string(Enumerable.Repeat("23456789ABCDEFGHJKLMNPQRSTUVWXYZ", 4)
.Select(s => s[random.Next(s.Length)]).ToArray());
// 绘制字符(带偏移和旋转)
float x = 20;
foreach (char c in code)
{
float angle = (random.NextFloat() - 0.5f) * 30; // -15° ~ +15°
canvas.Save();
canvas.RotateDegrees(angle, x + 6, height / 2);
canvas.DrawText(c.ToString(), x, height * 0.7f, paint);
canvas.Restore();
x += 22 + random.Next(-3, 4);
}
// 添加噪点
var noisePaint = new SKPaint { Color = SKColors.Gray };
for (int i = 0; i < 200; i++)
{
int sx = random.Next(width);
int sy = random.Next(height);
canvas.DrawRect(new SKRect(sx, sy, sx + random.Next(1, 3), sy + random.Next(1, 3)), noisePaint);
}
// 干扰线(2条浅灰斜线)
var linePaint = new SKPaint { Color = SKColors.LightGray, StrokeWidth = 1 };
canvas.DrawLine(0, random.Next(5, 15), width, random.Next(height - 15, height - 5), linePaint);
canvas.DrawLine(0, random.Next(height - 15, height - 5), width, random.Next(5, 15), linePaint);
using var image = surface.Snapshot();
using var data = image.Encode(SKEncodedImageFormat.Png, 100);
return data.ToArray();}
如何在 ASP.NET Core 中安全返回验证码并绑定 Session
验证码必须和服务端状态强绑定,否则攻击者可重复使用同一张图。关键不是“存到 Redis”,而是“生成时立刻写入短期凭证”。常见错误是把验证码明文塞进 Session,结果被中间人嗅探;或用 HttpContext.Session.SetString 却没启用 Session 中间件。
- 务必在
Program.cs中调用builder.Services.AddSession()和app.UseSession() - 生成验证码后,用
RandomNumberGenerator生成 32 字节随机 token 作为 key,验证码字符串作为 value,存入 Session(过期时间设为 5 分钟) - 接口返回时,HTTP 头禁用缓存:
Response.Headers.Append("Cache-Control", "no-store, no-cache") - 前端请求验证码图片时,URL 后拼上时间戳(如
?t=1712345678)防 IE 缓存
为什么验证码校验失败率高?重点检查这三点
用户输入正确却提示“验证码错误”,大概率不是算法问题,而是前后端对值的处理不一致。
- 前端提交前是否 trim() 了空格?是否把全角数字/字母转成了半角?(比如用户用中文输入法打了
1234) - 服务端 Session 是否跨请求丢失?检查是否在控制器里用了
[SessionState(SessionStateBehavior.Disabled)]或异步方法中误用了async void - 验证码字符串生成后是否被意外修改?比如用
ToUpper()处理但前端没转大写,或 Session 中存的是小写而校验时比对了大写
噪点和干扰线只是增加 OCR 成本,真正的防线在 token 绑定、传输防篡改和大小写/全半角归一化处理上。这些细节漏掉一个,再复杂的图形算法都白搭。


















