杰瑞科技汇

Java session验证码如何实现与校验?

核心概念:为什么需要 Session + 验证码?

  1. Session (会话):

    Java session验证码如何实现与校验?-图1
    (图片来源网络,侵删)
    • 是什么:Session 是服务器为了保存特定用户的状态而创建的一种对象,当用户访问服务器时,服务器会为该用户创建一个唯一的 Session ID,并将其发送给浏览器(通常通过 Cookie 保存),之后该用户每次发送请求时,浏览器都会带上这个 Session ID,服务器就能通过这个 ID 找到对应的 Session 对象,从而获取或保存该用户的数据。
    • 作用:在验证码场景下,我们可以将生成的验证码文本存入 Session 中,当用户提交表单时,我们从 Session 中取出这个文本,与用户输入的文本进行比对。
  2. 验证码:

    • 是什么:一种区分用户是计算机还是人的公共全自动程序,它可以防止恶意破解密码、刷票、论坛灌水等。
    • 作用:强制进行“人机交互”,确保当前操作是由真实用户在浏览器前完成的。
  3. 为什么两者结合?

    • 验证码的生成:服务器生成一张包含随机字符串的图片,并将这个字符串存入 Session。
    • 验证码的校验:用户在页面上看到图片,输入字符串,提交表单,服务器接收到用户输入后,从 Session 中取出之前存储的字符串,进行比对。
    • 安全性:验证码字符串不直接暴露在请求参数中,而是存储在服务器端的 Session 中,这比单纯在前端或通过隐藏域传递要安全得多。

实现步骤

我们将创建一个简单的 Java Web 项目,包含以下功能:

  1. 一个登录页面,包含用户名、密码、验证码输入框和验证码图片。
  2. 一个 Servlet 用于生成验证码图片,并将验证码文本存入 Session。
  3. 一个 Servlet 用于处理登录请求,并校验验证码。

准备工作

你需要一个 Java Web 开发环境(如 IntelliJ IDEA + Tomcat)以及一个 Java 图形库,我们这里使用最经典的 Java BufferedImage API,无需额外依赖。

Java session验证码如何实现与校验?-图2
(图片来源网络,侵删)

具体代码实现

项目结构

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)

这个页面包含表单和用于显示验证码的 <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)

这个 Servlet 接收请求,生成验证码,存入 Session,并将图片响应给浏览器。

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)

这个 Servlet 接收登录表单数据,并校验验证码。

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。

Java session验证码如何实现与校验?-图3
(图片来源网络,侵删)
<?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>

进阶与最佳实践

  1. 防止表单重复提交

    • 问题:用户点击登录按钮后,如果网络慢,他可能会多次点击,导致多次提交表单。
    • 解决方案:在验证码验证通过后,将一个标志(如 session.setAttribute("captchaVerified", "true"))存入 Session,在处理登录请求时,先检查这个标志,如果为 true,则处理登录,并立即将标志移除或设为 false,如果为 false,则直接忽略请求或提示“请不要重复提交”。
  2. 验证码时效性

    • 问题:生成的验证码应该在一小段时间后失效,否则用户长时间不操作,验证码图片还在,但 Session 里的验证码已经过时了。
    • 解决方案:可以设置 Session 的超时时间(在 web.xml 中配置 <session-config>),或者在生成验证码时记录一个时间戳,校验时判断时间差是否在有效期内。
  3. 使用第三方库

    • 手动实现验证码虽然能学到原理,但在实际项目中,推荐使用成熟的库,如:
      • Google reCAPTCHA: 最流行的验证服务,提供 "我是机器人" 的勾选框或图片点击验证。
      • Kaptcha: 一个流行的 Java 验证码生成库,配置简单,样式丰富。
      • JCaptcha: 另一个功能强大的 Java 验证码库。
  4. 更高安全性:滑动验证/点选验证

    • 传统字符型验证码容易被 OCR(光学字符识别)技术破解。
    • 现在更流行的是滑动验证(拖动滑块拼合图片)或点选验证(按顺序点击图片中的文字/物体),这些通常由第三方服务(如 reCAPTCHA, 极验)提供,交互体验更好,安全性也更高。

通过以上步骤,我们实现了一个完整的 Session + 验证码登录流程:

  1. 用户访问 index.html,页面加载,<img> 标签的 src 指向 /captcha
  2. 浏览器请求 /captchaCaptchaServlet 被调用。
  3. CaptchaServlet 生成随机码,存入 session.setAttribute("captchaCode", ...),然后将图片返回给浏览器显示。
  4. 用户输入用户名、密码和看到的验证码,点击提交,表单发送到 /login
  5. LoginServlet 被调用,获取 request.getParameter("captcha")session.getAttribute("captchaCode")
  6. 两者比对,如果一致,则继续验证用户名密码;如果不一致,则提示验证码错误并返回登录页。

这个流程是 Web 开发中保障表单安全的基础,务必理解其核心思想。

分享:
扫描分享到社交APP
上一篇
下一篇