欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

.NET生成动态验证码的完整步骤

程序员文章站 2022-09-06 23:53:43
前言 验证码是图片上写上几个字,然后对这几个字做特殊处理,如扭曲、旋转、修改文字位置,然后加入一些线条,或加入一些特殊效果,使这些在人类能正常识别的同时,机器却很难识别出...

前言

验证码是图片上写上几个字,然后对这几个字做特殊处理,如扭曲、旋转、修改文字位置,然后加入一些线条,或加入一些特殊效果,使这些在人类能正常识别的同时,机器却很难识别出来,以达到防爬虫、防机器人的效果。

验证码通常用于网站中,是防爬虫、防机器人侵入的好方法。以往.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生成多次截图而来,实际为静态图):

.NET生成动态验证码的完整步骤

然后再加入一些线条:

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生成多次截图而来,实际为静态图):

.NET生成动态验证码的完整步骤

还能做什么?

很遗憾,还有很多可以做,即使是加入线条,机器依然能轻而易举地识别出来。

不过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生成多次截图而来,实际为静态图):

.NET生成动态验证码的完整步骤

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生成多次截图而来,实际为静态图):

.NET生成动态验证码的完整步骤

可以注意到,direct2d生成的文字没有system.drawing那样的锯齿。

如果取消里面的两行注释,可以得到更加扭曲和旋转的效果(gif是由linqpad生成多次截图而来,实际为静态图):

.NET生成动态验证码的完整步骤

然后加入线条:

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生成多次截图而来,实际为静态图):

.NET生成动态验证码的完整步骤

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生成多次截图而来,实际为静态图):

.NET生成动态验证码的完整步骤

在此基础上,(感谢direct2d/wic)经过较小的改动,即可生成一个动态的gif图片。

只要略微修改以上代码:

  • pngbitmapencoder改成gifbitmapencoder*
  • 然后将下面的for循环取消注释
  • displacement.scale = 100.0f;改成displacement.scale = math.abs(frameid) * 10.0f;

即可看到以下效果(直接生成,非截图):

.NET生成动态验证码的完整步骤
.NET生成动态验证码的完整步骤
.NET生成动态验证码的完整步骤

结语

最终的代码生成效果,可以从这里下载,用linqpad 6打开。

本文使用的是sharpdx,是c#到directx的转换层。一个坏消息是,上图中使用的sharpdx已经停止维护了,但目前还没找到可以用于替换的库(可能由于它太好用了)。

以前我经常将direct2d用于游戏,但最近越来越多的时候direct2d已经用于解决实际问题。由于direct2d的高颜值、高性能,实际上,direct2d已经无处不在,浏览器/word/excel等日常软件都是深度集成direct2d的应用。相信direct2d可以用于更多的场景中。

总结

以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对的支持。