惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Engineering at Meta
Engineering at Meta
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
腾讯CDC
宝玉的分享
宝玉的分享
量子位
Recent Announcements
Recent Announcements
Martin Fowler
Martin Fowler
J
Java Code Geeks
V
Visual Studio Blog
阮一峰的网络日志
阮一峰的网络日志
Blog — PlanetScale
Blog — PlanetScale
大猫的无限游戏
大猫的无限游戏
博客园 - 叶小钗
S
SegmentFault 最新的问题
B
Blog
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
博客园 - 【当耐特】
小众软件
小众软件
The Cloudflare Blog
Y
Y Combinator Blog
I
InfoQ
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
GbyAI
GbyAI
IT之家
IT之家

博客园 - Hey,Coder!

微信小程序 web方案实现调音器功能 systemctl slice配置docker最大占用cpu 内存 pipenv环境变量配置 数据库差异对比工具 docker部署prometheus grafana pgexporter Alertmanager ubuntu24 换源 ssh配置 ubuntu24 Harbor部署 ubuntu24 kvm部署 cockpit web管理界面 .net api代理 Ubuntu 24.04 DMAR IOMMU 报错修复 uniapp iOS App 上架 vs文件软链接 Docker 网络别名 .net8通用中间件处理公共返回值结构 .net环境下OpenTelemetry Body 读取注意事项 jaeger界面显示异常 .net opentelemetry collector jaeger c# opentelemetry 自定义export c# opentelemetry 自定义export c# 生成对象的初始化代码 Elasticsearch ILM 与 Data Stream 概念及实例说明文档 portainer httphelper封装 Ocelot + Consul + SignalR 粘性会话 正交软件架构 docker postgresql17 主从复制 rabbitmq总结与c#示例 docker rabbitmq quartz dashboard docker consul registrator 服务自动注册consul 微服务动态扩容
.net8验证码
Hey,Coder! · 2026-09-09 · via 博客园 - Hey,Coder!

nuget

	  <PackageReference Include="SkiaSharp" Version="4.152.0" />

docker、linux中运行需要
	  <PackageReference Include="SkiaSharp.NativeAssets.Linux.NoDependencies" Version="4.152.0" />

字体文件

在docker环境中无法加载默认的字体,建议复制一个字体文件随着代码一起打包,然后修改为加载指定的字体

       var fontPath = Path.Combine(AppContext.BaseDirectory, "Fonts", "arial.ttf");
       using var typeface = SKTypeface.FromFile(fontPath);
       using var font = new SKFont(typeface, fontSize);

