Behavioral verification code (idiom click) (C# version and Java version)

First, look at the renderings

2. Background introduction

There are quite a lot of graphic verification codes on the Internet, such as NetEase Yidun , Tencent Firewall , Alibaba Cloud verification codes , etc. For reference, I realized a simple idiom click mode.

3. Implementation ideas

1. Select several pictures (the size used here is 320x160), and randomly select one of them as the background picture.

2. Organize an idiom library and use it as the words in the verification code.

3. Draw the selected idiom randomly (random position, random font, random color) on the background image, record the coordinate range of each word, and later use it to verify whether the user chooses correctly.

4. Return the idioms and pictures to the front end.

5. After the front end clicks, the clicked coordinate point is sent back to the back end, and the back end performs verification. 

Fourth, implement the code

C# ASP.NET MVC version

1. The backend generates a verification code image

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. Call layer: 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. View: 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. File: 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. File: 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 Edition

1. The backend generates a verification code image

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. Call layer: 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. Refer to the C# ASP.NET MVC version for the front-end page.

5. Operation effect

6. Online example

 Captcha test

Guess you like

Origin blog.csdn.net/a497785609/article/details/131783853