JSP+JavaScript 实现验证码登录功能

JSP + JavaScript 实现验证码登录功能

在开发一个 Web 应用时,用户登录几乎是每个系统都绕不开的环节。而为了防止恶意程序暴力破解密码,加入图形验证码成了最基础、也最有效的防护手段之一。最近我在做 Java Web 练手项目时,就动手实现了一套基于 JSP + Servlet + JavaScript 的动态验证码登录系统。整个过程不仅加深了我对前后端协作的理解,也让我不再“畏惧”图像生成这类看似复杂的操作。

今天,我就把这套方案完整地梳理一遍——从后端如何用 Java 绘图 API 生成带干扰线的验证码图片,到前端如何通过点击刷新提升体验,再到登录时如何安全比对,一气呵成。


核心机制:一次请求,一张图,一个 Session

这个系统的精妙之处在于“轻量但完整”:
浏览器请求一张图片 → 后端动态绘制并输出 → 把真实值存进当前用户的 Session → 前端提交时取出比对。

整个流程不依赖数据库、也不需要额外存储服务,仅靠 HTTP 协议本身的会话机制就能完成验证闭环。而 JavaScript 的加入,则让用户体验更流畅:不用刷新页面,点一下验证码就能换新图。


验证码图像怎么来的?Java 也能“画画”

很多人以为生成图片是件很“重”的事,其实不然。Java 提供了 java.awtjavax.imageio 这类原生 API,完全可以像画板一样在内存中绘图。

我们先封装一个核心工具类:ValidateCode.java。它负责创建 BufferedImage 对象,并在其上绘制背景、干扰线和随机字符。

package org.lanqiao.util;

import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.OutputStream;
import java.util.Random;

public class ValidateCode {
    private int width = 120;
    private int height = 40;
    private int codeCount = 4;
    private int lineCount = 50;
    private String code;
    private BufferedImage buffImg;

    private char[] codeSequence = {
        'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J',
        'K', 'L', 'M', 'N', 'P', 'Q', 'R', 'S', 'T', 'U',
        'V', 'W', 'X', 'Y', 'Z', '1', '2', '3', '4', '5',
        '6', '7', '8', '9'
    };

    public ValidateCode() {
        this.createCode();
    }

    public ValidateCode(int width, int height) {
        this.width = width;
        this.height = height;
        this.createCode();
    }

    public ValidateCode(int width, int height, int codeCount, int lineCount) {
        this.width = width;
        this.height = height;
        this.codeCount = codeCount;
        this.lineCount = lineCount;
        this.createCode();
    }

    private void createCode() {
        int x = width / (codeCount + 2);
        int fontHeight = height - 2;
        int codeY = height - 4;

        Random random = new Random();
        buffImg = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
        Graphics2D g = buffImg.createGraphics();

        // 白色背景
        g.setColor(Color.WHITE);
        g.fillRect(0, 0, width, height);

        // 抗锯齿
        g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);

        // 干扰线
        for (int i = 0; i < lineCount; i++) {
            int xs = random.nextInt(width);
            int ys = random.nextInt(height);
            int xe = xs + random.nextInt(width / 8);
            int ye = ys + random.nextInt(height / 8);
            int red = random.nextInt(255);
            int green = random.nextInt(255);
            int blue = random.nextInt(255);
            g.setColor(new Color(red, green, blue));
            g.drawLine(xs, ys, xe, ye);
        }

        // 生成并绘制验证码字符
        StringBuilder randomCode = new StringBuilder();
        for (int i = 0; i < codeCount; i++) {
            String strRand = String.valueOf(codeSequence[random.nextInt(codeSequence.length)]);
            int red = random.nextInt(255);
            int green = random.nextInt(255);
            int blue = random.nextInt(255);
            g.setColor(new Color(red, green, blue));
            g.drawString(strRand, (i + 1) * x, codeY);
            randomCode.append(strRand);
        }
        code = randomCode.toString();
    }

    public void write(OutputStream output) throws Exception {
        ImageIO.write(buffImg, "png", output);
    }

    public BufferedImage getBuffImg() {
        return buffImg;
    }

    public String getCode() {
        return code;
    }
}

