使用JSP+JavaScript實(shí)現(xiàn)驗(yàn)證碼登錄功能
引言
在現(xiàn)代 Web 應(yīng)用中,用戶身份驗(yàn)證是安全體系的第一道防線。然而,單純依賴用戶名和密碼的登錄機(jī)制早已不堪一擊——自動化腳本、暴力 破解、爬蟲攻擊層出不窮。一個(gè)看似簡單的“驗(yàn)證碼”功能,實(shí)則承載著抵御機(jī)器流量的重要職責(zé)。
最近我在重構(gòu)一個(gè)傳統(tǒng) Java Web 項(xiàng)目時(shí),重新審視了這一經(jīng)典安全組件的設(shè)計(jì)與實(shí)現(xiàn)。今天就來分享如何使用 JSP + Servlet + JavaScript 技術(shù)棧,從零搭建一個(gè)結(jié)構(gòu)清晰、交互流暢、具備基礎(chǔ)防攻擊能力的驗(yàn)證碼登錄系統(tǒng)。
整個(gè)過程并不復(fù)雜,但涉及前后端協(xié)作、會話管理、圖像生成等多個(gè)關(guān)鍵點(diǎn)。我們將一步步完成:動態(tài)驗(yàn)證碼圖片生成 → 前端展示與刷新 → 表單提交 → 后臺校驗(yàn)全流程,并在最后探討其可擴(kuò)展性與未來演進(jìn)方向。
動態(tài)驗(yàn)證碼是如何工作的?
驗(yàn)證碼的核心邏輯其實(shí)很樸素:服務(wù)器生成一段隨機(jī)字符,繪制成干擾性強(qiáng)的圖片供人識別,同時(shí)將原始值存儲在當(dāng)前用戶的 Session 中;當(dāng)用戶輸入后,后臺比對輸入值與 Session 中的記錄是否一致。
這個(gè)過程中有幾個(gè)關(guān)鍵設(shè)計(jì)必須注意:
- 防止緩存:瀏覽器不能緩存驗(yàn)證碼圖片,否則點(diǎn)擊刷新無效
- 時(shí)效控制:驗(yàn)證碼不應(yīng)永久有效,通常設(shè)置為60秒過期
- 一次消費(fèi):成功驗(yàn)證后應(yīng)立即清除,防止重放攻擊
- 大小寫處理:建議統(tǒng)一轉(zhuǎn)小寫或大寫進(jìn)行比較,提升用戶體驗(yàn)
接下來我們就通過三個(gè)核心模塊來落地這套機(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注解簡化部署配置; - 設(shè)置 HTTP 頭禁止緩存,確保每次請求都能拿到新圖;
- 干擾線數(shù)量適中(50條),避免影響正常識別;
- 字符集排除了易混淆的
O/0和I/l/1,降低用戶輸錯(cuò)概率; - 顏色隨機(jī)化增強(qiáng) OCR 難度,但不過度花哨以免影響可讀性;
- 最終驗(yàn)證碼轉(zhuǎn)為小寫存入 Session,方便后續(xù)忽略大小寫校驗(yàn)。
小技巧:如果你希望支持中文提示詞渲染,可以考慮引入 TTF 字體文件并調(diào)用 g.setFont() 加載自定義字體,這對國際化項(xiàng)目尤其有用。
登錄頁面設(shè)計(jì)(Login.jsp)
前端頁面采用純 JSP 編寫,結(jié)合內(nèi)聯(lián) CSS 和輕量 JS 實(shí)現(xiàn)良好視覺效果與交互體驗(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)證碼圖片的動態(tài)加載機(jī)制:
- 初始
<img src="captcha">請求生成第一張圖; - 用戶點(diǎn)擊圖片時(shí),JS 修改
src屬性,附加時(shí)間戳參數(shù)(如?timestamp=171234567890); - 由于 URL 發(fā)生變化,瀏覽器不會使用緩存,從而觸發(fā) Servlet 重新生成新驗(yàn)證碼。
這種做法簡單高效,無需額外 AJAX 請求或 DOM 操作,非常適合輕量級場景。
登錄邏輯處理(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;'>? 登錄成功!歡迎回來," + username + "</h2>");
session.removeAttribute("verify_code"); // 防止重復(fù)使用
} else {
out.println("<script>alert('用戶名或密碼錯(cuò)誤!'); history.back();</script>");
}
}
}
幾點(diǎn)說明:
- 輸入值統(tǒng)一轉(zhuǎn)為小寫再比對,避免因大小寫導(dǎo)致失敗;
- 校驗(yàn)失敗直接彈窗并返回上一頁,保持上下文;
- 成功登錄后主動移除 Session 中的驗(yàn)證碼,杜絕二次利用;
- 當(dāng)前示例使用硬編碼賬戶,實(shí)際項(xiàng)目中應(yīng)連接數(shù)據(jù)庫查詢用戶信息。
安全提醒:生產(chǎn)環(huán)境中應(yīng)使用加密存儲密碼(如 BCrypt),并通過 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í)間 | 在 web.xml 中設(shè)置 Session 超時(shí)(如 <session-config><timeout>1</timeout></session-config>) |
| AJAX 提交 | 改用異步提交,避免整頁刷新,提升 UX |
| 返回 JSON | 后端返回結(jié)構(gòu)化數(shù)據(jù),便于前后端分離架構(gòu)集成 |
| 滑動/點(diǎn)選驗(yàn)證碼 | 引入更復(fù)雜的交互式驗(yàn)證機(jī)制,適合高安全要求場景 |
例如,你可以將當(dāng)前的同步表單提交改為 Fetch API 異步請求:
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)無刷新登錄體驗(yàn)。
寫在最后:傳統(tǒng)驗(yàn)證碼的未來在哪里?
我們今天實(shí)現(xiàn)的是典型的“圖像+文本”型驗(yàn)證碼,屬于 CAPTCHA 的初級形態(tài)。隨著 AI 視覺技術(shù)的發(fā)展,這類簡單干擾圖像正變得越來越容易被破解。
但反過來看,AI 也在催生新一代智能驗(yàn)證方式。比如阿里 recently 開源的 Z-Image-ComfyUI 模型,具備強(qiáng)大的文生圖能力和語義理解水平,完全可以用來構(gòu)建“智能圖形挑戰(zhàn)”:
- “請選出包含雨傘的圖片”
- “根據(jù)描述生成指定圖案”
- “判斷兩幅圖是否表達(dá)相同含義”
這些基于語義理解的任務(wù),機(jī)器難以批量偽造,而人類卻能輕松應(yīng)對。未來的驗(yàn)證碼可能不再是“你是不是人”,而是“你有沒有人類的理解力”。
當(dāng)然,在絕大多數(shù)普通業(yè)務(wù)系統(tǒng)中,傳統(tǒng)的字符驗(yàn)證碼依然夠用且成本低廉。只要合理設(shè)計(jì)、及時(shí)更新策略,它仍然是性價(jià)比極高的安全防護(hù)手段。
如果你正在做一個(gè)學(xué)生管理系統(tǒng)、內(nèi)部辦公平臺或小型 CMS,本文這套 JSP + Servlet 方案完全足夠快速上線;
如果你關(guān)注前沿趨勢,不妨嘗試將 Z-Image 這類 AIGC 工具接入驗(yàn)證流程,探索下一代人機(jī)識別的可能性。
技術(shù)沒有高低之分,只有適不適合。守住底線安全,同時(shí)保持對未來的敏感,這才是開發(fā)者應(yīng)有的姿態(tài)。
以上就是使用JSP+JavaScript實(shí)現(xiàn)驗(yàn)證碼登錄功能的詳細(xì)內(nèi)容,更多關(guān)于JSP JavaScript驗(yàn)證碼登錄的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
JavaScript實(shí)現(xiàn)計(jì)算字符串中出現(xiàn)次數(shù)最多的字符和出現(xiàn)的次數(shù)
這篇文章主要介紹了JavaScript實(shí)現(xiàn)計(jì)算字符串中出現(xiàn)次數(shù)最多的字符和出現(xiàn)的次數(shù),本文直接給出實(shí)現(xiàn)代碼,需要的朋友可以參考下2015-03-03
JS中使用Array函數(shù)shift和pop創(chuàng)建可忽略參數(shù)的例子
這篇文章主要介紹了JS中使用Array函數(shù)shift和pop創(chuàng)建可忽略參數(shù)的例子,這是一種比較高級的應(yīng)用,需要的朋友可以參考下2014-05-05
TypeScript快速上手語法及結(jié)合vue3用法詳解
TypeScript是一種由微軟開發(fā)的自由開源的編程語言,主要提供了類型系統(tǒng)和對ES6的支持,下面這篇文章主要給大家介紹了關(guān)于TypeScript快速上手語法及結(jié)合vue3用法的相關(guān)資料,需要的朋友可以參考下2024-02-02
JS前端開發(fā)之exec()和match()的對比使用
match()方法可在字符串內(nèi)檢索指定的值,或找到一個(gè)或多個(gè)正則表達(dá)式的匹配,下面這篇文章主要給大家介紹了關(guān)于JS前端開發(fā)之exec()和match()的對比使用的相關(guān)資料,需要的朋友可以參考下2022-09-09
JavaScript中檢測數(shù)據(jù)類型的四種方法總結(jié)
這篇文章主要為大家詳細(xì)介紹了四個(gè)JavaScript中檢測數(shù)據(jù)類型的常用方法,文中的示例代碼講解詳細(xì),具有一定的參考價(jià)值,需要的可以參考一下2023-04-04
jQuery animate()實(shí)現(xiàn)背景色漸變效果的處理方法【使用jQuery.color.js插件】
這篇文章主要介紹了jQuery animate()實(shí)現(xiàn)背景色漸變效果的處理方法,結(jié)合實(shí)例形式分析了jQuery顏色插件jquery.color.js實(shí)現(xiàn)背景色漸變的簡單操作技巧,需要的朋友可以參考下2017-03-03
用js提交表單解決一個(gè)頁面有多個(gè)提交按鈕的問題
這篇文章主要介紹了用js提交表單解決一個(gè)頁面有多個(gè)提交按鈕的問題,主要是判斷是否為提交文本,然后再執(zhí)行相應(yīng)的動作,需要的朋友可以參考下2014-09-09

