using System.Security.Cryptography;
using Microsoft.Extensions.Caching.Memory;
namespace Rmsys.Api.Endpoints;
///
/// 图形验证码:服务端生成 SVG(不依赖 System.Drawing,Linux/Windows 都能跑),
/// 验证码文本存内存缓存,3 分钟过期、校验一次即作废(防重放)。
///
public static class CaptchaEndpoints
{
private const string Chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; // 去掉 0/O/1/I/L 易混字符
private const int Length = 4;
private const int TtlSeconds = 180;
public static void MapCaptchaEndpoints(this WebApplication app)
{
app.MapGet("/api/auth/captcha", (IMemoryCache cache) =>
{
var code = NewCode();
var id = Guid.NewGuid().ToString("N");
cache.Set("captcha:" + id, code, TimeSpan.FromSeconds(TtlSeconds));
var svg = BuildSvg(code);
// 只有显式设置 RMSYS_CAPTCHA_DEBUG=1 才回传答案(仅供联调/自测)。
// 安装包部署时不设置该变量,生产环境永远不会泄露答案。
var debug = Environment.GetEnvironmentVariable("RMSYS_CAPTCHA_DEBUG") == "1";
if (debug)
return Results.Ok(new { ok = true, id, svg, code, ttl = TtlSeconds });
return Results.Ok(new { ok = true, id, svg, ttl = TtlSeconds });
});
/// 登录时校验:命中即删除(一次一用)
app.MapPost("/api/auth/captcha/check", (CaptchaCheckReq req, IMemoryCache cache) =>
{
var ok = Consume(cache, req.id, req.code);
return Results.Ok(new { ok });
});
}
/// 校验并从缓存移除;失败也移除(避免被反复猜)
public static bool Consume(IMemoryCache cache, string? id, string? input)
{
if (string.IsNullOrWhiteSpace(id) || string.IsNullOrWhiteSpace(input)) return false;
var key = "captcha:" + id;
if (!cache.TryGetValue(key, out var code) || string.IsNullOrEmpty(code)) return false;
cache.Remove(key);
return string.Equals(code, input.Trim(), StringComparison.OrdinalIgnoreCase);
}
private static string NewCode()
{
var buf = new char[Length];
for (var i = 0; i < Length; i++) buf[i] = Chars[RandomNumberGenerator.GetInt32(Chars.Length)];
return new string(buf);
}
private static string BuildSvg(string code)
{
var rnd = RandomNumberGenerator.Create();
int R(int min, int max) { var b = new byte[4]; rnd.GetBytes(b); return min + (int)(BitConverter.ToUInt32(b, 0) % (uint)(max - min)); }
double D(double min, double max) => min + (rnd.NextDoubleLike() * (max - min));
const int w = 132, h = 44;
var sb = new System.Text.StringBuilder();
sb.Append($"");
return sb.ToString();
}
private static double NextDoubleLike(this RandomNumberGenerator r)
{
var b = new byte[4];
r.GetBytes(b);
return BitConverter.ToUInt32(b, 0) / (double)uint.MaxValue;
}
public record CaptchaCheckReq(string? id, string? code);
}