核心概念:为什么需要 Session + 验证码?
-
Session (会话):
(图片来源网络,侵删)- 是什么:Session 是服务器为了保存特定用户的状态而创建的一种对象,当用户访问服务器时,服务器会为该用户创建一个唯一的 Session ID,并将其发送给浏览器(通常通过 Cookie 保存),之后该用户每次发送请求时,浏览器都会带上这个 Session ID,服务器就能通过这个 ID 找到对应的 Session 对象,从而获取或保存该用户的数据。
- 作用:在验证码场景下,我们可以将生成的验证码文本存入 Session 中,当用户提交表单时,我们从 Session 中取出这个文本,与用户输入的文本进行比对。
-
验证码:
- 是什么:一种区分用户是计算机还是人的公共全自动程序,它可以防止恶意破解密码、刷票、论坛灌水等。
- 作用:强制进行“人机交互”,确保当前操作是由真实用户在浏览器前完成的。
-
为什么两者结合?
- 验证码的生成:服务器生成一张包含随机字符串的图片,并将这个字符串存入 Session。
- 验证码的校验:用户在页面上看到图片,输入字符串,提交表单,服务器接收到用户输入后,从 Session 中取出之前存储的字符串,进行比对。
- 安全性:验证码字符串不直接暴露在请求参数中,而是存储在服务器端的 Session 中,这比单纯在前端或通过隐藏域传递要安全得多。
实现步骤
我们将创建一个简单的 Java Web 项目,包含以下功能:
- 一个登录页面,包含用户名、密码、验证码输入框和验证码图片。
- 一个 Servlet 用于生成验证码图片,并将验证码文本存入 Session。
- 一个 Servlet 用于处理登录请求,并校验验证码。
准备工作
你需要一个 Java Web 开发环境(如 IntelliJ IDEA + Tomcat)以及一个 Java 图形库,我们这里使用最经典的 Java BufferedImage API,无需额外依赖。

