首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> 網頁程式設計 >> JSP 程式設計 >> 關於JSP

Java Web 安全控制體系:登入驗證、權限攔截、Filter 過濾器、標籤庫授權與驗證碼完整實現

     編輯:關於JSP

Web 安全控制分層模型

縱深防禦策略在 Java Web 中的落地:

  1. 傳輸層:HTTPS 強制、HSTS、Secure Cookie、CSP 頭。
  2. 網關層:WAF、IP 黑白名單、限流、防刷、SQL 注入/XSS 過濾。
  3. 應用層 - 認證**:登入驗證、Session 管理、CSRF Token、驗證碼、多因子認證 (MFA)。
  4. 應用層 - 授權**:基於角色 (RBAC)、資源 (ABAC)、權限表達式的訪問控制。
  5. 代碼層**:參數校驗、輸出編碼、安全 API 使用、敏感數據脫敏。

本文聚焦應用層認證授權的 JSP/Servlet 經典實現。

登入認證流程設計

1. 登入表單 (login.jsp)

<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>

2. 登入處理 Servlet

@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() + "/");
    }
}

權限攔截器:Filter 實現全局保護

@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);
    }
}

頁面級授權:JSTL + 自定義標籤

1. 使用 JSTL 核心標籤

<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>

2. 封裝為自定義標籤 (更語義化、可復用)

<%@ 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;
    }
}

驗證碼生成與驗證

1. 驗證碼 Servlet (生成圖片)

@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());
    }
}

2. 前端刷新機制

<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>

登出與 Session 管理

@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");
    }
}

進階安全加固清單

  1. 密碼存儲:BCrypt/SCrypt/Argon2 雜湊,**絕不**明文/MD5/SHA1。`BCrypt.hashpw(password, BCrypt.gensalt(12))`。
  2. 會話安全:`session.setMaxInactiveInterval(30*60)`、Cookie `Secure; HttpOnly; SameSite=Strict`、防會話固定 (登入後換 Session ID)。
  3. CSRF 同源檢查:雙重 Cookie 模式 (`X-CSRF-Token` Header + Cookie) 或 SameSite Cookie 替代。
  4. XSS 防護:輸出編碼 (`${fn:escapeXml()}`)、CSP 頭 (`Content-Security-Policy: default-src 'self'; script-src 'self'`)。
  5. SQL 注入**:全鏈路參數化查詢,嚴禁字串拼接 SQL。
  6. 文件上傳**:類型白名單、大小限制、重命名存儲、獨立域名/存儲桶、掃毒。
  7. 審計日誌**:登入/登出/敏感操作記錄 (用戶、IP、時間、結果、請求 ID)。
  8. 安全頭**:`X-Frame-Options: DENY`、`X-Content-Type-Options: nosniff`、`Referrer-Policy: strict-origin-when-cross-origin`。

總結

Java Web 安全控制核心:**Filter 統一認證授權攔截、Session 承載用戶主體與權限、CSRF Token 保護狀態改變請求、驗證碼防暴力破解、JSTL/自定義標籤頁面級授權、登出銷毀會話、密碼強雜湊、輸出編碼防 XSS、參數化防注入、安全響應頭**。此體系雖為傳統 JSP/Servlet 手工實現,但原理與 Spring Security、Apache Shiro 等框架內核一致,理解透徹有助於框架深度使用與故障排查。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved