引言
- 随着安全需求的不断提升,传统的文本验证码已经无法满足防止机器自动识别和攻击的要求。点选式验证码作为一种交互式的验证手段,因其更难被自动化脚本破解而逐渐受到欢迎。利用开源图像处理库
SixLabors.ImageSharp
来实现点选式验证码功能。
ImageSharp简介
- ImageSharp介绍
SixLabors.ImageSharp
是一个跨平台、无依赖的.NET标准图像处理库,支持多种格式的读写和图像操作,其高性能和丰富的API使得它成为.NET开发者进行图像处理的理想工具。与传统的System.Drawing库相比,ImageSharp具有更高的性能和更灵活的扩展性。
点选验证码原理
- 点选验证码通常要求
用户从一组图片中选择出符合特定条件的图片
,如选择包含特定文字或图形
的图片。这种验证方式能够有效地阻止自动化脚本的攻击,因为自动化脚本很难模拟人类的视觉和点击操作。
使用ImageSharp点选验证码实现
- 随机生成点选验证文字
private readonly static Random _random = new();
/// <summary>
/// 生成随机汉字
/// </summary>
/// <param name="number"></param>
/// <returns></returns>
private static string GetRandomCode(int number)
{
var str = "天地玄黄宇宙洪荒日月盈昃辰宿列张寒来暑往秋收冬藏闰余成岁律吕调阳云腾致雨露结为霜金生丽水玉出昆冈剑号巨阙珠称夜光果珍李柰菜重芥姜海咸河淡鳞潜羽翔龙师火帝鸟官人皇始制文字乃服衣裳推位让国有虞陶唐吊民伐罪周发殷汤坐朝问道垂拱平章爱育黎首臣伏戎羌遐迩体率宾归王";
char[] str_char_arrary = str.ToArray();
HashSet<string> hs = new HashSet<string>();
bool randomBool = true;
while (randomBool)
{
if (hs.Count == number)
break;
int rand_number = _random.Next(str_char_arrary.Length);
hs.Add(str_char_arrary[rand_number].ToString());
}
string code = string.Join("", hs);
return code;
}
- 随机生成点选文字坐标
/// <summary>
/// 获取生成汉子位置随机数
/// </summary>
/// <param name="min"></param>
/// <param name="max"></param>
/// <returns></returns>
private static int GetRandom(int min, int max)
{
return _random.Next(min, max);
}
/// <summary>
/// 生成坐标
/// </summary>
/// <returns></returns>
private IList<PointModel> GeneratePoint(int originalWidth, int originalHeight,int codeCount)
{
List<PointModel> points = new List<PointModel>();
int paddingNum = 20;
var sp = (originalWidth - paddingNum * 2) / codeCount;
for (var i = 0; i < codeCount; i++)
{
var x = GetRandom(i * sp + paddingNum, (i + 1) * sp - paddingNum);
var y = GetRandom(paddingNum, originalHeight - paddingNum*2); //留点边距
points.Add(new PointModel(x, y));
}
return points;
}
//坐标实体类
public class PointModel
{
/// <summary>
/// x坐标
/// </summary>
public int X { get; set; }
/// <summary>
/// y坐标
/// </summary>
public int Y { get; set; }
public PointModel(int x, int y)
{
X = x;
Y = y;
}
}
- 生成点选验证码返回给前端
/// <summary>
/// 转换为相对于图片的百分比单位
/// </summary>
/// <param name="widthAndHeight">图片宽高</param>
/// <param name="xAndy">相对于图片的绝对尺寸</param>
/// <returns>(int:xPercent, int:yPercent)</returns>
private (int, int) ToPercentPos((int, int) widthAndHeight, (int, int) xAndy)
{
(int, int) rtnResult = (0, 0);
// 注意: int / int = int (小数部分会被截断)
rtnResult.Item1 = (int)(((double)xAndy.Item1) / ((double)widthAndHeight.Item1) * 100);
rtnResult.Item2 = (int)(((double)xAndy.Item2) / ((double)widthAndHeight.Item2) * 100);
return rtnResult;
}
/// <summary>
/// 生成验证数据
/// </summary>
/// <returns>object</returns>
public async Task<object> GetCaptchaAsync(string captchaKey)
{
// //获取网络图片
// //var client = new HttpClient();
// //var stream = await client.GetStreamAsync(");
// //client.Dispose();
// //Bitmap baseImage = new Bitmap(stream);
// //stream.Dispose();
// TODO: 设置答案: 4个字
int answerLength = 4;
string imagesDir = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "captcha", "clickword");
string[] imagesFiles = Directory.GetFiles(imagesDir.ToPath());
int imgIndex = _random.Next(imagesFiles.Length);
string randomImgFile = imagesFiles[imgIndex];
using var baseImage = await Image.LoadAsync<Rgba32>(randomImgFile.ToPath());
//重置底图尺寸
baseImage.Mutate(x =>
{
x.Resize(310, 155);
});
//字体
string fontsDir = Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "captcha", "fonts");
string[] fontFiles = new DirectoryInfo(fontsDir.ToPath())?.GetFiles()
?.Where(m => m.Extension.ToLower() == ".ttf")
?.Select(m => m.FullName).ToArray();
int baseWidth = baseImage.Width;
int baseHeight = baseImage.Height;
//设置字体
var fontSize = 32;
List<PointModel> data = new List<PointModel>();
List<int> pointFontList = new List<int>();
//设置颜色
Color[] colorArray = { Color.Black, Color.Red, Color.Blue, Color.Green, Color.Orange, Color.Brown, Color.DarkBlue };
List<char> fontlist = GetRandomCode(6).ToList();
//随机生成拼图坐标
IList<PointModel> points = GeneratePoint(baseWidth, baseHeight, fontlist.Count);
//实际答案
List<string> wordList = new List<string>();
for (int i = 0; i < fontlist.Count; i++)
{
FontFamily[] families = SystemFonts.Families.ToArray();
FontFamily fontFamily=new FontFamily();
if (fontFiles.Length > 0)
{
//随机字体
var randomFont = fontFiles[_random.Next(fontFiles.Length)];
FontCollection fonts = new FontCollection();
fontFamily = fonts.Add(randomFont);
}
else
{
if (families.Length > 0)
{
// 随机选择一个索引
int randomIndex = _random.Next(families.Length);
fontFamily = families[randomIndex];
}
}
var font = new Font(fontFamily, fontSize, FontStyle.Bold);
bool isFinish = false;
int restCount = 0;
while (!isFinish)
{
restCount++;
if (restCount >= 100 || pointFontList.Count>=6)
isFinish = true;
int fontIndex = _random.Next(points.ToArray().Length);
if (!pointFontList.Contains(fontIndex))
{
pointFontList.Add(fontIndex);
if (data.Count < answerLength)
{
(int, int) percentPos = ToPercentPos((baseWidth, baseHeight), (points[fontIndex].X, points[fontIndex].Y));
// 添加正确答案 位置数据
data.Add(new PointModel(percentPos.Item1, percentPos.Item2));
wordList.Add(fontlist[i].ToString());
}
isFinish = true;
// 创建文本选项
using var fontImage = new Image<Rgba32>(fontSize, fontSize);
fontImage.Mutate(ctx => ctx
.DrawText(fontlist[i].ToString(), font, colorArray[_random.Next(colorArray.Length)], new PointF(0, 0))
.Rotate(_random.Next(-45, 45))
);
baseImage.Mutate(o =>
{
//o.DrawText(fontlist[i].ToString(), font, colorArray[random.Next(colorArray.Length)], new PointF(points[fontIndex].X, points[fontIndex].Y));
o.DrawImage(fontImage, new Point(points[fontIndex].X, points[fontIndex].Y),1);
});
}
}
}
var token = Guid.NewGuid().ToString();
var captchaData = new
{
Token = token,
Data = new
{
BaseImage = baseImage.ToBase64String(PngFormat.Instance),
PoinText = "请依次点击: " + string.Join(",", wordList)
}
};
var key = string.Format(captchaKey, token);
await _cache.SetAsync(key, data);
return captchaData;
}
- 服务器端验证点选验证码
/// <summary>
/// 检查验证数据
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
public async Task<bool> CheckAsync(CaptchaInput input)
{
if (input == null || input.Data.IsNull())
{
return false;
}
var key = string.Format(input.CaptchaKey, input.Token);
if (await _cache.ExistsAsync(key))
{
try
{
var point = JsonConvert.DeserializeObject<List<PointModel>>(input.Data);
var pointV = await _cache.GetAsync<List<PointModel>>(key);
for (int i = 0; i < pointV.Count; i++)
{
if (Math.Abs(pointV[i].X - point[i].X) > 10 || Math.Abs(pointV[i].Y - point[i].Y) > 10)
{
await _cache.DelAsync(key);
return false;
}
}
if (input.DeleteCache)
{
await _cache.DelAsync(key);
}
return true;
}
catch
{
await _cache.DelAsync(key);
return false;
}
}
else
{
return false;
}
}
最终效果图:
参考
- https://github.com/SixLabors/ImageSharp
- https://docs.sixlabors.com/
公众号“点滴分享技术猿”