行为式验证码(成语点选)(C#版和Java版)

OpenCV-Python 263次浏览 本站

一、先看效果图

二、背景介绍

图形验证码网上有挺多,比如:网易易盾腾讯防水墙阿里云验证码等等。参考了一下,自己实现了一个简单的成语点选的模式。

三、实现思路

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.Framework
{
    public class ValidateHelper
    {
        private static readonly Random Random = new Random();

        #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 = pos[0].ToInt();
                        var y = pos[1].ToInt();

                        //坐标范围
                        var xcore = score[0].Split(new[] { "-" }, StringSplitOptions.RemoveEmptyEntries);
                        var ycore = score[1].Split(new[] { "-" }, StringSplitOptions.RemoveEmptyEntries);
                        if (xcore.Length == 2 && x >= xcore[0].ToInt() && x < xcore[1].ToInt() && ycore.Length == 2 &&
                            y >= ycore[0].ToInt() && y < ycore[1].ToInt())
                            passed++;
                    }
                }

            return passed == inputPointDic.Count;
        }

        #endregion

        #region 随机获取成语验证码

        public static string GetWord()
        {
            var source =
                "心旷神怡|心平气和|十年寒窗|孙康映雪|埋头苦干|勤学苦练|发奋图强|前功尽废|艰苦卓绝|坚苦卓绝|勤学苦练|同德一心|节俭力行|幼学壮行|急起直追|奋勇向前|志坚行苦|咬紧牙关|映雪读书|并心同力|分秒必争|身体力行|逆水行舟|学如登山|废寝忘食|朝夕不倦|发愤图强|躬体力行|不辞辛苦|学而不厌|开足马力|听命由天|自强不息|穿壁引光|力争上游|得失在人|惊人之举|尽心竭力|刻苦耐劳|凿壁偷光|旗开得胜|一分为二|当仁不让|力争上游|干劲冲天|奋发图强|争先恐后|四平八稳|分秒必争|一马当先|自告奋勇|踊跃争先|";
            source +=
                "杯弓蛇影|鹤立鸡群|画蛇添足|生龙活虎|指鹿为马|雕虫小技|鸡毛蒜皮|千军万马|万马奔腾|泥牛入海|气象万千|马到成功|叶公好龙|藏龙卧虎|成帮结队|凤毛麟角|弱肉强食|对牛弹琴|狡兔三窟|井底之蛙|龙飞凤舞|车水马龙|虎头蛇尾|狼吞虎咽|黔驴技穷|一箭双雕|好生之德|包罗万象|惊弓之鸟|盲人摸象|一马当先|塞翁失马|含沙射影|万象更新|普渡众生|白驹过隙|打草惊蛇|管中窥豹|守株待兔|青梅竹马|骑虎难下|画龙点睛|亡羊补牢|";
            var arr = source.Split(new[] { "|" }, StringSplitOptions.RemoveEmptyEntries);

            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>();

            //第1步:随机取一张背景图
            var bg = GetMapPath("~/Content/image/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,iVBORw0KGgoAAAANSUhEUgAAACgAAAXuCAMAAAAuqZpRAAADAFBMVEVMaXELCw8PDxMPDxcUFBQmKCoNDREZkfoRERT///8NDREPEBUNDRMMDBEMDBEODhMNDROfpK8Xjfn///8LCxEKCg8HBwz///8MDBANDRFnbXP+/v7////////////8/Pzz8/MPDxH////Jy81jZGaipav5+flgYWX//v13qrPZ2dn+/v7//vye0f3U1NQek/qbn6MTjvry8vL//v6oqauTlJd0dXk4nvv////m5ub9/f2ZnqX+/v3Ex836+voMDA+tra31eXmsrKz19/h7fH/8/P3////8/Pwmps9jtfyy0v3t7e2eoqfS09UOjPrP0NL////+//+2t7qDg4YomPrd6/+bnaL////09PTQ1tft7e1FSU3///6Sy/3///++wshiaHT//PmaoKn8+/uLi42/wcTHyMq4ubvLz9Px8fGhqrPk5OQYj/qRlp6srKytra2hpar//v319faTlpvp6emrrbBnbXHd3d1nbXN5v/3n5+ggyK6oqq7Ly82/xMve3t7//vz///9CpfuVnaawtr56tvuVnabw8PCVnab///+rrK2srKzX19fW2Nu5vMC3vcSoqqyzucGfpKqr1/17hIzwZ2e5vMD9/f3p6elmbHO7wMjAw8bEyM6VnaZasPv7+/truf3h4eL///9Eu8f7+PbQ6P6ts7tSzLqrsrr39/iHxv3//v7///9Np/vwZ2efpK/u7u5FSU21u8N7hIyrrK28vsHD4v7Q0tXw9//h4eH3e3uVnaZzeoHj4+MYkPoYkPqnrrfm8f6nrbNnbXW83P4YkPqVnaausrml1P0XauUYkPqwtr6u2P2gpbBSzLoYkPp1eHtorfvJ3/32enp7hIwWauViaXS3vMRqcHcXi/iZxfwgyK7wZ2d7hYyVnabb7f9QyrlSzLkqybGKvvymzPwYkPr1enpRzLpFSU1FSU1jaXQgyK6ss7z///+fpK+hqrOsrKz1enpjaXTwZ2cgyK5SzLpnbXPO5vyVy/sglPonl/pEpft4vvtfsvtWrfs0nvoZf7Z+AAAA7XRSTlMACQgEAgEPzAPgDQcTFxwgFb8pzREaBl8jC8zxmYEgtcsZkmsoH+YxPwaF2RDods0Qy7IYYDYh0YuO1UWoCcor32C/2ELkv8EN2uVvKVLK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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, // 刷新回调事件
        onClose: 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.options = $.extend(clickCaptcha.DEFAULTS, options);
                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();

                if ($.isFunction(that.options.onClose)) {
                    that.options.onClose.call(that.$element);
                }
            });

        }
    };

    _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 ($.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();
        }
    }

    _proto.showTip = function (flag, text) {
        var that = this;
        var parentDom = that.options.mode == 'pop' ? $("body .valid_popup") : that.$element;
        if (flag) {
            if (text == undefined) { text = "验证成功" }
            parentDom.find(".valid_tips__text").html(text);
            parentDom.find(".valid_contain").addClass("valid--success");
        } else {
            if (text == undefined) { text = "验证失败,请重试" }
            parentDom.find(".valid_tips__text").html(text);
            parentDom.find(".valid_contain").addClass("valid--error");
        }
    }

    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 版。

五、运行效果

六、在线示例

验证码测试

发表评论

电子邮件地址不会被公开。 必填项已用*标注