可以看到,这段代码并没有什么高深的技术,就是一步步“画”出来:先铺底色,再画杂乱的彩色线条作为干扰,最后一个个写出扭曲感十足的字符。如果你希望更难识别,还可以加上旋转、波浪变形等效果,不过对于一般场景来说,这样已经足够防住大多数自动化脚本了。


字体也能内嵌?避免部署依赖的小技巧

为了让验证码看起来更有设计感,而不是默认的宋体或 Arial,我们可以加载自定义字体(比如一种手写风格的 .ttf 文件)。但直接引用外部文件会导致部署麻烦——万一服务器没装这个字体呢?

解决方案是:把字体文件转成字节数组,硬编码进 Java 类里。

下面这个 ImgFontByte.java 就干了这件事:

package org.lanqiao.util;

import java.awt.Font;
import java.io.ByteArrayInputStream;

public class ImgFontByte {
    public Font getFont(int fontHeight) {
        try {
            Font baseFont = Font.createFont(Font.TRUETYPE_FONT, new ByteArrayInputStream(hex2byte(getFontByteStr())));
            return baseFont.deriveFont(Font.PLAIN, fontHeight);
        } catch (Exception e) {
            return new Font("Arial", Font.PLAIN, fontHeight);
        }
    }

    private byte[] hex2byte(String str) {
        if (str == null || str.length() % 2 != 0) return null;
        byte[] b = new byte[str.length() / 2];
        for (int i = 0; i < str.length(); i += 2)
            b[i / 2] = (byte) Integer.decode("0x" + str.substring(i, i + 2)).intValue();
        return b;
    }

    private String getFontByteStr() {
        return "0001000000100040000400c04f532f327d8175d4000087740000005650434c5461e3d9fb000087cc...";
    }
}

虽然这一长串十六进制字符串看着吓人,但它其实就是某个 .ttf 文件的二进制内容转换而来。只要你的 IDE 能编译过去,字体就能正常加载。当然,如果只是练习用途,也可以直接去掉这部分逻辑,使用系统默认字体即可。


图片接口怎么暴露?用 Servlet 接管请求

前端要显示验证码图片,自然要用 <img src="...">。那么这个 src 指向哪里?答案是一个专门处理图像输出的 Servlet。

我们定义一个 ValidateCodeServlet.java,映射路径为 /vCode

package org.lanqiao.util;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;

@WebServlet("/vCode")
public class ValidateCodeServlet extends HttpServlet {
    private static final long serialVersionUID = 1L;

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {

        response.setContentType("image/jpeg");
        response.setHeader("Pragma", "no-cache");
        response.setHeader("Cache-Control", "no-cache");
        response.setDateHeader("Expires", 0);

        HttpSession session = request.getSession();

        ValidateCode vCode = new ValidateCode(120, 40, 4, 50);
        session.setAttribute("code", vCode.getCode().toLowerCase());

        vCode.write(response.getOutputStream());
    }
}

关键点有三个:
1. 设置响应类型为 image/jpeg,告诉浏览器这是张图;
2. 禁用缓存,确保每次请求都能拿到新图;
3. 把生成的真实验证码小写化后存入 Session,后续登录验证时取出来比对。

这样一来,只要前端访问 /vCode,就会得到一张全新的验证码图片,同时服务端也记住了它的内容。


前端交互怎么做?一行 JS 实现“点击换图”

现在后端准备好了,接下来是前端。我们在 login.jsp 中嵌入图片,并绑定点击事件。

<%@ page language="java" contentType="text/html; charset=UTF-8"
         pageEncoding="UTF-8"%>
<%
    String basePath = request.getScheme() + "://" +
                      request.getServerName() + ":" +
                      request.getServerPort() +
                      request.getContextPath() + "/";
%>

