.NetCore登入功能生成验证码
生成验证码
每一个.NetCore Web项目必不可少的就是登入注册功能,通常登入界面都会有一张验证码图片,只有根据图片输入验证码才能登入成功。验证码的主要功能就是验证操作者是人而非机器人,目的就是防止机器人暴力破解。
1、随机验证码
生成验证码,首先验证码的内容肯定是随机的。每次生成的内容都不相同。
.NetCore随机生成验证码代码示例如下:
string Letters = "1,2,3,4,5,6,7,8,9,0,A,B,C,D,E,F,G,H,I,H,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z,a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y,z";
var array = Letters.Split(",");
var random = new Random();
string code= "";
for(int i = 0; i < 4; i++)
{
code+= array[random.Next(array.Length)];
}
以上代码是生成字母与数字随机组合的4位验证码,使用的就是Random随机数。
2、验证码画图
获取到随机生成的验证码内容后,就需要根据验证码内容生成图片。而.NetCore数据画图有三种方式。
System.Drawing.Common、SkiaSharp以及ImageSharp
2.1、System.Drawing.Common
使用System.Drawing.Common数据画图代码如下
public byte[] GetCodeImage(string captchaCode)
{
//验证码颜色集合
Color[] c = {Color.MediumAquamarine,Color.MediumBlue,Color.MediumOrchid,Color.MediumPurple,Color.MediumSeaGreen,Color.MediumSlateBlue,
Color.MediumSpringGreen,Color.Maroon,Color.MediumTurquoise,Color.MidnightBlue,Color.MintCream,Color.MistyRose,
Color.Moccasin,Color.NavajoWhite,Color.Navy,Color.OldLace,Color.MediumVioletRed,Color.Magenta,Color.Linen,Color.LimeGreen,
Color.LavenderBlush,Color.LawnGreen,Color.LemonChiffon,Color.LightBlue,Color.LightCoral,Color.LightGoldenrodYellow,
Color.LightGreen,Color.LightPink,Color.LightSalmon,Color.LightSeaGreen,Color.LightSkyBlue,Color.LightSteelBlue,Color.LightYellow,
Color.Lime,Color.Olive,Color.OliveDrab,Color.Orange,Color.OrangeRed,Color.Silver,Color.SkyBlue,Color.SlateBlue,
Color.Snow,Color.SpringGreen,Color.SteelBlue,Color.Tan,Color.Teal,Color.Thistle,Color.Tomato,Color.Transparent,
Color.Turquoise,Color.Violet,Color.Wheat,Color.White,Color.WhiteSmoke,Color.Sienna,Color.Lavender,Color.SeaShell,
Color.SandyBrown,Color.Orchid,Color.PaleGoldenrod,Color.PaleGreen,Color.PaleTurquoise,Color.PaleVioletRed,Color.PapayaWhip,
Color.PeachPuff,Color.Peru,Color.Pink,Color.Plum,Color.PowderBlue,Color.Purple,Color.Red,Color.RosyBrown,Color.RoyalBlue,Color.SaddleBrown,
Color.Salmon,Color.SeaGreen,Color.Yellow,Color.Khaki,Color.Cyan,Color.DarkMagenta,Color.DarkKhaki,Color.DarkGreen,Color.DarkGoldenrod,
Color.DarkCyan,Color.DarkBlue,Color.Ivory,Color.Crimson,Color.Cornsilk,Color.CornflowerBlue,Color.Coral,Color.Chocolate,
Color.DarkOliveGreen,Color.Chartreuse,Color.BurlyWood,Color.Brown,Color.BlueViolet,Color.Blue,Color.BlanchedAlmond,
Color.Black,Color.Bisque,Color.Beige,Color.Azure,Color.Aquamarine,Color.Aqua,Color.AntiqueWhite,Color.AliceBlue,Color.CadetBlue,
Color.DarkOrange,Color.YellowGreen,Color.DarkRed,Color.Indigo,Color.IndianRed,Color.DarkOrchid,Color.Honeydew,Color.GreenYellow,
Color.Green,Color.Goldenrod,Color.Gold,Color.GhostWhite,Color.Gainsboro,Color.Fuchsia,Color.ForestGreen,Color.HotPink,Color.Firebrick,
Color.FloralWhite,Color.DodgerBlue,Color.DeepSkyBlue,Color.DeepPink,Color.DarkViolet,Color.DarkTurquoise,Color.DarkSlateBlue,
Color.DarkSeaGreen,Color.DarkSalmon};
//验证码字体集合
string[] fonts = { "Verdana", "Microsoft Sans Serif", "Comic Sans MS", "Arial" };
//定义图像的大小,生成图像的实例
var image = new Bitmap(140, 50);
// 画图
var g = Graphics.FromImage(image);
// 背景色
g.Clear(Color.LightGray);
// 随机
var random = new Random();
// 干扰
for (var i = 0; i < 6; i++)
{
// 干扰点
//var x = random.Next(image.Width);
//var y = random.Next(image.Height);
//g.DrawRectangle(new Pen(Color.LightGray, 0), x, y, 1, 1);
// 干扰线
g.DrawLine(new Pen(c[random.Next(c.Length)], 4), new Point(random.Next(140), random.Next(50)), new Point(random.Next(140), random.Next(50)));
}
//验证码绘制在g中
for (var i = 0; i < captchaCode.Length; i++)
{
//随机颜色索引值
var cindex = random.Next(c.Length);
//随机字体索引值
var findex = random.Next(fonts.Length);
//字体
var f = new Font(fonts[findex], 30, FontStyle.Italic);
//颜色
Brush b = new SolidBrush(c[cindex]);
//绘制一个验证字符
g.DrawString(captchaCode.Substring(i, 1), f, b, i * 32 + 1, 1);
}
var ms = new MemoryStream();
image.Save(ms, ImageFormat.Png);
g.Dispose();
image.Dispose();
return ms.ToArray();
}
以上代码中captchaCode为随机验证码变量。返回的结果为图片Byte数组。
2.2、SkiaSharp
使用SkiaSharp数据画图代码如下:
public byte[] GetCodeImage(string captchaText, int width = 140, int height = 50, int lineNum = 4, int lineStrookeWidth = 2)
{
// 颜色集合
List<SKColor> colors = new List<SKColor>();
colors.Add(SKColors.AliceBlue);
colors.Add(SKColors.PaleGreen);
colors.Add(SKColors.PaleGoldenrod);
colors.Add(SKColors.Orchid);
colors.Add(SKColors.OrangeRed);
colors.Add(SKColors.Orange);
colors.Add(SKColors.OliveDrab);
colors.Add(SKColors.Olive);
colors.Add(SKColors.OldLace);
colors.Add(SKColors.Navy);
colors.Add(SKColors.NavajoWhite);
colors.Add(SKColors.Moccasin);
colors.Add(SKColors.MistyRose);
colors.Add(SKColors.MintCream);
colors.Add(SKColors.MidnightBlue);
colors.Add(SKColors.MediumVioletRed);
colors.Add(SKColors.MediumTurquoise);
colors.Add(SKColors.MediumSpringGreen);
colors.Add(SKColors.LightSteelBlue);
colors.Add(SKColors.LightYellow);
colors.Add(SKColors.Lime);
colors.Add(SKColors.LimeGreen);
colors.Add(SKColors.Linen);
colors.Add(SKColors.PaleTurquoise);
colors.Add(SKColors.Magenta);
colors.Add(SKColors.MediumAquamarine);
colors.Add(SKColors.MediumBlue);
colors.Add(SKColors.MediumOrchid);
colors.Add(SKColors.MediumPurple);
colors.Add(SKColors.MediumSeaGreen);
colors.Add(SKColors.MediumSlateBlue);
colors.Add(SKColors.Maroon);
colors.Add(SKColors.PaleVioletRed);
colors.Add(SKColors.PapayaWhip);
colors.Add(SKColors.PeachPuff);
colors.Add(SKColors.Snow);
colors.Add(SKColors.SpringGreen);
colors.Add(SKColors.SteelBlue);
colors.Add(SKColors.Tan);
colors.Add(SKColors.Teal);
colors.Add(SKColors.Thistle);
colors.Add(SKColors.Tomato);
colors.Add(SKColors.Violet);
colors.Add(SKColors.Wheat);
colors.Add(SKColors.White);
colors.Add(SKColors.WhiteSmoke);
colors.Add(SKColors.Yellow);
colors.Add(SKColors.YellowGreen);
colors.Add(SKColors.Turquoise);
colors.Add(SKColors.LightSkyBlue);
colors.Add(SKColors.SlateBlue);
colors.Add(SKColors.Silver);
colors.Add(SKColors.Peru);
colors.Add(SKColors.Pink);
colors.Add(SKColors.Plum);
colors.Add(SKColors.PowderBlue);
colors.Add(SKColors.Purple);
colors.Add(SKColors.Red);
colors.Add(SKColors.SkyBlue);
colors.Add(SKColors.RosyBrown);
colors.Add(SKColors.SaddleBrown);
colors.Add(SKColors.Salmon);
colors.Add(SKColors.SandyBrown);
colors.Add(SKColors.SeaGreen);
colors.Add(SKColors.SeaShell);
colors.Add(SKColors.Sienna);
colors.Add(SKColors.RoyalBlue);
colors.Add(SKColors.LightSeaGreen);
colors.Add(SKColors.LightSalmon);
colors.Add(SKColors.LightPink);
colors.Add(SKColors.Crimson);
colors.Add(SKColors.Cyan);
colors.Add(SKColors.DarkBlue);
colors.Add(SKColors.DarkCyan);
colors.Add(SKColors.DarkGoldenrod);
colors.Add(SKColors.DarkGray);
colors.Add(SKColors.Cornsilk);
colors.Add(SKColors.DarkGreen);
colors.Add(SKColors.DarkMagenta);
colors.Add(SKColors.DarkOliveGreen);
colors.Add(SKColors.DarkOrange);
colors.Add(SKColors.DarkOrchid);
colors.Add(SKColors.DarkRed);
colors.Add(SKColors.DarkSalmon);
colors.Add(SKColors.DarkKhaki);
colors.Add(SKColors.DarkSeaGreen);
colors.Add(SKColors.CornflowerBlue);
colors.Add(SKColors.Chocolate);
colors.Add(SKColors.AntiqueWhite);
colors.Add(SKColors.Aqua);
colors.Add(SKColors.Aquamarine);
colors.Add(SKColors.Azure);
colors.Add(SKColors.Beige);
colors.Add(SKColors.Bisque);
colors.Add(SKColors.Coral);
colors.Add(SKColors.Black);
colors.Add(SKColors.Blue);
colors.Add(SKColors.BlueViolet);
colors.Add(SKColors.Brown);
colors.Add(SKColors.BurlyWood);
colors.Add(SKColors.CadetBlue);
colors.Add(SKColors.Chartreuse);
colors.Add(SKColors.BlanchedAlmond);
colors.Add(SKColors.Transparent);
colors.Add(SKColors.DarkSlateBlue);
colors.Add(SKColors.DarkTurquoise);
colors.Add(SKColors.IndianRed);
colors.Add(SKColors.Indigo);
colors.Add(SKColors.Ivory);
colors.Add(SKColors.Khaki);
colors.Add(SKColors.Lavender);
colors.Add(SKColors.LavenderBlush);
colors.Add(SKColors.HotPink);
colors.Add(SKColors.LawnGreen);
colors.Add(SKColors.LightBlue);
colors.Add(SKColors.LightCoral);
colors.Add(SKColors.LightCyan);
colors.Add(SKColors.LightGoldenrodYellow);
colors.Add(SKColors.LightGreen);
colors.Add(SKColors.LemonChiffon);
colors.Add(SKColors.Honeydew);
colors.Add(SKColors.Green);
colors.Add(SKColors.DarkViolet);
colors.Add(SKColors.DeepPink);
colors.Add(SKColors.DeepSkyBlue);
colors.Add(SKColors.DimGray);
colors.Add(SKColors.DodgerBlue);
colors.Add(SKColors.Firebrick);
colors.Add(SKColors.GreenYellow);
colors.Add(SKColors.FloralWhite);
colors.Add(SKColors.Fuchsia);
colors.Add(SKColors.Gainsboro);
colors.Add(SKColors.GhostWhite);
colors.Add(SKColors.Gold);
colors.Add(SKColors.Goldenrod);
colors.Add(SKColors.ForestGreen);
//创建bitmap位图
using (SKBitmap image2d = new SKBitmap(width, height, SKColorType.Bgra8888, SKAlphaType.Premul))
{
//创建画笔
using (SKCanvas canvas = new SKCanvas(image2d))
{
//填充背景颜色
canvas.DrawColor(SKColors.LightGray);
var array = captchaText.ToArray();
Random random = new Random();
for(int i = 0; i < array.Length; i++)
{
//将文字写到画布上
using (SKPaint drawStyle = CreatePaint(random.Next(colors.Count), height))
{
canvas.DrawText(array[i].ToString(), i*32+1, height - 1, drawStyle);
}
}
//画随机干扰线
using (SKPaint drawStyle = new SKPaint())
{
for (int i = 0; i < lineNum; i++)
{
drawStyle.Color = colors[random.Next(colors.Count)];
drawStyle.StrokeWidth = lineStrookeWidth;
canvas.DrawLine(random.Next(0, width), random.Next(0, height), random.Next(0, width), random.Next(0, height), drawStyle);
}
}
//返回图片byte
using (SKImage img = SKImage.FromBitmap(image2d))
{
using (SKData p = img.Encode(SKEncodedImageFormat.Jpeg, 100))
{
return p.ToArray();
}
}
}
}
}
// 创建画笔
private SKPaint CreatePaint(int colorNo, float fontSize)
{
SKTypeface font = SKTypeface.FromFamilyName("微软雅黑", 16, 16, SKFontStyleSlant.Italic);
SKPaint paint = new SKPaint();
paint.IsAntialias = true;
paint.Color = colors[colorNo];
paint.Typeface = font;
paint.TextSize = fontSize;
return paint;
}
SkiaSharp代码与System.Drawing.Common逻辑上一致,所以代码很相似。
2.3、ImageSharp
ImageSharp与SkiaShar一致,使用一个即可。
3、保存与返回
获取到图片的Byte数组后,随机生成一个Key,将Key与验证码内容保存至Redis中。再将Key与Byte数组返回致前端。
随机生成Key可使用当前时间戳与随机字符结合。不重复即可。Guid也可
存入Redis可设置一个有效期,建议有效期单位为分钟。
存入Redis参考代码如下:
// 从配置文件中读取Redis连接字符串
RedisHelper redisHelper = new RedisHelper(ConfigHelper.GetConfig("RedisConn"));
// 设置redis数据库
redisHelper.SetDb(2);
// 生成Key
DateTimeOffset dateTimeOffset = new DateTimeOffset(DateTime.Now);
long mills = dateTimeOffset.ToUnixTimeMilliseconds();
Random random = new Random();
string indeCode = "Identity" + mills + random.Next(10) + random.Next(10) + random.Next(10) + random.Next(10);
// 存入Redis 有效期5分钟
bool flag = redisHelper.SetValueMinute(indeCode, code, 5);
if (flag)
{
return indeCode;
}
else
{
throw new Exception("存入Redis失败");
}
RedisHelper代码:
public class RedisHelper
{
private ConnectionMultiplexer redis { get; set; }
private IDatabase db { get; set; }
public RedisHelper(string connection)
{
redis = ConnectionMultiplexer.Connect(connection);
}
/// <summary>
/// 修改redis数据库
/// </summary>
/// <param name="i"></param>
public void SetDb(Int32 i)
{
db = redis.GetDatabase(i);
}
/// <summary>
/// 保存
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
/// <returns></returns>
public bool SetValue(string key, string value)
{
return db.StringSet(key, value);
}
/// <summary>
/// 保存且附带到期时间
/// </summary>
/// <param name="key"></param>
/// <param name="value"></param>
/// <param name="min"></param>
/// <returns></returns>
public bool SetValueMinute(string key,string value,int min)
{
return db.StringSet(key, value, new TimeSpan(TimeSpan.TicksPerMinute * min));
}
/// <summary>
/// 获取
/// </summary>
/// <param name="key"></param>
/// <returns></returns>
public string GetValue(string key)
{
return db.StringGet(key);
}
/// <summary>
/// 删除
/// </summary>
/// <param name="key"></param>
/// <returns></returns>
public bool DeleteKey(string key)
{
return db.KeyDelete(key);
}
}
4、登入验证
登入时前端需将Key与前端输入的验证码都传入登入接口,登入接口需从Redis中根据Key查询相应的验证码内容,检查输入的验证码与对应的验证码内容是否匹配,只有匹配才可进行接下来的登入操作。
使用ReidsHelper中的GetValue方法即可。
5、所遇问题
System.Drawing.Common与SkiaSharp所写的代码,本地调试均可成功。
本地调试结果如下:
转图片如下:
而将项目部署至Linux的Docker中,则都会出现问题。SkiaSharp所遇问题为之有干扰线,没有验证码内容。
System.Drawing.Common所遇问题是:
The type initializer for ‘Gdip‘ threw an exception
网上讲解:SkiaSharp遇到的问题是DrawText方法,将Text内容画到图片中,因为设置的Text字体是Windows中的字体,Linux中没有该字体。所以无法将验证码写入图片中。解决办法就是将Windows中的字体导入Linux中,即可。
而System.Drawing.Common遇到的报错原因是System.Drawing.Common是windows版本的动态库,导入该包时也有提醒。该依赖包只可在Windows中使用。解决方案就是在Linux中下载libgdiplus即可。
以上方案均尝试,无法解决对应的问题。
可能因为网络教程的.NetCore版本是3.1。而我使用的版本为5.0。(仅猜测)
而使用System.Drawing.Common的解决方案,可以解决SkiaSharp的问题。因为libgdiplus是附带字体与语言的。
6、部署方案
因为是使用Docker部署的,每次部署都会生成新的容器,而新容器中又没有libgdiplus。所有又需要重新下载libgdiplus。
只需项目运行依赖的netcore镜像中包含libgdiplus即可解决该问题,如何生成一个附带libgdiplus的镜像包。
操作步骤如下:
首先导入一个对应的Netcore基础镜像包。版本3.1或者5.0自定义
docker pull mcr.microsoft.com/dotnet/aspnet:5.0
docker pull mcr.microsoft.com/dotnet/core/aspnet:3.1
之后运行进入容器内部
docker run -it mcr.microsoft.com/dotnet/aspnet:5.0 /bin/bash
docker run -it mcr.microsoft.com/dotnet/core/aspnet:3.1 /bin/bash
进入容器后使用命令下载libgdiplus,该操作比较耗时
apt-get update -y
apt-get install -y libgdiplus
apt-get clean
ln -s /usr/lib/libgdiplus.so /usr/lib/gdiplus.dll
下载完成后退出容器,使用docker ps -a查看容器ID。根据容器DI生成镜像
docker commit -a="simple" -m="added libgdiplus based on .netcore5.0" 容器ID dotnetcore-with-libgdiplus:v5.0
docker commit -a="simple" -m="added libgdiplus based on .netcore3.1" 容器ID dotnetcore-with-libgdiplus:v3.1
生成镜像后修改项目的DockerFile文件 即可
FROM dotnetcore-with-libgdiplus:v5.0 AS base
FROM dotnetcore-with-libgdiplus:v3.1 AS base