在ASP.NET平台构建具备基本安全能力的网站时,图形验证码是一道常见且实用的防线。它利用人类视觉易于识别而程序难以自动解析的扭曲文字,阻断批量注册与暴力登录。核心思路是服务端随机产生字符串,使用绘图接口将其渲染成图片返回前端,同时把正确文本保存在会话状态中,待用户提交表单时做一致性校验。

随机码生成与Session存储机制
验证码的安全基础在于随机性。如果字符串可被预测,整个防护就形同虚设。在.NET中,我们可以使用Random类配合字符集数组来生成指定长度的乱码,字符集应排除容易混淆的字母数字,例如将数字0与字母O分开处理。为了提升不可预测性,也可用Guid的部分片段做二次混淆,但注意控制长度避免用户难以输入。
生成后的文本必须存储在服务端,常见的做法是放入Session,因为Session与用户浏览器Cookie中的标识绑定,其他访客无法读取。在Web Forms或MVC里都可通过HttpContext.Current.Session赋值。需要注意Session默认有超时,若用户停留页面过久导致Session失效,校验时应给出友好提示而非直接报错。另外在负载均衡环境下,要确保多台服务器共享Session状态,否则会出现图片在一台机器生成、提交到另一台机器比对失败的情况。
下面示例展示一个生成四位随机码并存入Session的方法。这里把字符集限定为大写字母与数字的组合,并将结果转为大写方便比对。实际项目中可将其封装成独立工具类,在控制器或页面后台调用。
using System;
using System.Web;
public class CodeBuilder
{
public static string MakeCode(int len)
{
string chars = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789";
Random rnd = new Random();
char[] buf = new char[len];
for (int i = 0; i < len; i++)
{
buf[i] = chars[rnd.Next(chars.Length)];
}
string code = new string(buf);
HttpContext.Current.Session["vc"] = code;
return code;
}
}
使用System.Drawing绘制验证码图像
ASP.NET通过System.Drawing命名空间提供完整的GDI+绘图能力。我们创建Bitmap对象作为画布,利用Graphics类在其上绘制背景、文字和干扰元素。文字可使用不同字体、大小和颜色,并施加旋转或位移,使机器识别难度增加。背景宜用浅色填充,防止与前景文字对比度过低影响真人阅读。
干扰线、噪点是防OCR的关键。可以在画布上随机画几条曲线或直线,再散布若干像素点。要注意干扰强度平衡:过于简单会被打码平台轻易绕过,过于复杂则真实用户也无法辨认。一般三到五条干扰线加少量噪点即可。以下代码演示从随机码到输出图片流的完整过程,使用MemoryStream返回,前端以img标签的src指向该接口即可显示。
using System;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Web;
public class VcHandler : IHttpHandler
{
public void ProcessRequest(HttpContext ctx)
{
string code = CodeBuilder.MakeCode(4);
Bitmap bmp = new Bitmap(120, 40);
Graphics g = Graphics.FromImage(bmp);
g.Clear(Color.WhiteSmoke);
Random r = new Random();
for (int i = 0; i < 4; i++)
{
g.DrawString(code[i].ToString(),
new Font("Arial", 20, FontStyle.Italic),
new SolidBrush(Color.FromArgb(r.Next(255), r.Next(255), r.Next(255))),
new PointF(i * 28 + 5, r.Next(5)));
}
for (int j = 0; j < 4; j++)
{
g.DrawLine(new Pen(Color.Gray),
new Point(r.Next(120), r.Next(40)),
new Point(r.Next(120), r.Next(40)));
}
MemoryStream ms = new MemoryStream();
bmp.Save(ms, ImageFormat.Png);
ctx.Response.ContentType = "image/png";
ctx.Response.BinaryWrite(ms.ToArray());
g.Dispose();
bmp.Dispose();
}
public bool IsReusable { get { return false; } }
}
上述代码在一般处理程序ashx中运行良好。若使用MVC,可改为返回FileResult的Action,逻辑完全一致。需要留意的是System.Drawing在部分跨平台场景如Linux容器中存在依赖限制,传统Windows服务器或借助原生库封装时最为稳定。
前后端校验与常见安全误区
图片生成只是第一步,真正的防护效果取决于后端校验是否严谨。用户提交表单时,应从Session取出之前保存的验证码文本,与输入值统一大小写后比较。比较必须使用固定时间算法或至少避免异常分支过早返回,以防计时攻击,不过对于普通站点而言,直接等价判断并限制错误次数已能挡住大部分 bot。
一个常见误区是把验证码答案直接写进页面隐藏域或前端Cookie,这等于把钥匙挂在门上。也有开发者用图片文件名携带答案,同样危险。正确做法是答案只存服务端Session或缓存,并设置短期有效。另一个误区是永远不刷新,用户输错后还显示同一张图,攻击者可复用识别结果。应在每次加载或校验失败后重新生成图片与Session值。
此外要考虑无障碍与降级。验证码对视障用户不友好,正式产品应提供语音码或短信验证作为替代。在频繁失败锁定时可引入限流中间件,结合IP统计减少资源消耗。下面示例展示MVC控制器中如何完成比对逻辑,其中包含失败重新生成的处理思路。
using System.Web.Mvc;
using System.Web;
public class AccountController : Controller
{
[HttpPost]
public ActionResult Login(string user, string pass, string vc)
{
string sess = HttpContext.Session["vc"] as string;
if (string.IsNullOrEmpty(sess) || !string.Equals(sess, vc, StringComparison.OrdinalIgnoreCase))
{
ViewBag.Msg = "验证码错误";
return View();
}
// 继续校验账号密码
return RedirectToAction("Index", "Home");
}
}
综合来看,利用ASP.NET原生绘图能力实现图形验证码并不复杂。只要把握随机源可靠、图像干扰适度、答案留存服务端、校验及时刷新四个要点,就能以极小成本补足系统的第一道自动拦截关卡。后续若业务规模扩大,再考虑引入行为验证或第三方人机校验服务即可。