java + html 图片点击文字验证码

发布于:2025-08-16 ⋅ 阅读:(13) ⋅ 点赞:(0)

html资源文件下载地址:

链接:https://pan.baidu.com/s/1f5gaZ36XKYjsbtGpN3dyeg?pwd=2cx2
提取码:2cx2

效果图

在这里插入图片描述

需要代码

后台java

1.创建CreateVerifyTextModel


```java
 	import java.util.List;

/**
 * @description 创建文字和坐标返回对象
 * @author liuhongrong
 * @since 2025/7/21 15:37
 * @desc
 */
public class CreateVerifyTextModel {

    /**
     * 汉字
     */
    private String verifyText;

    /**
     * 汉字坐标
     */
    private List<String> codeList;


    public String getVerifyText() {
        return verifyText;
    }

    public void setVerifyText(String verifyText) {
        this.verifyText = verifyText;
    }

    public List<String> getCodeList() {
        return codeList;
    }

    public void setCodeList(List<String> codeList) {
        this.codeList = codeList;
    }
}

2.创建GraphicsUtil 图像背景和汉字

import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletRequest;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.Random;

/**
 * @description 生成 图像
 * @author liuhongrong
 * @since 2025/7/21 14:19
 * @desc
 */
public class GraphicsUtil {

    /**
     * 生成背景图片
     * @return
     */
    public static BufferedImage getBackGround(HttpServletRequest request){
        // 读取本地图片
        String realPath=request.getServletContext().getRealPath("/cms");
        // 1. 生成 1-6 的随机数
        Random random = new Random();
        int randomNumber = random.nextInt(9) + 1; // 1 <= randomNumber <= 9
        File inputFile = new File(realPath+"/vscode/imaegs/"+randomNumber+".png");
        BufferedImage image = null;
        try {
            image = ImageIO.read(inputFile);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        return image;
    }



    public static Color getRandColor(int s,int e){
        Random random = new Random();
        if(s>255)s = 255;
        if(e>255)e = 255;
        int r = s+random.nextInt(e-s);
        int g = s+random.nextInt(e-s);
        int b = s+random.nextInt(e-s);
        return new Color(r, g, b);
    }


    /**
     * 图片 线条色
     * @param random
     * @param fc
     * @param bc
     * @return
     */
    public static Color getRandColor(Random random, int fc, int bc){
        if (fc > 255)
            fc = 255;
        if (bc > 255)
            bc = 255;
        int r = fc + random.nextInt(bc - fc);
        int g = fc + random.nextInt(bc - fc);
        int b = fc + random.nextInt(bc - fc);
        return new Color(r, g, b);
    }


    /**
     * 创建顶部图片,提示验证文字
     */
    public static BufferedImage  headVerifyText(BufferedImage image,String verifyText){
        // 创建顶部图片
        BufferedImage bi = new BufferedImage(image.getWidth(), 25, BufferedImage.TYPE_INT_RGB);
        Graphics gra = bi.getGraphics();
        // 设置背景颜色
//        gra.setColor(Color.WHITE);
        gra.setColor(Color.LIGHT_GRAY);
        // 填充区域
        gra.fillRect(0, 0, bi.getWidth(), bi.getHeight());
        // 设置边框颜色
        gra.setColor(Color.lightGray);
        // 设置边框区域
        gra.drawRect(1, 1, bi.getWidth() - 2, bi.getHeight() - 2);
        // 设置文字背景颜色
        Font font = new Font("Microsoft YaHei", Font.BOLD, 16);
        gra.setFont(font);
        gra.setColor(Color.BLACK);
        gra.drawString("请依次点击:" + verifyText, (bi.getWidth() - 10*font.getSize())/2,
                bi.getHeight()/2 + font.getSize()/2);//设置文字字体 与位子  居中

        return bi;
    }



    /**
     * 生成验证汉字
     * @return
     */
    public static String getRandomChineseChar() {
        String str = null;
        int hs, ls;
        Random random = new Random();
        hs = (176 + Math.abs(random.nextInt(39)));
        ls = (161 + Math.abs(random.nextInt(93)));
        byte[] b = new byte[2];
        b[0] = (new Integer(hs).byteValue());
        b[1] = (new Integer(ls).byteValue());
        try  {
            str = new String(b, "GBK"); //转成中文
        } catch (UnsupportedEncodingException ex) {
            ex.printStackTrace();
        }
        return str;
    }

3.创建CreateVerifyText 创建验证的汉字坐标

import java.awt.*;
import java.awt.image.BufferedImage;
import java.util.List;
import java.util.*;

/**
 * @description 创建验证的汉字坐标
 * @author liuhongrong
 * @since 2025/7/21 15:31
 * @desc
 */
public class CreateVerifyText {

    //生成汉字的个数
    private static Integer[] arr = new Integer[] {1, 2, 3, 4, 5};

    //汉字颜色随机范围
    private static Color[] colors = {Color.ORANGE, Color.GREEN, Color.CYAN};

    public static CreateVerifyTextModel create(BufferedImage image){
        //生成汉字
        StringBuilder verifyText= new StringBuilder();
        List<String> codeList = new ArrayList<String>();
        int hight = image.getHeight();
        Random random = new Random();
        //转成集合
        List<Integer> intList = Arrays.asList(arr);
        //重新随机排序
        Collections.shuffle(intList);
        int x = 0;
        int y = 0;
        //定义随机1到arr.length某一个字不参与校验
        int num = random.nextInt(arr.length)+1;
        for (int i = 0; i < arr.length; i++) { // 5个汉字,只点4个
            String ch = GraphicsUtil.getRandomChineseChar();
            int place = intList.get(i);
            if (place == 1) {
                x = new Random().nextInt(30) + 40; // 自己定义的位子坐标
                y = new Random().nextInt(30) + 40; // i=1的时候,y的值
            }
            if (place == 2) {
                x = new Random().nextInt(40) + 120; // 自己定义的位子坐标
                y = new Random().nextInt(30) + 50; // i=2的时候,y的值
            }
            if (place == 3) {
                x = new Random().nextInt(70) + 200; // 自己定义的位子坐标
                y = new Random().nextInt(50) + 100; // i=3的时候,y的值
            }
            if (place == 4) {
                x = new Random().nextInt(70) + 80; // i=4的时候,x的值
                y = new Random().nextInt(30) + 90; // 自己定义的位子坐标
            }
            if (place == 5) {
                x = new Random().nextInt(70) + 180; // i=4的时候,x的值
                y = new Random().nextInt(30) + 50; // 自己定义的位子坐标
            }

            System.out.println("x:" + x + ",y:" + y + ",hight:" + hight);
            Graphics graphics = image.getGraphics();
            // 设置颜色
            graphics.setColor(Color.red);
            graphics.setFont(new Font("宋体", Font.BOLD, 30));
            //字体颜色
            graphics.setColor(colors[random.nextInt(colors.length)]);
            graphics.drawString(ch, x, y);
            if (place != num) {
                verifyText.append(ch);
                codeList.add(x + "_" + y); // jsp页面坐标原点在字的中间,drawString方法坐标原点在中间
            }
        }
        //返回对象
        CreateVerifyTextModel textmodel=new CreateVerifyTextModel();
        textmodel.setVerifyText(verifyText.toString());
        textmodel.setCodeList(codeList);
        return textmodel;
    }
}

4.创建 VerificationCodeAction

import com.zkjw.core.modules.verificationCode.CreateVerifyText;
import com.zkjw.core.modules.verificationCode.CreateVerifyTextModel;
import com.zkjw.core.modules.verificationCode.GraphicsUtil;
import org.apache.commons.lang.StringUtils;
import org.springframework.web.servlet.mvc.multiaction.MultiActionController;
import javax.imageio.ImageIO;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * @description  创建-验证码和校验-验证码是否正确
 * @author liuhongrong
 * @since 2025/7/21 14:10
 * @desc
 */
public class VerificationCodeAction extends MultiActionController {


    private static final String KEY = "randomCode";

    //定义点选文字图片验证码允许的误差值
    private static final int ERROR_AMOUNT = 12;// 定义允许的误差值,单位是px


    /**生成验证码
     */
    public void getVerificationCode(HttpServletRequest request,HttpServletResponse response) {
        ServletOutputStream outStream = null;
        try {
            //生成背景图片
            BufferedImage image = GraphicsUtil.getBackGround(request);
            //生成文字坐标
            CreateVerifyTextModel textmodel= CreateVerifyText.create(image);
            String verifyText= textmodel.getVerifyText();
            List<String> codeList = textmodel.getCodeList();
            System.out.println("需要点击的汉字:" + verifyText);
            //放入session
            //将产生的随机汉字验证码存进session中进行保存
            String sessionid = request.getSession().getId();
            request.getSession().setAttribute(sessionid + KEY, codeList);
            //增加验证码请求次数
//            RedisValidImgCodeUtils.increment(KEY_TOTAL, CACHE_SECONDS);
            // 可以将图片合并传入前端  也可以直接传数据汉字给前端
            BufferedImage bi=GraphicsUtil.headVerifyText(image,verifyText);
            BufferedImage combined = new BufferedImage(image.getWidth(), image.getHeight() + bi.getHeight(), BufferedImage.TYPE_INT_RGB);
            Graphics graphicsH = combined.getGraphics();      //合并图片
            //生成提示放在上边-------------------------------------
//            graphicsH.drawImage(bi, 0, 0, null);
//            graphicsH.drawImage(image, 0, bi.getHeight(), null);
            //生成提示放在下边-------------------------------------
            graphicsH.drawImage(image, 0, 0, null);
            graphicsH.drawImage(bi, 0, image.getHeight(), null);
            outStream=  response.getOutputStream();
            ImageIO.write(combined, "jpg", outStream);
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try {
                if (outStream != null) {
                    outStream.close();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 验证验证码
     * @throws IOException
     */
    public void verifycode(HttpServletRequest request,HttpServletResponse response) throws IOException {
        //返回信息
        Map<String, Object> reultMap=new HashMap<>();
        reultMap.put("status", 0);
        reultMap.put("msg", "验证失败");
        String value = CMSUtil.base16ToStr(request.getParameter("code"));
        String sessionid = request.getSession().getId();
        Object  verifyList= request.getSession().getAttribute(sessionid + KEY);
        if (verifyList == null) {
            responseWriter(reultMap, response);
            return;
        }
        List<String> sValue = (List<String>) verifyList;
        //取到数据后直接清掉redis
        request.getSession().removeAttribute(sessionid + KEY);
        System.out.println("**前端请求数据***"+value);
        System.out.println("**后端实际数据**"+sValue);
        //为null 或者"" 或者 " "
        if (StringUtils.isBlank(value) || sValue == null || sValue.size() < 1) {
            responseWriter(reultMap, response);
            return;
        }

        String [] valueStr = value.split(",");
        if(valueStr.length != sValue.size() || valueStr.length != 4){
            responseWriter(reultMap, response);
            return;
        }

        /*判断坐标参数是否正确*/
        String str = "";
        for (int i = 0; i < valueStr.length; i++) {
            str = valueStr[i].toString();
            if(StringUtils.isBlank(str) || StringUtils.isBlank(sValue.get(i).toString())){
                responseWriter(reultMap, response);
                return;
            }
            String [] vL = valueStr[i].toString().split("_");
            String [] svL = sValue.get(i).toString().split("_");
            if(vL.length != svL.length || svL.length != 2){
                responseWriter(reultMap, response);
                return;
            }
            //x轴  y轴判断    坐标点在左上角 ,图片宽度30px  点击范围扩大12px,  范围在      x-13 < x <x+13  ;
            if(!(Integer.parseInt(svL[0])-ERROR_AMOUNT < Integer.parseInt(vL[0])+15 && Integer.parseInt(vL[0])-+15 < Integer.parseInt(svL[0])+ERROR_AMOUNT )
                    || !(Integer.parseInt(svL[1])-ERROR_AMOUNT < Integer.parseInt(vL[1])+15 && Integer.parseInt(vL[1])+15 < Integer.parseInt(svL[1])+ERROR_AMOUNT)){
                //增加验证失败次数
//                RedisValidImgCodeUtils.increment(KEY_FAIL, CACHE_SECONDS);
                responseWriter(reultMap, response);
                return;
            }
        }
        //增加验证通过次数
//        RedisValidImgCodeUtils.increment(KEY_SUCC, CACHE_SECONDS);
        reultMap.put("status", 1);
        reultMap.put("msg", "验证成功");
        responseWriter(reultMap, response);
    }

    /**
     * 返回信息
     */
    private void responseWriter(Map<String, Object> reultMap,HttpServletResponse response){
        try {
            response.setCharacterEncoding("UTF-8");
            response.getWriter().write(JsonUtil.jsonObjectToString(reultMap));
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

前台Html

1.Html页面

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <meta name="viewport" content="width=device-width,initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no"/>
    <title>验证码</title>
    <script type="text/javascript">var appPath="/"</script>
    <link href="css/vscode.css" type="text/css" rel="stylesheet"/>
</head>
<body>
<div class="verify-box">
    <div class="verify-btn" onclick="openCaptchaPopup()">
        <span class="verify_tips__icon">点此进行验证</span>
    </div>
</div>
<!-- 弹窗容器 -->
<div class="popup-overlay" id="captchaPopup">
    <div class="popup-content">
        <!-- 原有验证码内容 -->
        <div style="text-align:center;position:relative;">
            <div style="position:relative;display:inline-block;">
                <!-- 验证码图片 -->
                <img id="codeT3" src="/VerificationCodeAction.do?m=getVerificationCode&t=123456789" />
                <div class="verify-controls">
                    <!-- 刷新图标 -->
                    <button  class="verify_refresh" onclick="getCodeTree();" ></button>
                    <%--关闭--%>
                    <button  class="verify_colse" onclick="closeCaptchaPopup();" ></button>
                </div>
            </div>
            </br>
            <select id="codeSelect" style="display: none;"></select>
        </div>
    </div>
</div>
<script src="js/jquery-1.7.js" type="text/javascript"></script>
<script src="js/vscode.js" type="text/javascript"></script>
</body>
</html>

2.js页面

//点击次数
var number=0;

// 打开验证码弹窗
function openCaptchaPopup() {
    document.getElementById('captchaPopup').classList.add('show-popup');
}

// 关闭验证码弹窗
function closeCaptchaPopup() {
    number = 0;
    $(".text-point").remove();
    document.getElementById('captchaPopup').classList.remove('show-popup');
}

//获取验证码3
function getCodeTree() {
    number = 0;
    $(".text-point").remove();
    document.getElementById("codeSelect").options.length=0;
    var timestamp = new Date().getTime();
    $("#codeT3").attr("src",appPath+"/VerificationCodeAction.do?m=getVerificationCode&t=" + timestamp);
}

$(function() {
    $("#codeT3").bind("click", function(ev) {
        var oEvent = ev || event;
        //var number = $("#codeSelect option").length;
        number++;
        if (number > 4) {
            return;
        }
        var x = oEvent.pageX;
        var y = oEvent.pageY;
        var img = document.getElementById('codeT3');    //获取图片的原点
        var nodex = getNodePosition(img)[0];//原点x 与原点y
        var nodey = getNodePosition(img)[1];
        var xserver = parseInt(x) - parseInt(nodex);
        var yserver = parseInt(y) - parseInt(nodey);
        $("#codeSelect").append(
            "<option value='"+ (parseInt(number)+1) +"'>" + xserver + "_" + yserver
            + "</option>");
        var oDiv = document.createElement('img');
        oDiv.style.left = (parseInt(x)-15) + 'px'; // 指定创建的DIV在文档中距离左侧的位置    图片大小30 左右移动5
        oDiv.style.top = (parseInt(y) -15) + 'px'; // 指定创建的DIV在文档中距离顶部的位置
        oDiv.className = 'text-point';//加class 点刷新后删除遮罩
        document.body.appendChild(oDiv);
        //第四次点击后自动提交
        if (number == 4) {
            cheakOutTree();
        }
    });

})

//校验验证码
function cheakOutTree() {
    var txt = "";
    $("#codeSelect option").each(function (){
        var text = $(this).text();
        if(txt == ""){
            txt = text;
        }else{
            txt = txt + "," + text;
        }
    });
    $.ajax({
        type:"post",
        url:appPath+"/VerificationCodeAction.do?m=verifycode",
        data : {"code" : base16Str(txt) },
        cache : false,
        success : function(data) {
            var jsonObj = JSON.parse(data);
            layer.msg(jsonObj.msg);
            if (jsonObj.status == 0) {
                getCodeTree();
            }else{

            }
        }
    });
}

function getNodePosition(node) {
    var top = left = 0;
    while (node) {
        if (node.tagName) {
            top = top + node.offsetTop;
            left = left + node.offsetLeft;
            node = node.offsetParent;
        }
        else {
            node = node.parentNode;
        }
    }
    return [left, top];
}

3.css页面

.verify-btn {
    padding: 6px 12px;
    border: 1px solid #ccc;
    background-color: #f5f5f5;
    cursor: pointer;
    border-radius: 4px;
    text-align: center; /* 文字水平居中 */
    margin-left: 8px;
}

.verify_tips__icon{
    display: block;
    margin: 0 auto;
    width: 155px;
    height: 30px;
    line-height: 30px;
    vertical-align: middle;
    background: url(../../vscode/imaegs/icon_verify.png) no-repeat left;
    background-position: 0 -370px;
}


.verify-btn:hover {
    background-color: #e9e9e9;
}
/* 弹窗遮罩层样式 */
.popup-overlay {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background-color: rgba(0, 0, 0, 0.5);
    display: none;
    justify-content: center;
    align-items: center;
    z-index: 999;
}

/* 弹窗内容样式 */
.popup-content {
    background-color: white;
    padding: 20px;
    border-radius: 8px;
    box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}

/* 显示弹窗的类 */
.show-popup {
    display: flex;
}

/* 触发弹窗的按钮样式 */
.open-popup-btn {
    padding: 8px 16px;
    background-color: #4285f4;
    color: white;
    border: none;
    border-radius: 4px;
    cursor: pointer;
    margin: 20px;
}
.open-popup-btn:hover {
    background-color: #3367d6;
}

/* 透明背景容器样式 - 优化为完全透明 */
.verify-controls {
    position: absolute;
    top: 0;
    right: 0;
    background-color: transparent; /* 完全透明背景 */
    padding: 5px;
    z-index: 10; /* 确保在图片上方显示 */
    display: flex; /* 使用flex布局确保按钮并排显示 */
    gap: 2px; /* 按钮之间的间距 */
}
/* 刷新按钮样式 - 优化悬停效果 */
.verify_refresh{
    width: 30px;
    height: 30px;
    cursor: pointer;
    background-image: url(../../vscode/imaegs/icon_verify.png);
    background-position: 0 -680px;
    border: none;
    background-repeat: no-repeat;
    margin: 0;
    padding: 0;
    transition: transform 0.2s; /* 轻微缩放动画 */
    background-color: rgba(255, 255, 255, 0.2); /* 悬停时显示轻微背景 */
}
/* 关闭按钮样式 - 优化悬停效果 */
.verify_colse{
    width: 30px;
    height: 30px;
    cursor: pointer;
    background-image: url(../../vscode/imaegs/icon_verify.png);
    background-position:4px -526px;;
    border: none;
    background-repeat: no-repeat;
    margin: 0;
    padding: 0;
    transition: transform 0.2s; /* 轻微缩放动画 */
    background-color: rgba(255, 255, 255, 0.2); /* 悬停时显示轻微背景 */
}
/* 按钮悬停效果 */
.verify_refresh:hover, .verify_colse:hover {
    transform: scale(1.1); /* 悬停时轻微放大 */
    background-color: rgba(255, 255, 255, 0.2); /* 悬停时显示轻微背景 */
    border-radius: 3px; /* 圆角效果 */
}
.text-point{
    border:2px solid #FF0000;
    position:absolute;
    width: 30px;
    height: 30px;
    opacity: 0.7;
    border-radius:6px;
    box-shadow: 0 0 3px rgba(255, 82, 82, 0.2);
    z-index:1000;
}

网站公告

今日签到

点亮在社区的每一天
去签到