代码

 /// <summary>
 /// 验证码服务 - 使用 SkiaSharp 生成图片验证码
 /// 跨平台支持,避免 System.Drawing 的 GDI+ 依赖
 /// </summary>
 public class CaptchaService : ICaptchaService
 {
     // 内存存储验证码(key: captchaId, value: CaptchaEntry)
     private static readonly ConcurrentDictionary<string, CaptchaEntry> CaptchaStore = new();

     // 验证码有效期 5 分钟
     private const int ExpireMinutes = 5;

     public CaptchaResult Generate(int length = 4)
     {
         // 生成随机验证码
         var chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; // 去掉易混淆字符
         var random = new Random();
         var code = new string(Enumerable.Range(0, length).Select(_ => chars[random.Next(chars.Length)]).ToArray());
         var captchaId = Guid.NewGuid().ToString("N");
         var expireTime = DateTime.Now.AddMinutes(ExpireMinutes);

         var imageBase64 = GenerateImage(code);

         CaptchaStore[captchaId] = new CaptchaEntry
         {
             Code = code,
             ExpireAt = expireTime
         };

         // 清理过期验证码
         CleanupExpired();

         return new CaptchaResult
         {
             CaptchaId = captchaId,
             ImageBase64 = imageBase64,
             ExpireAt = expireTime
         };
     }

     public bool Validate(string captchaId, string captchaCode)
     {
         if (string.IsNullOrEmpty(captchaId) || string.IsNullOrEmpty(captchaCode))
             return false;

         if (!CaptchaStore.TryRemove(captchaId, out var entry))
             return false;

         if (entry.ExpireAt < DateTime.Now)
             return false;

         return string.Equals(entry.Code, captchaCode, StringComparison.OrdinalIgnoreCase);
     }

     public void Remove(string captchaId)
     {
         CaptchaStore.TryRemove(captchaId, out _);
     }

     private static string GenerateImage(string code)
     {
         const int width = 160;
         const int height = 50;
         const int padding = 10;
         const float fontSize = 20f;

         using var bitmap = new SKBitmap(width, height);
         using var canvas = new SKCanvas(bitmap);
         var random = new Random();

         // 浅灰背景
         canvas.DrawColor(new SKColor(245, 245, 245));

         // 干扰线
         using var linePaint = new SKPaint { StrokeWidth = 1, IsAntialias = true };
         for (int i = 0; i < 5; i++)
         {
             linePaint.Color = new SKColor((byte)random.Next(180, 220), (byte)random.Next(180, 220), (byte)random.Next(180, 220));
             canvas.DrawLine(random.Next(0, width), random.Next(0, height), random.Next(0, width), random.Next(0, height), linePaint);
         }

         // 干扰点
         using var dotPaint = new SKPaint();
         for (int i = 0; i < 30; i++)
         {
             dotPaint.Color = new SKColor((byte)random.Next(200, 230), (byte)random.Next(200, 230), (byte)random.Next(200, 230));
             canvas.DrawCircle(random.Next(0, width), random.Next(0, height), 1, dotPaint);
         }

         // 字体和画笔
         using var textPaint = new SKPaint { IsAntialias = true };

         var fontPath = Path.Combine(AppContext.BaseDirectory, "Fonts", "arial.ttf");
         using var typeface = SKTypeface.FromFile(fontPath);
         using var font = new SKFont(typeface, fontSize);
         //SKFont font = new SKFont(SKFontManager.Default.CreateTypeface(File.Open("Fonts//ARIAL.TTF", FileMode.Open)));
         //var font = new SKFont(SKTypeface.Default, fontSize);

         // 每个字符的水平位置
         float slotWidth = (float)(width - padding * 2) / code.Length;
         float baselineY = height * 0.72f; // 基线位置,72% 高度处

         for (int i = 0; i < code.Length; i++)
         {
             textPaint.Color = new SKColor((byte)random.Next(30, 90), (byte)random.Next(30, 90), (byte)random.Next(30, 90));

             // 字符中心 X 坐标
             float x = padding + slotWidth * i + slotWidth / 2f;
             // 每个字符轻微上下偏移
             float yOffset = random.Next(-3, 4);
             float y = baselineY + yOffset;
             // ±15° 范围内的小角度旋转
             float angle = random.Next(-15, 16);

             canvas.Save();
             canvas.Translate(x, y);
             canvas.RotateDegrees(angle);

             // 测量文字宽度用于水平居中
             var textWidth = font.MeasureText(code[i].ToString(), textPaint);
             canvas.DrawText(code[i].ToString(), -textWidth / 2f, 0, font, textPaint);

             canvas.Restore();
         }

         // 编码为 PNG,data URI 格式返回
         using var image = SKImage.FromBitmap(bitmap);
         using var data = image.Encode(SKEncodedImageFormat.Png, 100);
         using var stream = new MemoryStream();
         data.SaveTo(stream);

         return "data:image/png;base64," + Convert.ToBase64String(stream.ToArray());
     }

     private static void CleanupExpired()
     {
         var expiredKeys = CaptchaStore
             .Where(p => p.Value.ExpireAt < DateTime.Now)
             .Select(p => p.Key)
             .ToList();

         foreach (var key in expiredKeys)
         {
             CaptchaStore.TryRemove(key, out _);
         }
     }

     private class CaptchaEntry
     {
         public string Code { get; set; }
         public DateTime ExpireAt { get; set; }
     }
 }