.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; }
}
}
留待后查,同时方便他人
联系我:ivesbao@163.com
联系我:ivesbao@163.com

浙公网安备 33010602011771号