JSP + JavaScript 实现验证码登录功能
在开发一个 Web 应用时,用户登录几乎是每个系统都绕不开的环节。而为了防止恶意程序暴力破解密码,加入图形验证码成了最基础、也最有效的防护手段之一。最近我在做 Java Web 练手项目时,就动手实现了一套基于 JSP + Servlet + JavaScript 的动态验证码登录系统。整个过程不仅加深了我对前后端协作的理解,也让我不再“畏惧”图像生成这类看似复杂的操作。
今天,我就把这套方案完整地梳理一遍——从后端如何用 Java 绘图 API 生成带干扰线的验证码图片,到前端如何通过点击刷新提升体验,再到登录时如何安全比对,一气呵成。
核心机制:一次请求,一张图,一个 Session
这个系统的精妙之处在于“轻量但完整”:
浏览器请求一张图片 → 后端动态绘制并输出 → 把真实值存进当前用户的 Session → 前端提交时取出比对。
整个流程不依赖数据库、也不需要额外存储服务,仅靠 HTTP 协议本身的会话机制就能完成验证闭环。而 JavaScript 的加入,则让用户体验更流畅:不用刷新页面,点一下验证码就能换新图。
验证码图像怎么来的?Java 也能“画画”
很多人以为生成图片是件很“重”的事,其实不然。Java 提供了 java.awt 和 javax.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 管理、前后端交互等多个知识点。看似简单,实则麻雀虽小五脏俱全。无论是毕业设计还是企业项目,这类模块都是极佳的练手机会。
下次当你看到登录页上的那张小图时,不妨多看一眼——说不定,它正悄悄守护着整个系统的安全入口。

2095


被折叠的 条评论
为什么被折叠?



