使用JSP+JavaScript實(shí)現(xiàn)驗(yàn)證碼登錄功能
引言
在現(xiàn)代 Web 應(yīng)用中,用戶身份驗(yàn)證是安全體系的第一道防線。然而,單純依賴用戶名和密碼的登錄機(jī)制早已不堪一擊——自動(dòng)化腳本、暴力 破解、爬蟲攻擊層出不窮。一個(gè)看似簡(jiǎn)單的“驗(yàn)證碼”功能,實(shí)則承載著抵御機(jī)器流量的重要職責(zé)。
最近我在重構(gòu)一個(gè)傳統(tǒng) Java Web 項(xiàng)目時(shí),重新審視了這一經(jīng)典安全組件的設(shè)計(jì)與實(shí)現(xiàn)。今天就來(lái)分享如何使用 JSP + Servlet + JavaScript 技術(shù)棧,從零搭建一個(gè)結(jié)構(gòu)清晰、交互流暢、具備基礎(chǔ)防攻擊能力的驗(yàn)證碼登錄系統(tǒng)。
整個(gè)過(guò)程并不復(fù)雜,但涉及前后端協(xié)作、會(huì)話管理、圖像生成等多個(gè)關(guān)鍵點(diǎn)。我們將一步步完成:動(dòng)態(tài)驗(yàn)證碼圖片生成 → 前端展示與刷新 → 表單提交 → 后臺(tái)校驗(yàn)全流程,并在最后探討其可擴(kuò)展性與未來(lái)演進(jìn)方向。
動(dòng)態(tài)驗(yàn)證碼是如何工作的?
驗(yàn)證碼的核心邏輯其實(shí)很樸素:服務(wù)器生成一段隨機(jī)字符,繪制成干擾性強(qiáng)的圖片供人識(shí)別,同時(shí)將原始值存儲(chǔ)在當(dāng)前用戶的 Session 中;當(dāng)用戶輸入后,后臺(tái)比對(duì)輸入值與 Session 中的記錄是否一致。
這個(gè)過(guò)程中有幾個(gè)關(guān)鍵設(shè)計(jì)必須注意:
- 防止緩存:瀏覽器不能緩存驗(yàn)證碼圖片,否則點(diǎn)擊刷新無(wú)效
- 時(shí)效控制:驗(yàn)證碼不應(yīng)永久有效,通常設(shè)置為60秒過(guò)期
- 一次消費(fèi):成功驗(yàn)證后應(yīng)立即清除,防止重放攻擊
- 大小寫處理:建議統(tǒng)一轉(zhuǎn)小寫或大寫進(jìn)行比較,提升用戶體驗(yàn)
接下來(lái)我們就通過(guò)三個(gè)核心模塊來(lái)落地這套機(jī)制。
驗(yàn)證碼圖像生成(Servlet)
我們創(chuàng)建一個(gè) VerifyCodeServlet,專門負(fù)責(zé)生成并輸出 JPEG 圖像。它被綁定到 /captcha 路徑,前端只需 <img src="captcha"> 即可獲取最新驗(yàn)證碼。
package com.example.captcha;
import javax.imageio.ImageIO;
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.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.Random;
@WebServlet("/captcha")
public class VerifyCodeServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
private int width = 100;
private int height = 40;
private int codeCount = 4;
private int lineCount = 50;
@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);
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics2D g = image.createGraphics();
g.setColor(Color.WHITE);
g.fillRect(0, 0, width, height);
g.setColor(Color.GRAY);
g.drawRect(0, 0, width - 1, height - 1);
Font font = new Font("Arial", Font.BOLD | Font.ITALIC, 28);
g.setFont(font);
char[] chars = { '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' };
Random random = new Random();
StringBuilder codeBuilder = new StringBuilder();
// 繪制干擾線
g.setColor(Color.LIGHT_GRAY);
for (int i = 0; i < lineCount; i++) {
int x1 = random.nextInt(width);
int y1 = random.nextInt(height);
int x2 = random.nextInt(width);
int y2 = random.nextInt(height);
g.drawLine(x1, y1, x2, y2);
}
// 繪制字符
for (int i = 0; i < codeCount; i++) {
String c = String.valueOf(chars[random.nextInt(chars.length)]);
codeBuilder.append(c);
g.setColor(new Color(random.nextInt(100), random.nextInt(100), random.nextInt(100)));
g.drawString(c, 20 + i * 18, 28);
}
HttpSession session = request.getSession();
session.setAttribute("verify_code", codeBuilder.toString().toLowerCase());
ImageIO.write(image, "jpeg", response.getOutputStream());
g.dispose();
}
}
這段代碼有幾個(gè)值得注意的細(xì)節(jié):
- 使用
@WebServlet注解簡(jiǎn)化部署配置; - 設(shè)置 HTTP 頭禁止緩存,確保每次請(qǐng)求都能拿到新圖;
- 干擾線數(shù)量適中(50條),避免影響正常識(shí)別;
- 字符集排除了易混淆的
O/0和I/l/1,降低用戶輸錯(cuò)概率; - 顏色隨機(jī)化增強(qiáng) OCR 難度,但不過(guò)度花哨以免影響可讀性;
- 最終驗(yàn)證碼轉(zhuǎn)為小寫存入 Session,方便后續(xù)忽略大小寫校驗(yàn)。
小技巧:如果你希望支持中文提示詞渲染,可以考慮引入 TTF 字體文件并調(diào)用 g.setFont() 加載自定義字體,這對(duì)國(guó)際化項(xiàng)目尤其有用。
登錄頁(yè)面設(shè)計(jì)(Login.jsp)
前端頁(yè)面采用純 JSP 編寫,結(jié)合內(nèi)聯(lián) CSS 和輕量 JS 實(shí)現(xiàn)良好視覺(jué)效果與交互體驗(yàn)。
<%@ 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%>" rel="external nofollow" >
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>用戶登錄</title>
<style>
body {
font-family: Arial, sans-serif;
background-color: #f4f6f9;
padding: 50px;
text-align: center;
}
.login-box {
width: 400px;
margin: 0 auto;
background: white;
border-radius: 10px;
padding: 30px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
}
input[type="text"], input[type="password"] {
width: 100%;
padding: 10px;
margin: 10px 0;
border: 1px solid #ccc;
border-radius: 5px;
box-sizing: border-box;
}
img {
cursor: pointer;
border: 1px solid #ddd;
border-radius: 5px;
margin-top: 10px;
}
input[type="submit"] {
background-color: #007bff;
color: white;
padding: 12px 30px;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 16px;
}
input[type="submit"]:hover {
background-color: #0056b3;
}
</style>
</head>
<body>
<div class="login-box">
<h2>用戶登錄</h2>
<form action="userLogin" method="post">
<label>用戶名:</label>
<input type="text" name="username" required>
<label>密碼:</label>
<input type="password" name="password" required>
<label>驗(yàn)證碼:</label>
<input type="text" name="verifyCode" maxlength="4" required>
<img src="captcha" id="captchaImg" alt="驗(yàn)證碼">
<small>點(diǎn)擊圖片更換驗(yàn)證碼</small>
<input type="submit" value="登錄">
</form>
</div>
<script type="text/javascript">
window.onload = function () {
const img = document.getElementById("captchaImg");
img.onclick = function () {
this.src = "captcha?timestamp=" + new Date().getTime();
};
};
</script>
</body>
</html>
這里的關(guān)鍵在于驗(yàn)證碼圖片的動(dòng)態(tài)加載機(jī)制:
- 初始
<img src="captcha">請(qǐng)求生成第一張圖; - 用戶點(diǎn)擊圖片時(shí),JS 修改
src屬性,附加時(shí)間戳參數(shù)(如?timestamp=171234567890); - 由于 URL 發(fā)生變化,瀏覽器不會(huì)使用緩存,從而觸發(fā) Servlet 重新生成新驗(yàn)證碼。
這種做法簡(jiǎn)單高效,無(wú)需額外 AJAX 請(qǐng)求或 DOM 操作,非常適合輕量級(jí)場(chǎng)景。
登錄邏輯處理(UserLoginServlet.java)
最后是登錄表單的接收與驗(yàn)證邏輯:
package com.example.captcha;
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("/userLogin")
public class UserLoginServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
@Override
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("username");
String password = request.getParameter("password");
String userInput = request.getParameter("verifyCode").toLowerCase();
String realCode = (String) session.getAttribute("verify_code");
if (realCode == null || !realCode.equals(userInput)) {
out.println("<script>alert('驗(yàn)證碼錯(cuò)誤!'); history.back();</script>");
return;
}
if ("admin".equals(username) && "123456".equals(password)) {
out.println("<h2 style='color:green;'>? 登錄成功!歡迎回來(lái)," + username + "</h2>");
session.removeAttribute("verify_code"); // 防止重復(fù)使用
} else {
out.println("<script>alert('用戶名或密碼錯(cuò)誤!'); history.back();</script>");
}
}
}
幾點(diǎn)說(shuō)明:
- 輸入值統(tǒng)一轉(zhuǎn)為小寫再比對(duì),避免因大小寫導(dǎo)致失?。?/li>
- 校驗(yàn)失敗直接彈窗并返回上一頁(yè),保持上下文;
- 成功登錄后主動(dòng)移除 Session 中的驗(yàn)證碼,杜絕二次利用;
- 當(dāng)前示例使用硬編碼賬戶,實(shí)際項(xiàng)目中應(yīng)連接數(shù)據(jù)庫(kù)查詢用戶信息。
安全提醒:生產(chǎn)環(huán)境中應(yīng)使用加密存儲(chǔ)密碼(如 BCrypt),并通過(guò) PreparedStatements 防止 SQL 注入。
可行的優(yōu)化方向
雖然上述方案已能滿足基本需求,但在真實(shí)項(xiàng)目中還可以進(jìn)一步增強(qiáng):
| 優(yōu)化項(xiàng) | 實(shí)現(xiàn)方式 |
|---|---|
| 添加噪點(diǎn) | 在圖像上繪制隨機(jī)像素點(diǎn),增加 OCR 識(shí)別難度 |
| 設(shè)置過(guò)期時(shí)間 | 在 web.xml 中設(shè)置 Session 超時(shí)(如 <session-config><timeout>1</timeout></session-config>) |
| AJAX 提交 | 改用異步提交,避免整頁(yè)刷新,提升 UX |
| 返回 JSON | 后端返回結(jié)構(gòu)化數(shù)據(jù),便于前后端分離架構(gòu)集成 |
| 滑動(dòng)/點(diǎn)選驗(yàn)證碼 | 引入更復(fù)雜的交互式驗(yàn)證機(jī)制,適合高安全要求場(chǎng)景 |
例如,你可以將當(dāng)前的同步表單提交改為 Fetch API 異步請(qǐng)求:
document.querySelector('form').addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData(e.target);
const res = await fetch('userLogin', {
method: 'POST',
body: formData
});
const html = await res.text();
document.body.innerHTML = html;
});
配合后端返回 JSON 響應(yīng),即可實(shí)現(xiàn)無(wú)刷新登錄體驗(yàn)。
寫在最后:傳統(tǒng)驗(yàn)證碼的未來(lái)在哪里?
我們今天實(shí)現(xiàn)的是典型的“圖像+文本”型驗(yàn)證碼,屬于 CAPTCHA 的初級(jí)形態(tài)。隨著 AI 視覺(jué)技術(shù)的發(fā)展,這類簡(jiǎn)單干擾圖像正變得越來(lái)越容易被破解。
但反過(guò)來(lái)看,AI 也在催生新一代智能驗(yàn)證方式。比如阿里 recently 開(kāi)源的 Z-Image-ComfyUI 模型,具備強(qiáng)大的文生圖能力和語(yǔ)義理解水平,完全可以用來(lái)構(gòu)建“智能圖形挑戰(zhàn)”:
- “請(qǐng)選出包含雨傘的圖片”
- “根據(jù)描述生成指定圖案”
- “判斷兩幅圖是否表達(dá)相同含義”
這些基于語(yǔ)義理解的任務(wù),機(jī)器難以批量偽造,而人類卻能輕松應(yīng)對(duì)。未來(lái)的驗(yàn)證碼可能不再是“你是不是人”,而是“你有沒(méi)有人類的理解力”。
當(dāng)然,在絕大多數(shù)普通業(yè)務(wù)系統(tǒng)中,傳統(tǒng)的字符驗(yàn)證碼依然夠用且成本低廉。只要合理設(shè)計(jì)、及時(shí)更新策略,它仍然是性價(jià)比極高的安全防護(hù)手段。
如果你正在做一個(gè)學(xué)生管理系統(tǒng)、內(nèi)部辦公平臺(tái)或小型 CMS,本文這套 JSP + Servlet 方案完全足夠快速上線;
如果你關(guān)注前沿趨勢(shì),不妨嘗試將 Z-Image 這類 AIGC 工具接入驗(yàn)證流程,探索下一代人機(jī)識(shí)別的可能性。
技術(shù)沒(méi)有高低之分,只有適不適合。守住底線安全,同時(shí)保持對(duì)未來(lái)的敏感,這才是開(kāi)發(fā)者應(yīng)有的姿態(tài)。
以上就是使用JSP+JavaScript實(shí)現(xiàn)驗(yàn)證碼登錄功能的詳細(xì)內(nèi)容,更多關(guān)于JSP JavaScript驗(yàn)證碼登錄的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
一文詳解JavaScript中如何阻止元素的默認(rèn)行為
在網(wǎng)頁(yè)開(kāi)發(fā)中,許多用戶操作會(huì)觸發(fā)瀏覽器的默認(rèn)行為,例如,當(dāng)用戶點(diǎn)擊一個(gè)超鏈接時(shí),瀏覽器會(huì)導(dǎo)航到鏈接的目標(biāo)頁(yè)面,為了提升用戶體驗(yàn)或控制頁(yè)面行為,我們通常需要阻止這些默認(rèn)行為,所以本文我們將探討如何在JavaScript中阻止元素的默認(rèn)行為,需要的朋友可以參考下2025-06-06
JS基于onclick事件實(shí)現(xiàn)單個(gè)按鈕的編輯與保存功能示例
這篇文章主要介紹了JS基于onclick事件實(shí)現(xiàn)單個(gè)按鈕的編輯與保存功能,結(jié)合實(shí)例形式分析了JS實(shí)現(xiàn)onclick響應(yīng)事件的轉(zhuǎn)換相關(guān)操作技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2017-02-02
實(shí)現(xiàn)高性能javascript的注意事項(xiàng)
JavaScript代碼在web應(yīng)用程序中經(jīng)常用到,但是很多開(kāi)發(fā)者忽視了一些性能方面的知識(shí),如何編寫高性能javascript代碼呢?接下來(lái),小編跟大家一起學(xué)習(xí)2019-05-05
微信小程序?qū)崿F(xiàn)獲取用戶信息替換用戶名和頭像到首頁(yè)
本文詳細(xì)講解了微信小程序?qū)崿F(xiàn)獲取用戶信息替換用戶名和頭像到首頁(yè)的方法,文中通過(guò)示例代碼介紹的非常詳細(xì)。對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-06-06
js實(shí)現(xiàn)仿阿里巴巴城市選擇框效果實(shí)例
這篇文章主要介紹了js實(shí)現(xiàn)仿阿里巴巴城市選擇框效果,實(shí)例分析了javascript結(jié)合css與數(shù)組實(shí)現(xiàn)城市選擇框的方法,需要的朋友可以參考下2015-06-06

