不能用system.drawing生成跨平台验证码,因其依赖gdi+,在linux/macos容器中会抛system.typeinitializationexception;推荐用masuit.tools或imagesharp替代,需剔除易混淆字符、设正确缓存头与content-type,并确保字体可用。

别用 System.Drawing 直接绘图生成验证码——除非你 100% 确认只跑在 Windows Server 且不进 Docker。它在 Linux/macOS 容器里会直接抛 System.TypeInitializationException: The type initializer for 'Gdip' threw an exception.,不是配置问题,是底层 GDI+ 根本不存在。
用 Masuit.Tools 快速生成带抗 OCR 干扰的 PNG 流
这是目前最轻量、跨平台、免编译、无内存泄漏风险的起点。它自动探测可用字体、复用内存池、启用干扰线+干扰点+渐变文字,且不依赖 SkiaSharp 的 native assets。
-
ValidateCode.CreateValidateCode(6)生成 6 位字符串(别用 4 位,暴力穷举太容易) - 字符集已默认剔除易混淆字符(
0/O/l/I),不用自己筛 - 调用
code.CreateValidateGraphic(28)时传 28 是经验值:字号太小(如 16)干扰点易覆盖文字;太大(如 40)留白少、干扰线密度下降 - 务必用
PooledMemoryStream接收输出流,别用new MemoryStream()—— 高并发下Bitmap+Graphics的 Dispose 漏掉一个就可能 OOM - 返回图片时设
Content-Type: image/png,不是image/jpeg:PNG 支持透明通道,干扰边缘更自然;JPEG 压缩反而强化字符轮廓,帮 OCR 提特征
用 ImageSharp 替代 System.Drawing 的实操要点
如果你不想引入 Masuit.Tools,ImageSharp 是第二推荐方案:纯托管、跨平台、无 native 依赖,但需手动补全抗 OCR 逻辑。
- 通过
dotnet add package SixLabors.ImageSharp引入,v2+ 支持 .NET 6+ - 固定输出尺寸如
200x80,避免用奇数宽高(如199x79)——某些 CDN 或代理对非整除尺寸做压缩时会模糊文字 - 干扰线控制在 2–3 条,用
DrawLines+ 半透明色(Color.FromArgb(60, 100, 100, 100)),太多反而降低可读性 - 字符绘制必须加轻微随机旋转(±6° 内)和 X/Y 方向微偏移(±3px),否则 OCR 模板匹配一打一个准
- 别用默认字体名(如
"Arial"):Linux Alpine 容器里根本没这个字体,图片会空白或报错Font 'Arial' not found;改用SystemFonts.Find("DejaVu Sans") ?? SystemFonts.Default
Session 存取验证码值总为空?顺序和判空是关键
不是代码写错了,是中间件注册顺序或取值时机不对。ASP.NET Core 中 Session 默认不启用,且生命周期极短。
- 在
Program.cs中必须显式添加:builder.Services.AddSession(options => options.IdleTimeout = TimeSpan.FromMinutes(5)); -
app.UseSession()必须放在app.UseRouting()之后、app.UseEndpoints()之前——放错位置等于没开 - 存值用:
HttpContext.Session.SetString("captcha_code", code.ToUpperInvariant().Trim()) - 取值前必须判空:
var stored = HttpContext.Session.GetString("captcha_code"); if (stored == null) return BadRequest("验证码已过期"); - 校验时统一处理用户输入:
input.Trim().Replace(" ", "").ToUpperInvariant(),防止全角空格、粘贴带换行、大小写混输导致“明明输对了却报错”
前端刷新验证码总显示旧图?缓存策略比后端逻辑更优先
这不是后端没生成新图,是浏览器或 CDN 把上次响应缓存住了。解决它不靠重写后端,靠控制 HTTP 头和 URL 参数。
- 后端返回图片时加响应头:
context.Response.Headers.Append("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0"); - 前端刷新逻辑别只改
img.src,要加时间戳参数:img.src = '/captcha?' + Date.now(); - 禁用
location.reload()模拟刷新——它重载整个页面,体验差,且无法绕过 img 标签的本地缓存 - 如果用了 Nginx 或 CDN,确认它们没对
/captcha路径做缓存配置(比如proxy_cache或cache-control: public)
最常被忽略的是字符集清理和缓存控制:易混淆字符(0/O/l/I)不剔除,OCR 工具用基础模板就能扫出 80%;HTTP 缓存头不设或设错,前端再怎么点刷新按钮也没用——图根本没重新请求。











