縱深防禦策略在 Java Web 中的落地:
本文聚焦應用層認證授權的 JSP/Servlet 經典實現。
<form action="${pageContext.request.contextPath}/login" method="post">
<input type="hidden" name="${csrfTokenName}" value="${csrfToken}"/>
<div>用戶名:<input name="username" required/></div>
<div>密碼:<input type="password" name="password" required/></div>
<div>驗證碼:<input name="captcha" maxlength="4"/>
<img src="${pageContext.request.contextPath}/captcha.jpg" onclick="this.src=this.src+'?'+Date.now()"/>
</div>
<button type="submit">登入</button>
</form>
@WebServlet("/login")
public class LoginServlet extends HttpServlet {
@Autowired UserService userService;
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException {
String username = req.getParameter("username");
String password = req.getParameter("password");
String captchaInput = req.getParameter("captcha");
// 1. 驗證碼校驗
String captchaReal = (String) req.getSession().getAttribute("captcha");
if (captchaReal == null || !captchaReal.equalsIgnoreCase(captchaInput)) {
req.setAttribute("error", "驗證碼錯誤");
req.getRequestDispatcher("/login.jsp").forward(req, resp);
return;
}
req.getSession().removeAttribute("captcha"); // 一次性使用
// 2. 用戶名密碼驗證 (Service 層封裝 BCrypt 驗證)
UserVO user = userService.authenticate(username, password);
if (user == null) {
req.setAttribute("error", "用戶名或密碼錯誤");
req.getRequestDispatcher("/login.jsp").forward(req, resp);
return;
}
// 3. 登入成功:會話固定保護 (換新 Session)
HttpSession oldSession = req.getSession(false);
if (oldSession != null) oldSession.invalidate();
HttpSession newSession = req.getSession(true);
// 4. 存入用戶主體
newSession.setAttribute("principal", user);
newSession.setAttribute("roles", user.getRoles()); // List
newSession.setAttribute("permissions", user.getPermissions()); // Set
// 5. 生成 CSRF Token
String csrfToken = UUID.randomUUID().toString();
newSession.setAttribute("csrfToken", csrfToken);
// 6. 重定向至目標頁或默認首頁
String redirectUrl = (String) newSession.getAttribute("redirectUrl");
newSession.removeAttribute("redirectUrl");
resp.sendRedirect(redirectUrl != null ? redirectUrl : req.getContextPath() + "/");
}
}
@WebFilter("/*")
public class SecurityFilter implements Filter {
// 白名單:登入頁、靜態資源、錯誤頁、驗證碼
private static final Set WHITE_LIST = Set.of(
"/login.jsp", "/login", "/captcha.jpg", "/error/", "/css/", "/js/", "/images/", "/favicon.ico"
);
@Override
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) resp;
String uri = request.getRequestURI().substring(request.getContextPath().length());
// 1. 白名單放行
if (isWhiteListed(uri)) {
chain.doFilter(request, response);
return;
}
// 2. 檢查登入狀態
HttpSession session = request.getSession(false);
UserVO principal = (session != null) ? (UserVO) session.getAttribute("principal") : null;
if (principal == null) {
// 未登入:保存目標 URL,重定向登入頁
session = request.getSession(true);
session.setAttribute("redirectUrl", request.getRequestURI() +
(request.getQueryString() != null ? "?" + request.getQueryString() : ""));
response.sendRedirect(request.getContextPath() + "/login.jsp");
return;
}
// 3. 授權檢查 (簡易版:基於角色/權限)
if (!isAuthorized(principal, uri, request.getMethod())) {
response.sendError(HttpServletResponse.SC_FORBIDDEN, "權限不足");
return;
}
// 4. CSRF 保護 (針對狀態改變請求)
if (isMutatingMethod(request.getMethod())) {
String token = request.getParameter("csrfToken");
String sessionToken = (String) session.getAttribute("csrfToken");
if (token == null || !token.equals(sessionToken)) {
response.sendError(HttpServletResponse.SC_FORBIDDEN, "無效的 CSRF Token");
return;
}
}
chain.doFilter(request, response);
}
private boolean isWhiteListed(String uri) {
return WHITE_LIST.stream().anyMatch(uri::startsWith);
}
private boolean isAuthorized(UserVO user, String uri, String method) {
// 管理員放行
if (user.getRoles().contains("ADMIN")) return true;
// 普通用戶僅能訪問 /user/**, /order/own/**
if (user.getRoles().contains("USER")) {
return uri.startsWith("/user/") || uri.startsWith("/order/own/");
}
return false;
}
private boolean isMutatingMethod(String method) {
return "POST".equalsIgnoreCase(method) || "PUT".equalsIgnoreCase(method)
|| "DELETE".equalsIgnoreCase(method) || "PATCH".equalsIgnoreCase(method);
}
}
<c:if test="${principal.roles.contains('ADMIN')}">
<a href="/admin/users">用戶管理</a>
</c:if>
<c:if test="${principal.permissions.contains('order:export')}">
<button onclick="exportOrders()">導出訂單</button>
</c:if>
<%@ taglib prefix="sec" uri="https://example.com/tags/security" %>
<sec:authorize access="hasRole('ADMIN')">
<div class="admin-panel">...</div>
</sec:authorize>
<sec:authorize access="hasPermission('order:export')">
<button>導出</button>
</sec:authorize>
<sec:authentication property="principal.username" var="username" />
歡迎,${username}!
<sec:logout linkText="退出登入" logoutUrl="/logout" />
public class AuthorizeTag extends SimpleTagSupport {
private String access; // SpEL 表達式: hasRole('X'), hasPermission('Y'), isAuthenticated()
public void setAccess(String access) { this.access = access; }
@Override
public void doTag() throws IOException, JspException {
HttpServletRequest req = (HttpServletRequest) getJspContext().getRequest();
HttpSession session = req.getSession(false);
UserVO principal = (session != null) ? (UserVO) session.getAttribute("principal") : null;
boolean granted = evaluate(access, principal);
if (granted) {
getJspBody().invoke(null); // 渲染標籤體
}
}
private boolean evaluate(String expr, UserVO user) {
if (user == null) return "isAuthenticated()".equals(expr) ? false : false;
if ("isAuthenticated()".equals(expr)) return true;
if (expr.startsWith("hasRole('") && expr.endsWith("')")) {
String role = expr.substring(9, expr.length() - 2);
return user.getRoles().contains(role);
}
if (expr.startsWith("hasPermission('") && expr.endsWith("')")) {
String perm = expr.substring(15, expr.length() - 2);
return user.getPermissions().contains(perm);
}
return false;
}
}
@WebServlet("/captcha.jpg")
public class CaptchaServlet extends HttpServlet {
private static final int WIDTH = 120, HEIGHT = 40, LENGTH = 4;
private static final String CHARS = "23456789ABCDEFGHJKLMNPQRSTUVWXYZ";
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
// 1. 生成隨機碼
StringBuilder code = new StringBuilder();
Random rand = new Random();
for (int i = 0; i < LENGTH; i++) code.append(CHARS.charAt(rand.nextInt(CHARS.length())));
// 2. 存入 Session
req.getSession().setAttribute("captcha", code.toString());
// 3. 繪製干擾圖片
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.setFont(new Font("Consolas", Font.BOLD, 28));
g.setColor(Color.BLACK);
for (int i = 0; i < LENGTH; i++) {
g.rotate(-0.2 + rand.nextDouble() * 0.4, 15 + i * 25, 25);
g.drawString(code.substring(i, i+1), 15 + i * 25, 25);
g.rotate(0.2 - rand.nextDouble() * 0.4, 15 + i * 25, 25);
}
// 干擾線
g.setColor(Color.GRAY);
for (int i = 0; i < 5; i++) g.drawLine(rand.nextInt(WIDTH), rand.nextInt(HEIGHT), rand.nextInt(WIDTH), rand.nextInt(HEIGHT));
g.dispose();
// 4. 輸出
resp.setContentType("image/jpeg");
resp.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
ImageIO.write(image, "JPEG", resp.getOutputStream());
}
}
<img id="captchaImg" src="${pageContext.request.contextPath}/captcha.jpg"
style="cursor:pointer; vertical-align:middle" title="點擊刷新"/>
<script>
document.getElementById('captchaImg').onclick = function() {
this.src = '${pageContext.request.contextPath}/captcha.jpg?' + Date.now();
};
</script>
@WebServlet("/logout")
public class LogoutServlet extends HttpServlet {
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException {
HttpSession session = req.getSession(false);
if (session != null) session.invalidate();
resp.sendRedirect(req.getContextPath() + "/login.jsp");
}
}
Java Web 安全控制核心:**Filter 統一認證授權攔截、Session 承載用戶主體與權限、CSRF Token 保護狀態改變請求、驗證碼防暴力破解、JSTL/自定義標籤頁面級授權、登出銷毀會話、密碼強雜湊、輸出編碼防 XSS、參數化防注入、安全響應頭**。此體系雖為傳統 JSP/Servlet 手工實現,但原理與 Spring Security、Apache Shiro 等框架內核一致,理解透徹有助於框架深度使用與故障排查。