核心思想
验证码的核心目的,是区分“人”和“机器”(如自动化脚本),其工作流程如下:
- 服务端生成:服务器生成一个随机字符串(验证码)。
- 服务端存储:服务器将这个随机字符串存入
HttpSession中。 - 服务端展示:服务器将这个随机字符串转换成一张图片(或其他形式,如短信、语音),并返回给浏览器。
- 用户输入:用户在页面上看到图片,并将图片中的字符输入到表单中。
- 用户提交:用户提交表单,表单中包含了用户输入的验证码。
- 服务端验证:服务器从
HttpSession中取出之前存储的验证码,与用户提交的验证码进行比较。- 如果一致,则验证通过,继续处理业务逻辑(如登录、注册)。
- 如果不一致,则验证失败,提示用户“验证码错误”。
关键点:验证码的生命周期是“一次性的”,一旦验证成功或失败,服务器就应该立即将 Session 中的验证码清除,防止同一个验证码被重复使用。
实现步骤(以 Servlet + JSP 为例)
我们将分步实现一个完整的登录验证码流程。
第1步:创建 Maven Web 项目
确保你的项目是一个 Maven Web 项目,并添加 javax.servlet-api 依赖。
<dependencies>
<!-- Servlet API -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>provided</scope>
</dependency>
</dependencies>
第2步:创建验证码生成工具类 VerifyCodeUtils.java
这个类负责生成随机字符串和将其绘制成图片。
import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Random;
public class VerifyCodeUtils {
// 使用 Alphanumeric 类型的字符
private static final char[] chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789".toCharArray();
private static final int WIDTH = 100;
private static final int HEIGHT = 40;
private static final int CODE_LENGTH = 4; // 验证码长度
private static final int LINES = 5; // 干扰线数量
/**
* 生成随机验证码字符串
* @return 验证码字符串
*/
public static String generateVerifyCode() {
Random random = new Random();
StringBuilder sb = new StringBuilder();
for (int i = 0; i < CODE_LENGTH; i++) {
sb.append(chars[random.nextInt(chars.length)]);
}
return sb.toString();
}
/**
* 生成验证码图片
* @param verifyCode 验证码字符串
* @param out 输出流
* @throws IOException
*/
public static void outputImage(String verifyCode, OutputStream out) throws IOException {
BufferedImage image = new BufferedImage(WIDTH, HEIGHT, BufferedImage.TYPE_INT_RGB);
Graphics2D g2d = image.createGraphics();
// 设置背景色
g2d.setColor(Color.WHITE);
g2d.fillRect(0, 0, WIDTH, HEIGHT);
// 设置字体
g2d.setFont(new Font("Arial", Font.BOLD, 20));
Random random = new Random();
// 绘制验证码
for (int i = 0; i < verifyCode.length(); i++) {
g2d.setColor(getRandomColor(random));
int x = 10 + i * 22;
int y = 25 + random.nextInt(5);
g2d.drawString(String.valueOf(verifyCode.charAt(i)), x, y);
}
// 绘制干扰线
for (int i = 0; i < LINES; i++) {
g2d.setColor(getRandomColor(random));
int x1 = random.nextInt(WIDTH);
int y1 = random.nextInt(HEIGHT);
int x2 = random.nextInt(WIDTH);
int y2 = random.nextInt(HEIGHT);
g2d.drawLine(x1, y1, x2, y2);
}
g2d.dispose();
ImageIO.write(image, "JPEG", out);
}
private static Color getRandomColor(Random random) {
int r = random.nextInt(256);
int g = random.nextInt(256);
int b = random.nextInt(256);
return new Color(r, g, b);
}
}
第3步:创建 Servlet 生成并发送验证码 (VerifyCodeServlet.java)
这个 Servlet 负责调用工具类生成验证码,并将其存入 Session,最后将图片响应给浏览器。
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("/verifyCode")
public class VerifyCodeServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 1. 生成验证码
String verifyCode = VerifyCodeUtils.generateVerifyCode();
// 2. 将验证码存入 Session
HttpSession session = req.getSession();
session.setAttribute("verifyCode", verifyCode);
// 3. 设置响应内容为图片
resp.setContentType("image/jpeg");
resp.setHeader("Pragma", "No-cache");
resp.setHeader("Cache-Control", "no-cache");
resp.setDateHeader("Expires", 0);
// 4. 将图片写给浏览器
VerifyCodeUtils.outputImage(verifyCode, resp.getOutputStream());
}
}
第4步:创建登录页面 (login.jsp)
这个 JSP 页面包含一个图片标签来显示验证码,以及一个刷新验证码的链接。
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>登录页面</title>
<script>
function refreshCode() {
// 获取img标签
var img = document.getElementById("verifyCodeImg");
// 加上时间戳,防止浏览器缓存图片
img.src = "verifyCode?" + new Date().getTime();
}
</script>
</head>
<body>
<h2>用户登录</h2>
<form action="login" method="post">
用户名: <input type="text" name="username"><br>
密码: <input type="password" name="password"><br>
验证码: <input type="text" name="verifyCodeInput">
<!--
src 指向我们创建的验证码 Servlet
onclick="refreshCode()" 点击图片时调用刷新函数
-->
<img id="verifyCodeImg" src="verifyCode" onclick="refreshCode()" title="点击刷新验证码"><br>
<input type="submit" value="登录">
</form>
<%-- 显示错误信息 --%>
<font color="red">${requestScope.errorMsg}</font>
</body>
</html>
第5步:创建处理登录请求的 Servlet (LoginServlet.java)
这个 Servlet 接收登录请求,并从 Session 中取出验证码进行比对。
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 {
req.setCharacterEncoding("UTF-8");
// 1. 获取用户输入
String username = req.getParameter("username");
String password = req.getParameter("password");
String userInputCode = req.getParameter("verifyCodeInput");
// 2. 从 Session 中获取正确的验证码
HttpSession session = req.getSession();
String sessionCode = (String) session.getAttribute("verifyCode");
// 3. 验证码校验(注意大小写,这里我们统一转成大写比较)
if (sessionCode == null || !sessionCode.equalsIgnoreCase(userInputCode)) {
// 验证失败,将错误信息存入 request,并转发回登录页
req.setAttribute("errorMsg", "验证码错误!");
req.getRequestDispatcher("login.jsp").forward(req, resp);
return; // 终止后续流程
}
// 4. 验证码正确,清除 Session 中的验证码(非常重要!)
session.removeAttribute("verifyCode");
// 5. 进行后续的业务逻辑(如用户名密码校验)
// ... 这里省略用户名密码校验逻辑 ...
System.out.println("验证码通过,正在进行用户名密码校验...");
// 假设校验成功
resp.setContentType("text/html;charset=UTF-8");
resp.getWriter().write("<h1>登录成功!欢迎您," + username + "</h1>");
}
}
关键点解析
-
session.setAttribute("verifyCode", verifyCode)- 这是核心步骤,我们将生成的验证码字符串作为值,
"verifyCode"作为键,存入HttpSession对象中。HttpSession是每个用户独有的,所以不同用户的验证码不会混淆。
- 这是核心步骤,我们将生成的验证码字符串作为值,
-
<img src="verifyCode">- 浏览器会向
VerifyCodeServlet发送一个 GET 请求,VerifyCodeServlet生成图片并返回,浏览器接收到二进制图片数据后,就会将其显示在<img>标签内。
- 浏览器会向
-
onclick="refreshCode()"和img.src = "verifyCode?" + new Date().getTime()为了防止浏览器缓存图片导致刷新无效,我们在请求 URL 后面加了一个时间戳,每次点击,URL 都会变化,浏览器就会重新请求服务器,生成新的验证码图片。
-
session.removeAttribute("verifyCode")- 这是非常关键的一步,在验证成功后,必须立即从
Session中移除验证码,这可以防止“验证码重放攻击”,即攻击者截获一次成功的请求后,重放该请求来绕过验证码。
- 这是非常关键的一步,在验证成功后,必须立即从
-
不区分大小写
- 人类用户有时会分不清大小写(如
O和0),为了提升用户体验,通常在验证时不区分大小写,我们可以将Session中的码和用户输入的码都转换成大写或小写后再比较。
- 人类用户有时会分不清大小写(如
总结与扩展
| 步骤 | 操作 | 相关代码/技术 |
|---|---|---|
| 生成 | 服务端创建随机字符串和图片 | VerifyCodeUtils, BufferedImage, Graphics2D |
| 存储 | 将字符串存入用户专属的 Session | session.setAttribute("verifyCode", code) |
| 展示 | 将图片响应给浏览器,<img> 标签显示 |
resp.setContentType("image/jpeg") |
| 输入 | 用户在表单中输入字符 | <input type="text" name="verifyCodeInput"> |
| 验证 | 从 Session 取出码与用户输入比对 | session.getAttribute("verifyCode") |
| 清理 | 验证后立即从 Session 移除码 | session.removeAttribute("verifyCode") |
扩展思考:
- 其他验证码类型:除了图片验证码,还有短信验证码、邮件验证码、语音验证码等,它们的原理类似,只是“展示”和“输入”的方式不同,短信验证码是存入 Session,然后通过短信网关发送到用户手机,用户在页面上输入手机号和收到的验证码进行验证。
- 使用第三方库:在实际项目中,可以使用更成熟的库来生成验证码,Google 的
Kaptcha,它提供了更多定制选项(如扭曲、噪点等),使用起来也更方便。 - 安全性:验证码不是万能的,它是防御自动化攻击的第一道防线,结合 IP 限制、请求频率限制(Rate Limiting)等手段,可以构建更强大的安全体系。