具体代码实现
项目结构
java-session-captcha/
├── src/
│ └── main/
│ ├── java/
│ │ └── com/
│ │ └── example/
│ │ └── CaptchaDemo/
│ │ ├── utils/
│ │ │ └── CaptchaUtil.java // 验证码生成工具类
│ │ ├── CaptchaServlet.java // 生成验证码图片的Servlet
│ │ ├── LoginServlet.java // 处理登录请求的Servlet
│ │ └── LoginServlet.java // 处理登录请求的Servlet
│ └── webapp/
│ ├── WEB-INF/
│ │ └── web.xml
│ ├── index.html // 登录页面
│ └── images/ (空文件夹)
└── pom.xml // Maven 项目文件
登录页面 (index.html)
这个页面包含表单和用于显示验证码的 这个类负责生成图片和随机文本。 这个 Servlet 接收请求,生成验证码,存入 Session,并将图片响应给浏览器。 这个 Servlet 接收登录表单数据,并校验验证码。 如果你不使用 防止表单重复提交 验证码时效性 使用第三方库 更高安全性:滑动验证/点选验证 通过以上步骤,我们实现了一个完整的 Session + 验证码登录流程: 这个流程是 Web 开发中保障表单安全的基础,务必理解其核心思想。<img>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">登录页面</title>
<style>
body { font-family: Arial, sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background-color: #f4f4f4; }
.login-box { padding: 20px; background: white; border-radius: 8px; box-shadow: 0 0 10px rgba(0,0,0,0.1); }
.form-group { margin-bottom: 15px; }
label { display: block; margin-bottom: 5px; }
input[type="text"], input[type="password"] { width: 200px; padding: 8px; border: 1px solid #ddd; border-radius: 4px; }
.captcha-container { display: flex; align-items: center; gap: 10px; }
#captchaImage { border: 1px solid #ccc; cursor: pointer; }
input[type="submit"] { width: 100%; padding: 10px; background-color: #5cb85c; color: white; border: none; border-radius: 4px; cursor: pointer; }
input[type="submit"]:hover { background-color: #4cae4c; }
</style>
</head>
<body>
<div class="login-box">
<h2>用户登录</h2>
<form action="login" method="post">
<div class="form-group">
<label for="username">用户名:</label>
<input type="text" id="username" name="username" required>
</div>
<div class="form-group">
<label for="password">密码:</label>
<input type="password" id="password" name="password" required>
</div>
<div class="form-group">
<label for="captcha">验证码:</label>
<div class="captcha-container">
<!-- 显示验证码图片 -->
<img id="captchaImage" src="captcha" alt="验证码" title="点击刷新">
<!-- 隐藏的请求参数,防止 CSRF (可选但推荐) -->
<input type="hidden" name="csrfToken" value="${sessionScope.csrfToken}">
</div>
</div>
<input type="submit" value="登录">
</form>
</div>
<script>
// 点击验证码图片可以刷新
document.getElementById('captchaImage').addEventListener('click', function() {
// 在图片的 src 后面添加一个时间戳,防止浏览器缓存
this.src = 'captcha?' + new Date().getTime();
});
</script>
</body>
</html>
验证码生成工具类 (
CaptchaUtil.java)package com.example.CaptchaDemo.utils;
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.Random;
public class CaptchaUtil {
private static final int WIDTH = 120;
private static final int HEIGHT = 40;
private static final int CODE_LENGTH = 4; // 验证码长度
private static final String CHAR_POOL = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789"; // 去除易混淆字符
/**
* 生成验证码文本
* @return 随机字符串
*/
public static String generateCode() {
Random random = new Random();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < CODE_LENGTH; i++) {
sb.append(CHAR_POOL.charAt(random.nextInt(CHAR_POOL.length())));
}
return sb.toString();
}
/**
* 生成验证码图片
* @param code 验证码文本
* @return 图片的字节数组
*/
public static byte[] generateImage(String code) {
BufferedImage image = new BufferedImage(WIDTH, HEIGHT, BufferedImage.TYPE_INT_RGB);
Graphics2D g2d = image.createGraphics();
// 设置抗锯齿
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
// 背景
g2d.setColor(Color.WHITE);
g2d.fillRect(0, 0, WIDTH, HEIGHT);
// 边框
g2d.setColor(Color.BLACK);
g2d.drawRect(0, 0, WIDTH - 1, HEIGHT - 1);
// 绘制干扰线
drawRandomLines(g2d);
// 绘制验证码文本
drawRandomCode(g2d, code);
g2d.dispose();
// 将图片转换为字节数组
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
ImageIO.write(image, "JPEG", baos);
} catch (IOException e) {
e.printStackTrace();
}
return baos.toByteArray();
}
private static void drawRandomLines(Graphics2D g2d) {
Random random = new Random();
for (int i = 0; i < 5; i++) {
int x1 = random.nextInt(WIDTH);
int y1 = random.nextInt(HEIGHT);
int x2 = random.nextInt(WIDTH);
int y2 = random.nextInt(HEIGHT);
g2d.setColor(getRandomColor());
g2d.drawLine(x1, y1, x2, y2);
}
}
private static void drawRandomCode(Graphics2D g2d, String code) {
Random random = new Random();
int fontSize = HEIGHT - 10;
Font font = new Font("Arial", Font.BOLD, fontSize);
g2d.setFont(font);
for (int i = 0; i < code.length(); i++) {
char c = code.charAt(i);
int x = 5 + i * (WIDTH / CODE_LENGTH);
int y = HEIGHT / 2 + random.nextInt(10) - 5;
int angle = random.nextInt(30) - 15; // -15到15度
g2d.setColor(getRandomColor());
g2d.translate(x, y);
g2d.rotate(Math.toRadians(angle));
g2d.drawString(String.valueOf(c), 0, 0);
g2d.rotate(-Math.toRadians(angle));
g2d.translate(-x, -y);
}
}
private static Color getRandomColor() {
Random random = new Random();
return new Color(random.nextInt(256), random.nextInt(256), random.nextInt(256));
}
}
生成验证码的 Servlet (
CaptchaServlet.java)package com.example.CaptchaDemo;
import com.example.CaptchaDemo.utils.CaptchaUtil;
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("/captcha")
public class CaptchaServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 1. 生成验证码文本
String captchaCode = CaptchaUtil.generateCode();
// 2. 将验证码存入 Session
HttpSession session = req.getSession();
session.setAttribute("captchaCode", captchaCode);
// 为了防止并发问题,可以设置一个标识
session.setAttribute("captchaVerified", "false");
// 3. 生成验证码图片
byte[] imageBytes = CaptchaUtil.generateImage(captchaCode);
// 4. 设置响应头,告诉浏览器这是一张图片
resp.setContentType("image/jpeg");
resp.setHeader("Pragma", "No-cache");
resp.setHeader("Cache-Control", "no-cache");
resp.setDateHeader("Expires", 0);
// 5. 将图片字节数组写入响应输出流
resp.getOutputStream().write(imageBytes);
}
}
处理登录的 Servlet (
LoginServlet.java)package com.example.CaptchaDemo;
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("/login")
public class LoginServlet extends HttpServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 1. 设置请求编码
req.setCharacterEncoding("UTF-8");
resp.setContentType("text/html;charset=UTF-8");
// 2. 获取用户输入
String username = req.getParameter("username");
String password = req.getParameter("password");
String userInputCaptcha = req.getParameter("captcha"); // 注意,这里获取的是用户输入的,不是Session里的
// 3. 从 Session 中获取正确的验证码
HttpSession session = req.getSession();
String sessionCaptcha = (String) session.getAttribute("captchaCode");
// 4. 校验验证码 (不区分大小写)
if (sessionCaptcha == null || !sessionCaptcha.equalsIgnoreCase(userInputCaptcha)) {
// 验证码错误
// 1. 清除 Session 中的验证码,防止重复使用
session.removeAttribute("captchaCode");
// 2. 返回错误信息并跳转回登录页
resp.getWriter().println("<script>alert('验证码错误!'); window.location.href='index.html';</script>");
return;
}
// 5. 验证码正确,继续校验用户名和密码 (这里只是示例,没有连接数据库)
if ("admin".equals(username) && "password".equals(password)) {
// 登录成功
// 标记验证码已验证,防止表单重复提交
session.setAttribute("captchaVerified", "true");
resp.getWriter().println("<h1>欢迎, " + username + "!</h1>");
} else {
// 用户名或密码错误
resp.getWriter().println("<script>alert('用户名或密码错误!'); window.location.href='index.html';</script>");
}
}
}
web.xml 配置 (如果使用注解则可省略)@WebServlet 注解,就需要在 web.xml 中配置这两个 Servlet。
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<servlet>
<servlet-name>CaptchaServlet</servlet-name>
<servlet-class>com.example.CaptchaDemo.CaptchaServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>CaptchaServlet</servlet-name>
<url-pattern>/captcha</url-pattern>
</servlet-mapping>
<servlet>
<servlet-name>LoginServlet</servlet-name>
<servlet-class>com.example.CaptchaDemo.LoginServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>LoginServlet</servlet-name>
<url-pattern>/login</url-pattern>
</servlet-mapping>
</web-app>
进阶与最佳实践
session.setAttribute("captchaVerified", "true"))存入 Session,在处理登录请求时,先检查这个标志,如果为 true,则处理登录,并立即将标志移除或设为 false,如果为 false,则直接忽略请求或提示“请不要重复提交”。
web.xml 中配置 <session-config>),或者在生成验证码时记录一个时间戳,校验时判断时间差是否在有效期内。
index.html,页面加载,<img> 标签的 src 指向 /captcha。/captcha,CaptchaServlet 被调用。CaptchaServlet 生成随机码,存入 session.setAttribute("captchaCode", ...),然后将图片返回给浏览器显示。/login。LoginServlet 被调用,获取 request.getParameter("captcha") 和 session.getAttribute("captchaCode")。
