一、先看效果图
二、背景介绍
图形验证码网上有挺多,比如:网易易盾、腾讯防水墙、阿里云验证码等等。参考了一下,自己实现了一个简单的成语点选的模式。
三、实现思路
1.选择若干张图片(这里使用的是320x160的尺寸),随机从中抽取一张作为背景图。
2.整理一个成语库,用作验证码里的字。
3.将选择的成语随机(位置随机,字体随机,颜色随机)绘制到背景图上,记录每个字的坐标范围,后面用于验证用户是否选择正确。
4.将成语及图片返回给前端。
5.前端点击后,将点击坐标点传回后端,后端进行验证。
四、实现代码
C# ASP.NET MVC版
1.后端生成验证码图片
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.Globalization;
using System.IO;
using System.Text.RegularExpressions;
using System.Web;
namespace RC.Controller
{
public class ValidateHelper
{
#region 检测选中的位置是否为后台设置的文字位置(判断验证码输入是否有效)
/// <summary>
/// 检测选中的位置是否为后台设置的文字位置(判断验证码输入是否有效)
/// </summary>
/// <param name="input"></param>
/// <param name="range"></param>
/// <returns></returns>
public static bool Validate(string input, string range)
{
if (input.Length != 24) return false;
if (!new Regex(
"^\\d{24}$",
RegexOptions.CultureInvariant
| RegexOptions.Compiled
).IsMatch(input))
return false;
var list = new List<int>();
for (var i = 0; i < input.Length; i += 3)
list.Add(i + 3 <= input.Length ? int.Parse(input.Substring(i, 3)) : int.Parse(input.Substring(i)));
//输入的点坐标
var inputPointDic = new Dictionary<string, string>();
var index = 0;
for (var i = 0; i < list.Count; i += 2)
{
var x = list[i];
var y = list[i + 1];
inputPointDic.Add("P" + index, x + "," + y);
index++;
}
//每个点的坐标范围
var rangeDic = new Dictionary<string, string>(); //格式:Xmin-Xmax,Ymin-Ymax|...";
var arr = range.Split(new[] { "|" }, StringSplitOptions.RemoveEmptyEntries);
for (var i = 0; i < arr.Length; i++)
rangeDic.Add("P" + i, arr[i]);
var passed = 0;
if (rangeDic.Count == inputPointDic.Count)
//遍历判断每个点的坐标
foreach (var pair in inputPointDic)
{
var pos = pair.Value.Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries);
var score = rangeDic[pair.Key].Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries);
if (pos.Length == 2 && score.Length == 2)
{
//坐标点
var x = int.Parse(pos[0]);
var y = int.Parse(pos[1]);
//坐标范围
var xcore = score[0].Split(new[] { "-" }, StringSplitOptions.RemoveEmptyEntries);
var ycore = score[1].Split(new[] { "-" }, StringSplitOptions.RemoveEmptyEntries);
if (xcore.Length == 2 && x >= int.Parse(xcore[0]) && x < int.Parse(xcore[1]) &&
ycore.Length == 2 &&
y >= int.Parse(ycore[0]) && y < int.Parse(ycore[1]))
passed++;
}
}
return passed == inputPointDic.Count;
}
#endregion
#region 随机获取成语验证码
public static string GetWord()
{
var source =
"心旷神怡|心平气和|十年寒窗|孙康映雪|协心戮力|埋头苦干|勤学苦练|冬寒抱冰|夏热握火|发奋图强|前功尽废|艰苦卓绝|坚苦卓绝|勤学苦练|同德一心|节俭力行|幼学壮行|急起直追|咬紧牙关|奋勇向前|志坚行苦|咬紧牙关|映雪读书|并心同力|分秒必争|形势逼人|身体力行|逆水行舟|学如登山|坐以待旦|来处不易|废寝忘食|朝夕不倦|发愤图强|躬体力行|不辞辛苦|学而不厌|百无一成|勉求多福|开足马力|听命由天|自强不息|咬紧牙根|穿壁引光|力争上游|得失在人|惊人之举|一篑之功|尽心竭力|磨穿铁砚|绝甘分少|手不释卷|刻苦耐劳|凿壁偷光|旗开得胜|一分为二|当仁不让|力争上游|望风响应|干劲冲天|奋发图强|争先恐后|四平八稳|坐而待毙|分秒必争|欢欣踊跃|坐以待亡|一马当先|自告奋勇|踊跃争先|马到功成|";
source +=
"杯弓蛇影|鹤立鸡群|画蛇添足|生龙活虎|指鹿为马|雕虫小技|鸡毛蒜皮|千军万马|万马奔腾|虎口余生|狼虫虎豹|泥牛入海|成群打伙|狗急跳墙|气象万千|毒蛇猛兽|狐假虎威|马到成功|马到成功|鸡犬不宁|叶公好龙|藏龙卧虎|野性难驯|成帮结队|凤毛麟角|弱肉强食|瘦骨伶仃|予齿去角|伶牙利爪|杀鸡儆猴|抱头鼠窜|对牛弹琴|狡兔三窟|井底之蛙|龙飞凤舞|声名狼藉|车水马龙|虎头蛇尾|非池中物|狼吞虎咽|黔驴技穷|热地蚰蜒|一箭双雕|鹰击毛挚|好生之德|冷血动物|包罗万象|龟龙鳞凤|惊弓之鸟|盲人摸象|五毒俱全|一马当先|塞翁失马|调虎离窠|兜肚连肠|含沙射影|麟凤龟龙|万象更新|狼狈为奸|普渡众生|白驹过隙|打草惊蛇|跂行喙息|管中窥豹|守株待兔|鳄鱼眼泪|青梅竹马|骑虎难下|为鬼为蜮|为虎作伥|画龙点睛|鱼龙曼延|亡羊补牢|";
var arr = source.Split(new[] { "|" }, StringSplitOptions.RemoveEmptyEntries);
var random = new Random();
var code = arr[random.Next(0, arr.Length)];
return code;
}
#endregion
#region 根据成语生成验证码图片(背景随机,颜色随机,位置随机,字体随机)
/// <summary>
/// 根据成语生成验证码图片(背景随机,颜色随机,位置随机,字体随机)
/// </summary>
/// <param name="validCode"></param>
/// <returns></returns>
public static Dictionary<string, string> Create(string validCode)
{
var o = new Dictionary<string, string>();
var random = new Random();
//第1步:随机取一张背景图
var bg = GetMapPath("~/Content/images/validcode/" + random.Next(1, 16) + ".jpg");
//字体颜色集合
var colorArr = new List<Color>
{
HexToRGB("#5f4b50"),
HexToRGB("#cf390f"),
HexToRGB("#7b217a"),
HexToRGB("#e3d457"),
HexToRGB("#2a9557"),
HexToRGB("#3a463a")
};
//字体集合
var fontArr = new List<Font>
{
new Font("幼圆", 32, FontStyle.Bold),
new Font("隶书", 32),
new Font("微软雅黑", 32, FontStyle.Bold),
new Font("华文行楷", 32),
new Font("华文楷体", 32),
new Font("华文彩云", 32, FontStyle.Bold),
new Font("楷体", 32, FontStyle.Bold)
};
var image = Image.FromFile(bg);
image = AddWater(image);
using (image)
{
var width = image.Width;
var height = image.Height;
var sp = (width - 40) / 4;
using (var bitmap = new Bitmap(image))
{
var graphics = Graphics.FromImage(bitmap);
var arr = validCode.ToCharArray();
var posArr = new List<PointF>();
//计算出点坐标
for (var i = 0; i < arr.Length; i++)
{
var x = random.Next(i * sp + 20, (i + 1) * sp - 20);
var y = random.Next(40, height - 60); //留点边距
var point = new PointF(x, y);
posArr.Add(point);
}
//将文字随机放到坐标点上
var position = "";
foreach (var c in arr)
{
var font = fontArr[random.Next(fontArr.Count)];
var size = graphics.MeasureString(c.ToString(), font);
var j = random.Next(posArr.Count);
var k = random.Next(colorArr.Count);
var point = posArr[j];
position += point.X + "-" + (int)(point.X + size.Width) + "," + point.Y + "-" +
(int)(point.Y + size.Height) + "|"; //字点击范围
//旋转角度
var ret = random.Next(-70, 70);
var matrix = graphics.Transform;
matrix.RotateAt(ret, new PointF(point.X + size.Width / 2, point.Y + size.Height / 2));
graphics.Transform = matrix;
//写上文字
graphics.DrawString(c.ToString(), font, new SolidBrush(colorArr[k]), point);
//复原角度
matrix = graphics.Transform;
matrix.RotateAt(-ret, new PointF(point.X + size.Width / 2, point.Y + size.Height / 2));
graphics.Transform = matrix;
//移除已使用项,避免样式重复
posArr.Remove(posArr[j]);
colorArr.Remove(colorArr[k]);
fontArr.Remove(font);
}
o.Add("ValidText", validCode);
o.Add("ValidPos", position.TrimEnd('|'));
o.Add("ValidImage", BitmapToBase64(bitmap));
image.Dispose();
//按流输出
//using (var stream = new MemoryStream())
//{
// bitmap.Save(stream, ImageFormat.Gif);
// Response.ClearContent();
// Response.ContentType = "image/Gif";
// Response.BinaryWrite(stream.ToArray());
//}
return o;
}
}
}
public static Image AddWater(Image source)
{
var txt = "清山博客";
var font = new Font("楷体", 16, FontStyle.Bold, GraphicsUnit.Pixel);
var color = Color.FromArgb(180, 255, 255, 255);
var brush = new SolidBrush(color);
using (var graphics = Graphics.FromImage(source))
{
var size = graphics.MeasureString(txt, font);
var x = source.Width - (int)size.Width;
var y = source.Height - (int)size.Height;
var point = new Point(x, y);
var format = new StringFormat();
graphics.DrawString(txt, font, brush, point, format);
using (var stream = new MemoryStream())
{
source.Save(stream, ImageFormat.Jpeg);
source = Image.FromStream(stream);
}
}
return source;
}
#endregion
#region 辅助方法
protected static string GetMapPath(string strPath)
{
if (strPath.ToLower().StartsWith("http://")) return strPath;
if (HttpContext.Current != null) return HttpContext.Current.Server.MapPath(strPath);
strPath = strPath.Replace("/", "\\");
if (strPath.StartsWith("\\"))
strPath = strPath.TrimStart('\\');
else if (strPath.StartsWith("~")) strPath = strPath.Substring(1).TrimStart('\\');
return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, strPath);
}
protected static string BitmapToBase64(Bitmap bmp)
{
try
{
byte[] arr;
using (var ms = new MemoryStream())
{
bmp.Save(ms, ImageFormat.Jpeg);
arr = new byte[ms.Length];
ms.Position = 0;
var read = ms.Read(arr, 0, (int)ms.Length);
ms.Close();
}
return Convert.ToBase64String(arr);
}
catch (Exception)
{
return null;
}
}
protected static Color HexToRGB(string strHxColor)
{
try
{
if (strHxColor.Length == 0)
return Color.FromArgb(0, 0, 0); //设为黑色
return Color.FromArgb(int.Parse(strHxColor.Substring(1, 2), NumberStyles.AllowHexSpecifier),
int.Parse(strHxColor.Substring(3, 2), NumberStyles.AllowHexSpecifier),
int.Parse(strHxColor.Substring(5, 2), NumberStyles.AllowHexSpecifier));
}
catch
{
return Color.FromArgb(0, 0, 0);
}
}
#endregion
}
}
2.调用层:Controller
using System.Web.Mvc;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using RC.Framework;
namespace RC.Website.Controller.Front
{
public class DemoController : System.Web.Mvc.Controller
{
public ActionResult Validcode()
{
return View("~/views/Demo/Validcode.cshtml");
}
public ActionResult GetValidcode()
{
var code = ValidateHelper.GetWord();
var dic = ValidateHelper.Create(code);
Session["ValidText"] = dic["ValidText"];
Session["ValidImage"] = dic["ValidImage"];
Session["ValidPos"] = dic["ValidPos"]; //坐标位置,用于校验
var res = new JObject
{
["ValidText"] = dic["ValidText"],
["ValidImage"] = dic["ValidImage"]
//["ValidPos"] = dic["ValidPos"].ToStr()
};
return Content(res.ToString(Formatting.None));
}
public ActionResult ValidcodeForm()
{
var code = Request.Params["code"];
var pos = Session["ValidPos"].ToString();
JObject res;
if (code.IsEmpty())
{
res = new JObject
{
["IsSuccess"] = false,
["Body"] = "抱歉,请输入验证码"
};
return Content(res.ToString(Formatting.None));
}
var check = ValidateHelper.Validate(code, pos);
res = new JObject
{
["IsSuccess"] = check,
["Body"] = check ? "验证码校验通过" : "抱歉,验证码输入不正确"
};
return Content(res.ToString(Formatting.None));
}
}
}
3.视图:Validcode.cshtml
@model dynamic
@{
Layout = null;
}
<html>
<head runat="server">
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script src="~/Content/jquery-easyui-1.9.9/jquery.min.js"></script>
<title>验证码测试</title>
<style type="text/css">
body { padding: 0; margin: 0; font-size: 12px; }
.head { width: 90%; margin: 10px auto; border: 0px solid gainsboro; padding: 10px; background-color: white; font-size: 18px; font-weight: bold; color: brown; border-bottom: 2px solid brown }
.content { width: 90%; margin: 10px auto; border: 0px solid gainsboro; padding: 10px; background-color: white; }
.footer { width: 90%; margin: 10px auto; border: 0px solid gainsboro; padding: 10px; background-color: #F7F7F7; text-align: center; color: gray; }
ul { padding: 0 10px; }
li { list-style: none; line-height: 22px; }
th { border: gainsboro solid 1px; height: 25px; text-align: left; padding: 3px 5px; }
td { border: gainsboro solid 1px; height: 25px; padding-left: 10px; }
tr:hover td { background: none; }
table { border: gainsboro solid 1px; border-collapse: collapse; width: 100%; margin-bottom: 15px; }
a { text-decoration: none; }
a:link { color: orangered; }
a:visited { color: orangered; }
a:active { color: orangered; }
a:hover { color: orangered; }
fieldset { border: 1px solid #cccccc; margin-bottom: 10px; padding: 10px; }
.keyword { color: orangered; }
.btnRefush { outline: none; }
input { width: 250px; height: 32px; margin: 5px 0; border: 1px solid #ddd; }
.btn { display: inline-block; width: 120px; height: 30px; line-height: 30px; margin: 5px 0; text-align: center; border: 1px solid #ddd; cursor: pointer; color: #fff; background: #af1818; }
#CaptchaTwo .valid_contain { margin: 5px auto; }
/*PC端*/
@@media screen and (min-width:1200px) {
.todo { display: flex; justify-content: flex-start; flex-wrap: wrap; align-content: flex-start; width: 100%; padding: 0; margin: 0 }
.tag { border: 1px solid #ccc; width: 160px; margin-bottom: 15px; text-align: center; border-radius: 5px; padding: 20px 10px; margin-right: 15px }
.tag .num { border: 5px solid #1378bd; display: block; border-radius: 50%; height: 50px; width: 50px; margin: 0 auto; line-height: 50px; font-size: 22px; font-weight: bold; margin-bottom: 10px }
.tag .title { color: gray; font-size: 12px; }
.tag .orange { border: 5px solid orange; }
.btnRefush { width: 120px; height: 40px; border: 0; background: #1378bd; color: white; border-radius: 5px; display: block; margin: 40px auto; }
}
/*Pad端*/
@@media screen and (min-width:800px) and(max-width:1200px) {
.todo { display: flex; justify-content: flex-start; flex-wrap: wrap; align-content: flex-start; width: 100%; padding: 0; margin: 0 }
.tag { border: 1px solid #ccc; width: 160px; margin-bottom: 15px; text-align: center; border-radius: 5px; padding: 20px 10px; margin-right: 15px }
.tag .num { border: 5px solid #1378bd; display: block; border-radius: 50%; height: 50px; width: 50px; margin: 0 auto; line-height: 50px; font-size: 22px; font-weight: bold; margin-bottom: 10px }
.tag .title { color: gray; font-size: 12px; }
.tag .orange { border: 5px solid orange; }
.btnRefush { width: 120px; height: 40px; border: 0; background: #1378bd; color: white; border-radius: 5px; display: block; margin: 40px auto; }
}
/*手机端*/
@@media screen and (max-width:800px) {
.head { font-size: 16px; }
body { padding: 0; margin: 0; font-size: 14px; }
.todo { display: flex; justify-content: space-evenly; flex-wrap: wrap; align-content: flex-start; width: 100%; padding: 0; margin: 0 }
.tag { border: 1px solid #ccc; width: 40%; margin-bottom: 3.5%; text-align: center; border-radius: 5px; padding: 20px 10px; }
.tag .num { border: 5px solid #1378bd; display: block; border-radius: 50%; height: 50px; width: 50px; margin: 0 auto; line-height: 50px; font-size: 22px; font-weight: bold; margin-bottom: 10px }
.tag .title { color: gray; font-size: 14px; }
.tag .orange { border: 5px solid orange; }
.btnRefush { width: 40%; height: 40px; border: 0; background: #1378bd; color: white; border-radius: 5px; display: block; margin: 40px auto; }
}
</style>
<script src="/Content/captcha/captcha.js" type="text/javascript"></script>
<link href="/Content/captcha/captcha.css" rel="stylesheet" type="text/css" />
</head>
<body>
<div class="head">
验证码测试(成语点选)
</div>
<div class="content">
<div class="todo">
<table class="main-table scene1" style="width: 100%">
<tr><th colspan="2">场景1:选择验证码后,验证码值写入表单隐藏域,点击按钮提交表单</th></tr>
<tr>
<td style="width: 40%; text-align: right; padding: 0 10px;">
手机号码
</td>
<td>
<input name="txtPhone" type="text" maxlength="11" class="txt_input" placeholder="请输入您的手机号码">
</td>
</tr>
<tr>
<td style="width: 40%; text-align: right; padding: 0 10px;">
密码
</td>
<td>
<input name="txtPassword" type="password" maxlength="11" class="txt_input" placeholder="请输入您的密码">
</td>
</tr>
<tr>
<td style="width: 40%; text-align: right; padding: 0 10px;">
验证码
</td>
<td>
<div id="CaptchaOne"></div>
<input name="txtCode" id="txtCode" type="hidden" />
</td>
</tr>
<tr>
<td colspan="2" style="text-align: center">
<div class="btn validBtn1">提交表单</div>
</td>
</tr>
</table>
<table class="main-table scene2" style="width: 100%">
<tr><th colspan="2">场景2:点击提交按钮,显示验证码,选择验证码后,提交表单</th></tr>
<tr>
<td style="width: 40%; text-align: right; padding: 0 10px;">
手机号码
</td>
<td>
<input name="txtPhone" type="text" maxlength="11" class="txt_input" placeholder="请输入您的手机号码">
</td>
</tr>
<tr>
<td style="width: 40%; text-align: right; padding: 0 10px;">
密码
</td>
<td>
<input name="txtPassword" type="password" maxlength="11" class="txt_input" placeholder="请输入您的密码">
</td>
</tr>
<tr class="box2">
<td colspan="2" style="text-align: center">
<div id="CaptchaTwo" style="position: relative">
<div class="btn validBtn2">提交表单</div>
<!-- <div class="valid_contain">
<div class="valid_panel">
<div class="valid_bgimg">
<img class="valid_bg-img" />
</div>
<div class="valid_loadbox">
<div class="valid_loadbox__inner">
<div class="valid_loadicon"></div>
<span class="valid_loadtext">加载中...</span>
</div>
</div>
<div class="valid_top">
<button class="valid_refresh">刷新</button>
</div>
</div>
<div class="valid_control">
<div class="valid_tips">
<span class="valid_tips__icon"></span>
<span class="valid_tips__text">请依次点击图中成语</span>
</div>
</div>
</div> -->
</div>
</td>
</tr>
</table>
<table class="main-table scene3" style="width: 100%">
<tr><th colspan="2">场景3:点击提交按钮,弹窗显示验证码,选择验证码后,提交表单</th></tr>
<tr>
<td style="width: 40%; text-align: right; padding: 0 10px;">
手机号码
</td>
<td>
<input name="txtPhone" type="text" maxlength="11" class="txt_input" placeholder="请输入您的手机号码">
</td>
</tr>
<tr>
<td style="width: 40%; text-align: right; padding: 0 10px;">
密码
</td>
<td>
<input name="txtPassword" type="password" maxlength="11" class="txt_input" placeholder="请输入您的密码">
</td>
</tr>
<tr class="box2">
<td colspan="2" style="text-align: center">
<div class="btn validBtn3" id="CaptchaThree">提交表单</div>
</td>
</tr>
</table>
</div>
</div>
<script type="text/javascript">
// 场景1
var captcha1 = null;
$('#CaptchaOne').clickCaptcha({
reset: true,
imgUrl: '@Url.Action("GetValidcode", "Demo")', // 验证图片生成的请求地址
onComplete: function(code, captcha) {
console.log('点击完成后的验证码', code, captcha)
// captcha.initImg(); // 调用该方法,刷新图片
$(".scene1 #txtCode").val(code);
captcha1 = captcha;
}
});
$(".scene1 .validBtn1").click(function() {
var param = {}
param.phone = $(".scene1 [name='txtPhone']").val();
param.password = $(".scene1 [name='txtPassword']").val();
param.code = $(".scene1 [name='txtCode']").val();
if(!param.code) {
alert('请点击完成验证');
return;
}
$.ajax({
url: '@Url.Action("ValidcodeForm", "Demo")',
type: 'POST',
dataType: 'json',
data: param,
success: function(res) {
if(res.IsSuccess) {
alert(res.Body)
} else {
alert(res.Body)
captcha1.initImg();
}
}
});
});
// 场景2
$('.scene2 .validBtn2').click(function() {
var param = {}
param.phone = $(".scene2 [name='txtPhone']").val();
param.password = $(".scene2 [name='txtPassword']").val();
$('#CaptchaTwo').clickCaptcha({
mode: 'default', // 弹出方式 default-会替换页面指定区域内容;pop-弹窗
imgUrl: '@Url.Action("GetValidcode", "Demo")', // 验证图片生成的请求地址
/*
* 以下是联合提交的配置参数
* 配置submitUrl后,会将验证码以及其他参数一同提交到后台
* 注意:配置该参数后,onComplete回调会被忽略
*/
validFiled: 'code', // 验证码提交到后台的字段,默认为code
submitUrl: '@Url.Action("ValidcodeForm", "Demo")', // 提交数据地址
submitData: param, // 表单的其他数据,例如:账号、密码,会和验证码一同提交
onSubmit: function (res, captcha) {
console.log('提交成功', res, captcha) // res为后台返回给前台的完整数据
if(res.IsSuccess) {
alert(res.Body)
} else {
alert(res.Body)
captcha.initImg(); // 调用该方法,刷新图片
}
}
});
});
// 场景3
$('.scene3 .validBtn3').click(function() {
var param = {}
param.phone = $(".scene3 [name='txtPhone']").val();
param.password = $(".scene3 [name='txtPassword']").val();
$('#CaptchaThree').clickCaptcha({
mode: 'pop', // 弹出方式 default-会替换页面指定区域内容;pop-弹窗
imgUrl: '@Url.Action("GetValidcode", "Demo")', // 验证图片生成的请求地址
/*
* 以下是联合提交的配置参数
* 配置submitUrl后,会将验证码以及其他参数一同提交到后台
* 注意:配置该参数后,onComplete回调会被忽略
*/
validFiled: 'code', // 验证码提交到后台的字段,默认为code
submitUrl: '@Url.Action("ValidcodeForm", "Demo")', // 提交数据地址
submitData: param, // 表单的其他数据,例如:账号、密码,会和验证码一同提交
onSubmit: function (res, captcha) {
console.log('提交成功', res, captcha) // res为后台返回给前台的完整数据
if(res.IsSuccess) {
alert(res.Body)
// captcha.close(); // 调用该方法关闭弹窗,仅在mode: pop时有效
} else {
alert(res.Body)
// captcha.initImg(); // 调用该方法,刷新图片
}
}
});
});
</script>
</body>
</html>
4.文件:captcha.css
:root {
--Bg-Img: url('data:image/png;base64,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');
}
.valid_panel .valid_loadbox .valid_loadbox__inner,
.valid_panel .valid_loadbox .valid_loadbox__inner .valid_loadicon,
.valid_panel .valid_tips__content,
.valid_contain.valid--success .valid_tips .valid_tips__icon,
.valid_contain.valid--error .valid_tips .valid_tips__icon {
display: inline-block;
*display: inline;
zoom: 1;
vertical-align: top;
}
@keyframes loading {
0% {
transform: rotate(0deg);
}
to {
transform: rotate(1turn);
}
}
.valid_contain {
width: 320px;
position: relative;
}
.valid_panel {
width: 320px;
height: 160px;
overflow: hidden;
position: relative;
}
.valid_panel .valid_icon-point {
position: absolute;
width: 26px;
height: 33px;
cursor: pointer;
background-repeat: no-repeat;
}
.valid_panel .valid_icon-point.valid_point-1 {
background-image: var(--Bg-Img);
background-position: 0 -997px;
background-size: 40px 1518px;
}
.valid_panel .valid_icon-point.valid_point-2 {
background-image: var(--Bg-Img);
background-position: 0 -1111px;
background-size: 40px 1518px;
}
.valid_panel .valid_icon-point.valid_point-3 {
background-image: var(--Bg-Img);
background-position: 0 -1035px;
background-size: 40px 1518px;
}
.valid_panel .valid_icon-point.valid_point-4 {
background-image: var(--Bg-Img);
background-position: 0 -1073px;
background-size: 40px 1518px;
}
.valid_panel .valid_icon-point.valid_point-5 {
background-image: var(--Bg-Img);
background-position: 0 -1149px;
background-size: 40px 1518px;
}
.valid_panel .valid_top {
position: absolute;
right: 0;
top: 0;
max-width: 98px;
*max-width: 68px;
z-index: 2;
background-color: rgba(0, 0, 0, 0.12);
*background-color: transparent;
_background-color: transparent;
}
.valid_panel .valid_top:hover {
background-color: rgba(0, 0, 0, 0.2);
*background-color: transparent;
_background-color: transparent;
}
.valid_panel .valid_refresh {
width: 30px;
height: 30px;
margin-left: 4px;
cursor: pointer;
font-size: 0;
vertical-align: top;
text-indent: -9999px;
text-transform: capitalize;
border: none;
background-color: transparent;
}
.valid_panel .valid_refresh {
float: left;
background-image: var(--Bg-Img);
/* background: var(--Bg-Img); */
background-position: 0 -750px;
background-size: 40px 1518px;
}
.valid_panel .valid_refresh:hover {
background-image: var(--Bg-Img);
background-position: 0 -785px;
background-size: 40px 1518px;
}
.valid_panel .valid_loadbox {
display: none;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
text-align: center;
background-color: #f7f9fa;
border-radius: 2px;
}
.valid_panel .valid_loadbox .valid_loadbox__inner {
position: relative;
top: 50%;
margin-top: -25px;
}
.valid_panel .valid_loadbox .valid_loadbox__inner .valid_loadicon {
width: 32px;
height: 32px;
background-repeat: no-repeat;
}
.valid_panel .valid_loadbox .valid_loadbox__inner .valid_loadtext {
display: block;
line-height: 20px;
color: #45494c;
font-size: 12px;
}
.valid_panel.valid--loading .valid_loadicon {
background-image: var(--Bg-Img);
background-position: 0 -960px;
background-size: 40px 1518px;
animation: loading 0.8s linear infinite;
}
.valid_panel.valid--loading .valid_refresh {
cursor: not-allowed;
}
.valid_panel.valid--loadfail .valid_loadicon {
background-image: var(--Bg-Img);
background-position: 0 -890px;
background-size: 40px 1518px;
}
.valid_panel.valid--loadfail .valid_bgimg,
.valid_panel.valid--loading .valid_bgimg {
display: none;
}
.valid_panel.valid--loadfail .valid_loadbox,
.valid_panel.valid--loading .valid_loadbox {
display: block;
}
.valid_contain .valid_control {
position: relative;
width: 100%;
height: 40px;
margin-top: 10px;
box-sizing: border-box;
border: 1px solid #e4e7eb;
background-color: #f7f9fa;
}
.valid_control .valid_tips {
font-size: 14px;
line-height: 40px;
text-align: center;
}
.valid_control .valid_tips .valid_tips__text b {
letter-spacing: 3px;
font-weight: bold;
}
.valid_contain.valid--success .valid_control {
border-color: #52ccba;
background-color: #d2f4ef;
}
.valid_contain.valid--success .valid_tips {
color: #52ccba;
}
.valid_contain.valid--success .valid_tips .valid_tips__icon {
margin-right: 5px;
width: 17px;
height: 12px;
vertical-align: middle;
background-image: var(--Bg-Img);
background-position: 0 -111px;
background-size: 40px 1518px;
}
.valid_contain.valid--error .valid_tips {
color: #f57a7a;
}
.valid_contain.valid--error .valid_control {
border-color: #f57a7a;
background-color: #fce1e1;
}
.valid_contain.valid--error .valid_tips .valid_tips__icon {
margin-right: 5px;
width: 12px;
height: 12px;
vertical-align: middle;
background-image: var(--Bg-Img);
background-position: 0 -77px;
background-size: 40px 1518px;
}
/* 弹出模式 */
.valid_popup {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 9999;
text-align: center;
}
.valid_popup .valid_popup__mask {
touch-action: none;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: #000;
transition: opacity .3s linear;
will-change: opacity;
opacity: 0.3;
}
.valid_popup .valid_modal {
position: relative;
top: 50%;
margin: 0 auto;
transform: translate(0, -50%);
width: 350px;
box-sizing: border-box;
border-radius: 2px;
border: 1px solid #e4e7eb;
background-color: #fff;
box-shadow: 0 0 10px rgba(0,0,0,.3);
touch-action: none;
}
.valid_popup .valid_modal__header {
padding: 0 15px;
height: 50px;
text-align: left;
font-size: 0;
color: #45494c;
border-bottom: 1px solid #e4e7eb;
white-space: nowrap;
position: relative;
}
.valid_popup .valid_modal__title {
font-size: 16px;
line-height: 50px;
vertical-align: middle;
white-space: normal;
}
.valid_popup .valid_modal__close {
position: absolute;
top: 0;
right: 0;
width: 40px;
height: 100%;
text-align: center;
border: none;
background: transparent;
padding: 0;
cursor: pointer;
}
.valid_popup .valid_modal__close .valid_icon-close {
display: inline-block;
width: 11px;
height: 11px;
font-size: 0;
text-indent: -9999px;
text-transform: capitalize;
margin: auto;
vertical-align: middle;
background-image: var(--Bg-Img);
background-position: 0 -61px;
background-size: 40px 1518px;
}
.valid_popup .valid_modal__close:hover .valid_icon-close {
background-image: var(--Bg-Img);
background-position: 0 -45px;
background-size: 40px 1518px;
}
.valid_popup .valid_modal__body {
padding: 15px;
}
5.文件:captcha.js
(function ($) {
'use strict';
var clickCaptcha = function (element, options) {
this.$element = $(element);
this.curIndex = 0;
this.clickPoint = [];
this.options = $.extend({}, clickCaptcha.DEFAULTS, options);
this.initDOM();
};
clickCaptcha.VERSION = '1.0';
clickCaptcha.DEFAULTS = {
width: 320,
height: 160,
mode: 'default', // 渲染方式:default-嵌入式,pop-弹出
maxClick: 4, // 最多点击次数
loadingText: '加载中...',
failedText: '加载失败',
imgUrl: null, // 远程图片获取地址
onComplete: null, // 点选完成的回调事件
submitUrl: null, // 提交地址
submitData: {}, // 其他表单数据
onSubmit: null, // 表单提交回调事件(submitUrl)
onRefresh: null, // 刷新回调事件
};
function Plugin(option) {
return this.each(function () {
var $this = $(this);
var data = $this.data('lgb.clickCaptcha');
var options = typeof option === 'object' && option;
if (data && !/reset/.test(option)) {
// 弹窗模式下,关闭后再点开,需重新加载图片
data.initImg();
if(data.options.mode == 'pop') {
$("body").find(".valid_popup").show();
}
return;
}
if (!data) {
$this.data('lgb.clickCaptcha', data = new clickCaptcha(this, options));
}
if (typeof option === 'string') {
data[option]();
}
});
}
$.fn.clickCaptcha = Plugin;
$.fn.clickCaptcha.Constructor = clickCaptcha;
var _proto = clickCaptcha.prototype;
_proto.initDOM = function () {
var el = this.$element;
var domContainer = `<div class="valid_contain">
<div class="valid_panel">
<div class="valid_bgimg">
<img class="valid_bg-img" />
</div>
<div class="valid_loadbox">
<div class="valid_loadbox__inner">
<div class="valid_loadicon"></div>
<span class="valid_loadtext">加载中...</span>
</div>
</div>
<div class="valid_top">
<button class="valid_refresh">刷新</button>
</div>
</div>
<div class="valid_control">
<div class="valid_tips">
<span class="valid_tips__icon"></span>
<span class="valid_tips__text">请依次点击图中成语</span>
</div>
</div>
</div>`
if(this.options.mode == 'pop') {
var modelContainer = `
<div class="valid_popup">
<div class="valid_popup__mask"></div>
<div class="valid_modal">
<div class="valid_modal__header">
<span class="valid_modal__title">请完成安全验证</span>
<button type="button" class="valid_modal__close">
<span class="valid_icon-close">关闭</span>
</button>
</div>
<div class="valid_modal__body">
${domContainer}
</div>
</div>
</div> `
if($("body .valid_popup").length == 0) {
$("body").append($(modelContainer));
}
$("body").find(".valid_popup").show();
} else if(this.options.mode == 'hover') {
el.append($(domContainer));
el.find(".valid_contain").css({
"position": "absolute",
"left": 0,
"right": 0,
"bottom": 0,
"z-index": 99,
"margin": "0 auto"
})
} else {
el.html($(domContainer));
}
this.initImg();
this.bindEvents();
};
_proto.initImg = function() {
var that = this;
that.reset();
var parentDom = that.options.mode == 'pop' ? $("body .valid_popup") : that.$element;
parentDom.find(".valid_loadtext").text(that.options.loadingText);
parentDom.find(".valid_tips__text").html(that.options.loadingText);
parentDom.find(".valid_panel").addClass('valid--loading').removeClass("valid--loadfail");
$.ajax({
url: that.options.imgUrl,
type: 'GET',
dataType: 'json',
success: function(res) {
parentDom.find(".valid_panel").removeClass('valid--loading');
parentDom.find(".valid_bgimg").children(".valid_icon-point").remove();
parentDom.find(".valid_bgimg").children(".valid_bg-img").attr("src", 'data:image/jpg;base64,'+res.ValidImage);
parentDom.find(".valid_tips__text").html('请依次点击:<b>'+res.ValidText+'</b>');
},
error: function() {
parentDom.find(".valid_panel").removeClass('valid--loading').addClass('valid--loadfail');
parentDom.find(".valid_loadtext").text(that.options.failedText);
}
})
};
_proto.bindEvents = function () {
var that = this;
var parentDom = that.options.mode == 'pop' ? $("body .valid_popup") : that.$element;
// 图片点击事件
parentDom.find(".valid_bgimg").click(function(event) {
if(that.curIndex >= 4) {
return;
}
that.curIndex++;
var html = '<div class="valid_icon-point valid_point-'+that.curIndex+'" style="left: '+(event.offsetX-13)+'px; top: '+(event.offsetY-23)+'px;"></div>';
$(this).append(html);
that.clickPoint.push([event.offsetX, event.offsetY]);
// 成语点击完成
if(that.curIndex == that.options.maxClick) {
that.verify(dealMapArr(that.clickPoint))
}
});
// 刷新事件
parentDom.find(".valid_refresh").click(function() {
that.initImg();
if ($.isFunction(that.options.onRefresh)) {
that.options.onRefresh.call(that.$element);
}
});
// hover事件
if(that.options.mode == 'hover') {
parentDom.hover(function() {
parentDom.find(".valid_contain").show();
}, function() {
parentDom.find(".valid_contain").hide();
});
}
// 弹窗关闭事件
if(that.options.mode == 'pop') {
parentDom.find(".valid_modal__close").click(function() {
$("body").find(".valid_popup").hide();
});
}
};
_proto.verify = function (code) {
var that = this;
if(!code || code.length != 24) {
console.error('生成的校验码不合法')
return;
}
var parentDom = that.options.mode == 'pop' ? $("body .valid_popup") : that.$element;
if(that.options.submitUrl) {
var param = that.options.submitData;
var filed = that.options.validFiled || 'code';
param[filed] = code;
parentDom.find(".valid_tips__text").html('验证中,请稍后...');
$.ajax({
url: that.options.submitUrl,
type: 'POST',
dataType: 'json',
data: param,
success: function(res) {
if(res.IsSuccess) {
parentDom.find(".valid_tips__text").html('验证成功');
parentDom.find(".valid_contain").addClass("valid--success");
} else {
parentDom.find(".valid_tips__text").html('验证失败,请重试');
parentDom.find(".valid_contain").addClass("valid--error");
}
if ($.isFunction(that.options.onSubmit)) {
that.options.onSubmit.call(that.$element, res, that);
}
}
});
} else {
if ($.isFunction(that.options.onComplete)) {
that.options.onComplete.call(that.$element, code, that);
}
}
};
_proto.reset = function() {
var that = this;
that.curIndex = 0;
that.clickPoint = [];
var parentDom = that.options.mode == 'pop' ? $("body .valid_popup") : that.$element;
parentDom.find(".valid_contain").attr("class", "valid_contain");
}
_proto.close = function() {
var that = this;
if(that.options.mode == 'pop') {
$("body").find(".valid_popup").hide();
}
}
function dealMapArr(point) {
var res = '';
for(var i=0; i<point.length; i++) {
res += coverNum(point[i][0], 3)
res += coverNum(point[i][1], 3)
}
function coverNum(num, len) {
num = num.toFixed(0);
if(num.length > len) {
console.error('点击坐标值超过了处理长度')
return num;
}
var index = num.length;
while (index<len) {
index++;
num = '0'+num;
}
return num;
}
return res;
}
})(jQuery);
Java版
1.后端生成验证码图片
package com.cdrc.service;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class ValidateHelper {
public static boolean Validate(String input, String range) {
if (input.length() != 24)
return false;
String pattern = "^\\d{24}$";
if (!Pattern.matches(pattern, input))
return false;
ArrayList list = new ArrayList();
for (int i = 0; i < input.length(); i += 3) {
String tem = input.substring(i, i + 3);
list.add(Integer.parseInt(tem));
}
// 输入的点坐标
Hashtable inputPointDic = new Hashtable<String, String>();
int index = 0;
for (int i = 0; i < list.size(); i += 2) {
int x = (int)list.get(i);
int y = (int)list.get(i + 1);
inputPointDic.put("P" + index, x + "," + y);
index++;
}
// 每个点的坐标范围
Hashtable rangeDic = new Hashtable<String, String>(); // 格式:Xmin-Xmax,Ymin-Ymax|...";
String[] arr = range.split("\\|");
for (int i = 0; i < arr.length; i++)
rangeDic.put("P" + i, arr[i]);
int passed = 0;
if (rangeDic.size() == inputPointDic.size())
for (Iterator iterator = inputPointDic.keySet().iterator(); iterator.hasNext(); ) {
String key = (String) iterator.next();
String value = (String)inputPointDic.get(key);
String[] pos = value.split(",");
String[] score =((String)rangeDic.get(key)).split(",");
if (pos.length == 2 && score.length == 2) {
// //坐标点
int x = Integer.parseInt(pos[0]);
int y = Integer.parseInt(pos[1]);
// 坐标范围
String[] xcore = score[0].split("-");
String[] ycore = score[1].split("-");
if (xcore.length == 2 && x >= Integer.parseInt(xcore[0]) && x < Integer.parseInt(xcore[1]) &&
ycore.length == 2 && y >= Integer.parseInt(ycore[0]) && y < Integer.parseInt(ycore[1]))
passed++;
}
}
return passed == inputPointDic.size();
}
public static String GetWord() {
String source = "奋发图强|持之以恒|坚持不懈|锲而不舍|力争上游|勇往直前|斗志昂扬|壮志凌云|坚定不移|自强不息|朝气蓬勃|发奋图强|百折不挠|大智大勇|奋不顾身|铁杵成针|标新立异|继往开来|独树一帜|勤学苦练|不屈不挠|悬梁刺股|闻鸡起舞|卧薪尝胆|改天换地|革故鼎新|发愤忘食|只争朝夕|一日千里|百尺竿头|推陈出新|别具匠心|别具一格|画龙点睛|鱼龙曼延|亡羊补牢|车水马龙|自强不息|咬紧牙根|马到成功|千军万马|万马奔腾|雕虫小技|心旷神怡|心平气和|十年寒窗|孙康映雪|同德一心|节俭力行|幼学壮行|急起直追|朋心合力|孜孜不辍|乐事劝功|志坚行苦|临池学书|奋身独步|坐以待旦|跛行千里|废寝忘食|折节读书|朝夕不倦|务农息民|久坐地厚|坐薪悬胆|躬体力行|学而不厌|心慕力追|";
String[] arr = source.split("\\|");
String code = arr[Rand(0, arr.length)];
return code;
}
public static Hashtable<String, String> Create(String validCode) {
Hashtable o = new Hashtable<String, String>();
try {
// 第1步:随机取一张背景图
String path = Thread.currentThread().getContextClassLoader().getResource("").getPath().split("/bin")[0];
String bg =path + ("/WebContent/js/captcha/images/" + Rand(1, 15) + ".jpg");
BufferedImage image = ImageIO.read(new File(bg));
// 字体颜色集合
ArrayList colorArr = new ArrayList();
colorArr.add(HexToRGB("#5f4b50"));
colorArr.add(HexToRGB("#cf390f"));
colorArr.add(HexToRGB("#7b217a"));
colorArr.add(HexToRGB("#e3d457"));
colorArr.add(HexToRGB("#2a9557"));
colorArr.add(HexToRGB("#3a463a"));
// 字体集合
ArrayList fontArr = new ArrayList();
fontArr.add(new Font("幼圆", Font.BOLD, 36));
fontArr.add(new Font("隶书", Font.BOLD, 36));
fontArr.add(new Font("微软雅黑", Font.BOLD, 36));
fontArr.add(new Font("华文行楷", Font.BOLD, 36));
fontArr.add(new Font("华文楷体", Font.BOLD, 36));
fontArr.add(new Font("华文彩云", Font.BOLD, 36));
fontArr.add(new Font("楷体", Font.BOLD, 36));
// 获取画笔
Graphics2D graphics = (Graphics2D) image.getGraphics();
int width = image.getWidth();
int height = image.getHeight();
int sp = (width - 40) / 4;
ArrayList posArr = new ArrayList<PointF>();
// 计算出点坐标
for (int i = 0; i < validCode.length(); i++) {
int x = Rand(i * sp + 20, (i + 1) * sp - 20);
int y = Rand(30, height - 40); // 留点边距
PointF point = new ValidateHelper().new PointF(x, y);
posArr.add(point);
}
// 绘制文字
String position = "";
for (int i = 0; i < validCode.length(); i++) {
String c = String.valueOf(validCode.charAt(i));
PointF point = (PointF) posArr.get(Rand(0, posArr.size() - 1));
Font font = (Font) fontArr.get(Rand(0, fontArr.size() - 1));
Color color = (Color) colorArr.get(Rand(0, colorArr.size() - 1));
graphics.setFont(font);
graphics.setColor(color);
FontMetrics metrics = graphics.getFontMetrics();
int w = metrics.stringWidth(c);
int h = metrics.getHeight();
// 旋转角度
int degrees = Rand(-70, 70);
AffineTransform transform = new AffineTransform();
transform.rotate(Math.toRadians(degrees), ((int) point.X + (int) (w / 2)),
((int) point.Y + (int) (h / 2)));
graphics.setTransform(transform);
// 写上文字
graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); // 抗锯齿
// 实际写入文字的高度与坐标的高度不一样,需转换
// 参考:https://blog.csdn.net/qq_21567385/article/details/106078715
int standY = point.Y + metrics.getAscent()
- (metrics.getAscent() + metrics.getDescent() - font.getSize()) / 2;
graphics.drawString(c, point.X, standY);// 实际写入文字的高度与坐标的高度不一样,需转换
// System.out.println(i + " " + c + " x=" + point.X + " y=" + point.Y); //
position += point.X + "-" + (int) (point.X + w) + "," + point.Y + "-" + (int) (point.Y + h) + "|"; // 字点击范围
// 复原角度
AffineTransform transform2 = new AffineTransform();
transform.rotate(-Math.toRadians(degrees), ((int) point.X + (int) (w / 2)),
((int) point.Y + (int) (h / 2)));
graphics.setTransform(transform2);
// 移除已使用项,避免样式重复
colorArr.remove(color);
fontArr.remove(font);
posArr.remove(point);
}
// 将图片转换为base64
String bitmap = "";
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
ImageIO.write(image, "jpg", baos);
byte[] bytes = baos.toByteArray();
bitmap = Base64.getEncoder().encodeToString(bytes);
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (baos != null) {
baos.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
o.put("ValidText", validCode);
o.put("ValidPos", TrimEnd(position, "|"));
o.put("ValidImage", bitmap);
} catch (Exception ex) {
o.put("ValidText", "Error");
o.put("ValidPos", "");
o.put("ValidImage", ex.getMessage());
}
return o;
}
static Random random = new Random();
public static int Rand(int min, int max) {
int num = random.nextInt(max - min + 1) + min;
return num;
}
public static String TrimEnd(String inStr, String suffix) {
while (inStr.endsWith(suffix)) {
inStr = inStr.substring(0, inStr.length() - suffix.length());
}
return inStr;
}
public static Color HexToRGB(String str) {
str = str.toLowerCase();
final Matcher mx = Pattern.compile("^#([0-9a-z]{2})([0-9a-z]{2})([0-9a-z]{2})$").matcher(str);
if (!mx.find())
throw new IllegalArgumentException("invalid color value");
final int R = Integer.parseInt(mx.group(1), 16);
final int G = Integer.parseInt(mx.group(2), 16);
final int B = Integer.parseInt(mx.group(3), 16);
Color color = new Color(R, G, B);
return color;
}
public class PointF {
public int X;
public int Y;
public PointF(int x, int y) {
X = x;
Y = y;
}
}
}
2.调用层 :Controller
package com.cdrc.controller;
import com.cdrc.service.IService;
import com.cdrc.service.ValidateHelper;
import com.liuw.common.CustomCommon;
import com.liuw.web.UrlEx;
import org.json.JSONObject;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Hashtable;
@Controller
// 所有响应请求方法的父路径
@RequestMapping(value = {"/test/"})
public class TestController extends BaseController {
@Override
public IService getIService(String servletPath) {
return null;
}
@RequestMapping(value = {"/validate/demo"})
public ModelAndView validatedemo(HttpServletRequest request, HttpServletResponse response, @RequestBody(required = false) String bodyString) {
return super.doGetModel(request, response, bodyString);
}
@RequestMapping(value = {"/validate/ajax"})
public ModelAndView validateajax(HttpServletRequest request, HttpServletResponse response, @RequestBody(required = false) String bodyString) {
try {
String urlParams = null != bodyString ? bodyString : request.getQueryString();
String action = UrlEx.getQuery(urlParams, "action");
String result = "";
JSONObject o = new JSONObject();
switch (action) {
case "getimg":
String word = ValidateHelper.GetWord();
Hashtable tb = ValidateHelper.Create(word);
o.put("IsSuccess", true);
o.put("Body", "生成成功");
o.put("ValidText", tb.get("ValidText"));
o.put("ValidImage", tb.get("ValidImage"));
request.getSession().setAttribute("ValidPos", tb.get("ValidPos"));//将验证码坐标写入Session
result = o.toString();
break;
case "validate":
String code = UrlEx.getQuery(urlParams, "code");
Object validPos = request.getSession().getAttribute("ValidPos");
boolean res = ValidateHelper.Validate(code, String.valueOf(validPos));
o.put("IsSuccess", res);
o.put("Body", res ? "验证通过" : "抱歉,验证失败");
result = o.toString();
break;
}
com.liuw.web.ResponseEx.write(com.liuw.web.ContentTypeEnum.JSON, result, CustomCommon.CHARSET_DEFAULT);
return null;
} catch (Exception ex) {
return null;
}
}
}
3.前端页面参考C# ASP.NET MVC 版。
五、运行效果
六、在线示例
验证码测试