.net8验证码

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; }
     }
 }
posted @ 2026-09-09 15:14  Hey,Coder!  阅读(5)  评论(0)    收藏  举报