<!DOCTYPE html>
<html>
<head>
    <base href="<%=basePath%>">
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <title>用户登录</title>
    <script type="text/javascript">
        window.onload = function () {
            var img = document.getElementById("vCodeImg");
            img.onclick = function () {
                this.src = "vCode?date=" + new Date();
            };
        };
    </script>
    <style>
        body { font-family: Arial, sans-serif; padding: 50px; text-align: center; }
        .form-group { margin: 15px 0; }
        input[type=text], input[type=password] {
            padding: 8px; width: 200px; border: 1px solid #ccc; border-radius: 4px;
        }
        input[type=submit] {
            padding: 10px 20px; background-color: #007bff; color: white; border: none; cursor: pointer;
        }
        input[type=submit]:hover { background-color: #0056b3; }
    </style>
</head>
<body>
<h2>用户登录系统</h2>
<form action="UserServlet/userVerify" method="post">
    <div class="form-group">
        用户名:<input type="text" name="name" required><br>
    </div>
    <div class="form-group">
        密码:<input type="password" name="pwd" required><br>
    </div>
    <div class="form-group">
        是否保存密码:<input type="checkbox" name="save" value="true"><br>
    </div>
    <div class="form-group">
        验证码:<input type="text" name="verifyCode" required><br>
        <img src="vCode" id="vCodeImg" alt="验证码" style="cursor:pointer;" title="点击换一张">
    </div>
    <div class="form-group">
        <input type="submit" value="登录">
    </div>
</form>
</body>
</html>

注意这里的 JavaScript 逻辑:
每次点击图片,就把 src 修改为 vCode?date=时间戳。由于 URL 变了,浏览器就不会使用缓存,从而触发新的请求,返回新验证码。

这种“加参数防缓存”的做法非常常见,尤其适用于动态资源。


登录怎么验证?Session 比对是关键

最后一步,当用户填写完信息点击登录时,我们需要在后端进行校验。

package org.lanqiao.web;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.io.IOException;
import java.io.PrintWriter;

@WebServlet("/UserServlet/*")
public class UserServlet extends HttpServlet {
    private static final long serialVersionUID = 1L;

    protected void doPost(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {

        request.setCharacterEncoding("UTF-8");
        response.setContentType("text/html;charset=utf-8");

        PrintWriter out = response.getWriter();
        HttpSession session = request.getSession();

        String username = request.getParameter("name");
        String password = request.getParameter("pwd");
        String inputCode = request.getParameter("verifyCode");
        String realCode = (String) session.getAttribute("code");

        if (realCode == null || !realCode.equalsIgnoreCase(inputCode)) {
            out.println("<h3 style='color:red;'>验证码错误,请重试!</h3>");
            out.println("<a href='login.jsp'>返回登录页</a>");
            return;
        }

        // 模拟用户名密码验证
        if ("admin".equals(username) && "123456".equals(password)) {
            out.println("<h3 style='color:green;'>登录成功!欢迎回来:" + username + "</h3>");
        } else {
            out.println("<h3 style='color:red;'>用户名或密码错误!</h3>");
        }
    }
}

这里特别要注意的是:验证码只能用一次。一旦比对失败或成功,最好立即将 Session 中的 code 清除或更新,防止被重复利用。否则攻击者可能截获一次有效验证码后反复尝试。


实际运行与调试建议

部署这套系统时,记得检查以下几点:

  • 所有 Java 类是否放入正确的包路径?
  • 是否使用支持注解的 Tomcat 版本(如 7+)?若否,需手动配置 web.xml
  • 访问 login.jsp 时,打开浏览器开发者工具,查看 /vCode 请求是否返回 200 状态码且 Content-Type 是 image
  • 输入错误验证码时,是否提示正确?输入正确后能否进入下一步?

一个小坑:有时候你会发现验证码明明输对了却提示错误——很可能是大小写问题。所以建议统一转为小写比较,或者直接限制输入框只能输入大写。


可以怎么改进?几个实用方向

功能改进建议
验证码过期设置 Session 超时时间(如 5 分钟),或引入 Redis 存储并设置 TTL
区分大小写前端用 input.toUpperCase() 强制转换,后端同理处理
中文验证码替换 codeSequence 为汉字数组,并确保字体支持中文编码
移动端适配使用 Base64 编码将图片嵌入 HTML,减少请求数
更强安全性加入滑动拼图、算术题等交互式验证方式

写在最后:基础功永远值得打磨

如今 AI 已经能生成逼真的图像、语音甚至视频,各种“无感验证”技术也在兴起。但作为一名开发者,我始终认为:理解底层原理,掌握基本功,才是应对变化的根本。

就像这个小小的验证码系统,它涉及了 Servlet 生命周期、图像处理、Session 管理、前后端交互等多个知识点。看似简单,实则麻雀虽小五脏俱全。无论是毕业设计还是企业项目,这类模块都是极佳的练手机会。

下次当你看到登录页上的那张小图时,不妨多看一眼——说不定,它正悄悄守护着整个系统的安全入口。

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值