.NET生成动态验证码的完整步骤
前言
验证码是图片上写上几个字,然后对这几个字做特殊处理,如扭曲、旋转、修改文字位置,然后加入一些线条,或加入一些特殊效果,使这些在人类能正常识别的同时,机器却很难识别出来,以达到防爬虫、防机器人的效果。
验证码通常用于网站中,是防爬虫、防机器人侵入的好方法。以往.net中创建验证码,通常会使用system.drawing
创建“正常”的验证码。
在前一往篇博客.net中生成水印更好的方法中,提到了如何给图片加水印。本文将基于上篇博客进一步探索,使用direct2d创建验证码。
传统system.drawing
的做法
前置条件:引用system.drawing
,或者安装nuget包:system.drawing.common
:
<packagereference include="system.drawing.common" version="4.5.1" />
首先创建一个有几个文字的图片(基本操作):
byte[] getimage(int width, int height, string text) { using (var bitmap = new bitmap(width, height)) using (var g = graphics.fromimage(bitmap)) { var r = new random(); g.clear(colorfromhsl(r.nextdouble(), 1.0f, 0.8f, 0xff)); var brush = new solidbrush(color.black); var fontsize = width / text.length; var font = new font(fontfamily.genericserif, fontsize, fontstyle.bold, graphicsunit.pixel); for (var i = 0; i < text.length; i++) { brush.color = colorfromhsl(r.nextdouble(), 1.0f, 0.3f, 0xff); float x = i * fontsize; float y = r.next(0, height - fontsize); g.drawstring(text[i].tostring(), font, brush, x, y); } // 在这里面加入一些其它效果 var ms = new memorystream(); bitmap.save(ms, imageformat.png); return ms.toarray(); } }
效果(gif是由linqpad生成多次截图而来,实际为静态图):
然后再加入一些线条:
using (var pen = new pen(brush, 3)) { for (var i = 0; i < 4; ++i) { pen.color = colorfromhsl(r.nextdouble(), 1.0f, 0.4f, 0xff); var p1 = new point(r.next(width), r.next(height)); var p2 = new point(r.next(width), r.next(height)); g.drawline(pen, p1, p2); } }
效果(gif是由linqpad生成多次截图而来,实际为静态图):
还能做什么?
很遗憾,还有很多可以做,即使是加入线条,机器依然能轻而易举地识别出来。
不过edi.wang
在他的中也发布了一个生成验证码的nuget包:edi.captcha
,截止目前最新版是1.3.1:
<packagereference include="edi.captcha" version="1.3.1" />
这个包基于system.drawing
,加入了扭曲效果,加入了一些随机的x坐标偏移,极大地增加了ai识别的难度。
使用方式:
captcharesult result = captchaimagegenerator.getimage(200, 100, "hello");
其中captcharesult的定义如下:
public class captcharesult { public string captchacode { get; set; } public byte[] captchabytedata { get; set; } public string captchbase64data => convert.tobase64string(captchabytedata); public datetime timestamp { get; set; } }
生成的效果如下(gif是由linqpad生成多次截图而来,实际为静态图):
direct2d
在前一篇博客中,已经有了direct2d的相关简介。这里将不再介绍。
首先从最简单的图片上写文字开始:
byte[] saved2dbitmap(int width, int height, string text) { using var wic = new wic.imagingfactory2(); using var d2d = new d2d.factory(); using var wicbitmap = new wic.bitmap(wic, width, height, wic.pixelformat.format32bpppbgra, wic.bitmapcreatecacheoption.cacheondemand); using var target = new d2d.wicrendertarget(d2d, wicbitmap, new d2d.rendertargetproperties()); using var dwritefactory = new sharpdx.directwrite.factory(); using var brush = new solidcolorbrush(target, color.yellow); var r = new random(); target.begindraw(); target.clear(colorfromhsl(r.nextfloat(0, 1), 1.0f, 0.3f)); var textformat = new dwrite.textformat(dwritefactory, "times new roman", dwrite.fontweight.bold, dwrite.fontstyle.normal, width / text.length); for (int charindex = 0; charindex < text.length; ++charindex) { using var layout = new dwrite.textlayout(dwritefactory, text[charindex].tostring(), textformat, float.maxvalue, float.maxvalue); var layoutsize = new vector2(layout.metrics.width, layout.metrics.height); using var b2 = new lineargradientbrush(target, new d2d.lineargradientbrushproperties { startpoint = vector2.zero, endpoint = layoutsize, }, new gradientstopcollection(target, new[] { new gradientstop{ position = 0.0f, color = colorfromhsl(r.nextfloat(0, 1), 1.0f, 0.8f) }, new gradientstop{ position = 1.0f, color = colorfromhsl(r.nextfloat(0, 1), 1.0f, 0.8f) }, })); var position = new vector2(charindex * width / text.length, r.nextfloat(0, height - layout.metrics.height)); target.transform = matrix3x2.translation(-layoutsize / 2) * // 文字旋转和扭曲效果,取消注释以下两行代码 // matrix3x2.skew(r.nextfloat(0, 0.5f), r.nextfloat(0, 0.5f)) * // matrix3x2.rotation(r.nextfloat(0, mathf.pi * 2)) * matrix3x2.translation(position + layoutsize / 2); target.drawtextlayout(vector2.zero, layout, b2); } // 其它效果在这里插入 target.enddraw(); using (var encoder = new wic.bitmapencoder(wic, wic.containerformatguids.png)) using (var ms = new memorystream()) { encoder.initialize(ms); using (var frame = new wic.bitmapframeencode(encoder)) { frame.initialize(); frame.setsize(wicbitmap.size.width, wicbitmap.size.height); var pixelformat = wicbitmap.pixelformat; frame.setpixelformat(ref pixelformat); frame.writesource(wicbitmap); frame.commit(); } encoder.commit(); return ms.toarray(); } }
使用方式:
byte[] captchabytes = saved2dbitmap(200, 100, "hello");
效果(gif是由linqpad生成多次截图而来,实际为静态图):
可以注意到,direct2d生成的文字没有system.drawing
那样的锯齿。
如果取消里面的两行注释,可以得到更加扭曲和旋转的效果(gif是由linqpad生成多次截图而来,实际为静态图):
然后加入线条:
for (var i = 0; i < 4; ++i) { target.transform = matrix3x2.identity; brush.color = colorfromhsl(r.nextfloat(0,1), 1.0f, 0.3f); target.drawline( r.nextvector2(vector2.zero, new vector2(width, height)), r.nextvector2(vector2.zero, new vector2(width, height)), brush, 3.0f); }
效果(gif是由linqpad生成多次截图而来,实际为静态图):
direct2d的骚操作
direct2d中内置了许多,如阴影(shadow)等,这里我们需要用到的是位移特效(displacement)和水流特效(turbulence),为了实现特效,需要加入一个bitmap层,整体代码如下:
byte[] saved2dbitmap(int width, int height, string text) { using var wic = new wic.imagingfactory2(); using var d2d = new d2d.factory(); using var wicbitmap = new wic.bitmap(wic, width, height, wic.pixelformat.format32bpppbgra, wic.bitmapcreatecacheoption.cacheondemand); using var target = new d2d.wicrendertarget(d2d, wicbitmap, new d2d.rendertargetproperties()); using var dwritefactory = new sharpdx.directwrite.factory(); using var brush = new d2d.solidcolorbrush(target, color.yellow); using var encoder = new wic.pngbitmapencoder(wic); // pngbitmapencoder using var ms = new memorystream(); using var dc = target.queryinterface<d2d.devicecontext>(); using var bmplayer = new d2d.bitmap1(dc, target.pixelsize, new d2d.bitmapproperties1(new d2d.pixelformat(sharpdx.dxgi.format.b8g8r8a8_unorm, d2d.alphamode.premultiplied), d2d.desktopdpi.width, d2d.desktopdpi.height, d2d.bitmapoptions.target)); var r = new random(); encoder.initialize(ms); d2d.image oldtarget = dc.target; { dc.target = bmplayer; dc.begindraw(); var textformat = new dwrite.textformat(dwritefactory, "times new roman", dwrite.fontweight.bold, dwrite.fontstyle.normal, width / text.length); for (int charindex = 0; charindex < text.length; ++charindex) { using var layout = new dwrite.textlayout(dwritefactory, text[charindex].tostring(), textformat, float.maxvalue, float.maxvalue); var layoutsize = new vector2(layout.metrics.width, layout.metrics.height); using var b2 = new d2d.lineargradientbrush(dc, new d2d.lineargradientbrushproperties { startpoint = vector2.zero, endpoint = layoutsize, }, new d2d.gradientstopcollection(dc, new[] { new d2d.gradientstop{ position = 0.0f, color = colorfromhsl(r.nextfloat(0, 1), 1.0f, 0.8f) }, new d2d.gradientstop{ position = 1.0f, color = colorfromhsl(r.nextfloat(0, 1), 1.0f, 0.8f) }, })); var position = new vector2(charindex * width / text.length, r.nextfloat(0, height - layout.metrics.height)); dc.transform = matrix3x2.translation(-layoutsize / 2) * matrix3x2.skew(r.nextfloat(0, 0.5f), r.nextfloat(0, 0.5f)) * //matrix3x2.rotation(r.nextfloat(0, mathf.pi * 2)) * matrix3x2.translation(position + layoutsize / 2); dc.drawtextlayout(vector2.zero, layout, b2); } for (var i = 0; i < 4; ++i) { target.transform = matrix3x2.identity; brush.color = colorfromhsl(r.nextfloat(0, 1), 1.0f, 0.3f); target.drawline( r.nextvector2(vector2.zero, new vector2(width, height)), r.nextvector2(vector2.zero, new vector2(width, height)), brush, 3.0f); } target.enddraw(); } color background = colorfromhsl(r.nextfloat(0, 1), 1.0f, 0.3f); // for (var frameid = -10; frameid < 10; ++frameid) { dc.target = null; using var displacement = new d2d.effects.displacementmap(dc); displacement.setinput(0, bmplayer, true); displacement.scale = 100.0f; // math.abs(frameid) * 10.0f; var turbulence = new d2d.effects.turbulence(dc); displacement.setinputeffect(1, turbulence); dc.target = oldtarget; dc.begindraw(); dc.clear(background); dc.drawimage(displacement); dc.enddraw(); using (var frame = new wic.bitmapframeencode(encoder)) { frame.initialize(); frame.setsize(wicbitmap.size.width, wicbitmap.size.height); var pixelformat = wicbitmap.pixelformat; frame.setpixelformat(ref pixelformat); frame.writesource(wicbitmap); frame.commit(); } } encoder.commit(); return ms.toarray(); }
注意此代码使用了using var
语句,是c# 8.0的using declaration
功能,可以用using (var )
语句代替。
效果如下(gif是由linqpad生成多次截图而来,实际为静态图):
在此基础上,(感谢direct2d
/wic)经过较小的改动,即可生成一个动态的gif图片。
只要略微修改以上代码:
- 将
pngbitmapencoder
改成gifbitmapencoder
* - 然后将下面的
for
循环取消注释 - 将
displacement.scale = 100.0f;
改成displacement.scale = math.abs(frameid) * 10.0f;
即可看到以下效果(直接生成,非截图):
结语
最终的代码生成效果,可以从这里下载,用linqpad 6打开。
本文使用的是sharpdx,是c#到directx的转换层。一个坏消息是,上图中使用的sharpdx已经停止维护了,但目前还没找到可以用于替换的库(可能由于它太好用了)。
以前我经常将direct2d用于游戏,但最近越来越多的时候direct2d已经用于解决实际问题。由于direct2d的高颜值、高性能,实际上,direct2d已经无处不在,浏览器/word/excel等日常软件都是深度集成direct2d的应用。相信direct2d可以用于更多的场景中。
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对的支持。