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

Java Web Session 登入驗證完整實作:從登入攔截到登出清理的安全最佳實務

     編輯:Java 相關知識

前言

Session 機制是 Java Web 實現有狀態會話的核心。正確的登入驗證流程涉及憑證驗證、Session 建立、權限標記、攔截器保護、安全登出等多個環節。本文提供生產級的完整實作,涵蓋 Servlet 3.0+ 註解風格、攔截器模式、CSRF 防護基礎與安全加固建議。

核心流程圖

登入請求 (POST /login)
    │
    ▼
┌─────────────────────┐
│ 1. 參數校驗         │ ← 非空、長度、格式
│ 2. 憑證驗證         │ ← 查詢資料庫、BCrypt 比對
│ 3. 建立/獲取 Session│ ← request.getSession(true)
│ 4. 存入用戶主體     │ ← session.setAttribute("principal", user)
│ 5. 更新 Session ID  │ ← 防 Session Fixation 攻擊
│ 6. 設定安全屬性     │ ← HttpOnly, Secure, SameSite
│ 7. 重導向首頁       │ ← response.sendRedirect("/")
└─────────────────────┘

實體與資料存取層

// User.java
public class User {
    private Long id;
    private String username;
    private String passwordHash; // 儲存 BCrypt 雜湊,絕不儲存明文
    private String role;         // "ADMIN", "USER"
    private boolean enabled;
    // Getter/Setter...
}

// UserDao.java (介面)
public interface UserDao {
    User findByUsername(String username);
    // 其他方法...
}

// UserDaoImpl.java (JdbcTemplate 實作)
@Repository
public class UserDaoImpl implements UserDao {
    @Autowired JdbcTemplate jdbcTemplate;
    
    @Override
    public User findByUsername(String username) {
        String sql = "SELECT id, username, password_hash, role, enabled FROM users WHERE username = ?";
        return jdbcTemplate.queryForObject(sql, (rs, n) -> {
            User u = new User();
            u.setId(rs.getLong("id"));
            u.setUsername(rs.getString("username"));
            u.setPasswordHash(rs.getString("password_hash"));
            u.setRole(rs.getString("role"));
            u.setEnabled(rs.getBoolean("enabled"));
            return u;
        }, username);
    }
}

登入 Servlet 核心實作

@WebServlet("/login")
public class LoginServlet extends HttpServlet {
    @Autowired UserDao userDao;
    @Autowired PasswordEncoder passwordEncoder; // BCryptPasswordEncoder

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) 
            throws ServletException, IOException {
        
        req.setCharacterEncoding("UTF-8");
        String username = req.getParameter("username");
        String password = req.getParameter("password");
        String csrfToken = req.getParameter("csrfToken");
        
        // 1. 基礎校驗
        if (username == null || password == null || username.isBlank() || password.isBlank()) {
            sendError(resp, "帳號密碼不可為空");
            return;
        }
        
        // 2. CSRF 驗證 (簡易版:Session 中驗證)
        String sessionCsrf = (String) req.getSession().getAttribute("csrfToken");
        if (csrfToken == null || !csrfToken.equals(sessionCsrf)) {
            sendError(resp, "請求無效,請重新整理頁面");
            return;
        }
        
        // 3. 查詢用戶
        User user = userDao.findByUsername(username.trim());
        if (user == null || !user.isEnabled()) {
            sendError(resp, "帳號不存在或已停用");
            return;
        }
        
        // 4. 密碼驗證 (BCrypt)
        if (!passwordEncoder.matches(password, user.getPasswordHash())) {
            // 記錄失敗日誌、鎖定策略...
            sendError(resp, "密碼錯誤");
            return;
        }
        
        // 5. 登入成功:建立新 Session (防 Session Fixation)
        HttpSession oldSession = req.getSession(false);
        if (oldSession != null) oldSession.invalidate(); // 銷毀舊 Session
        
        HttpSession newSession = req.getSession(true); // 強制建立新 Session
        newSession.setAttribute("principal", user);    // 存入用戶主體
        newSession.setAttribute("loginTime", System.currentTimeMillis());
        newSession.setMaxInactiveInterval(30 * 60);    // 30 分鐘無操作過期
        
        // 6. 產生新 CSRF Token 供下次請求
        newSession.setAttribute("csrfToken", UUID.randomUUID().toString());
        
        // 7. 重導向 (避免表單重複提交)
        String redirectUrl = req.getContextPath() + "/dashboard";
        resp.sendRedirect(redirectUrl);
    }
    
    private void sendError(HttpServletResponse resp, String msg) throws IOException {
        resp.setContentType("text/html;charset=UTF-8");
        resp.getWriter().println("");
    }
}

登入攔截器 (Filter)

@WebFilter(urlPatterns = {"/dashboard/*", "/api/*", "/profile/*"})
public class AuthFilter implements Filter {
    
    private static final String[] PUBLIC_PATHS = {
        "/login", "/register", "/static/", "/css/", "/js/", "/images/", "/favicon.ico"
    };

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse resp = (HttpServletResponse) response;
        String path = req.getRequestURI().substring(req.getContextPath().length());
        
        // 1. 放行公開資源
        if (isPublicPath(path)) {
            chain.doFilter(request, response);
            return;
        }
        
        // 2. 檢查 Session 與主體
        HttpSession session = req.getSession(false);
        User principal = (session != null) ? (User) session.getAttribute("principal") : null;
        
        if (principal == null) {
            // 未登入:儲存原請求 URL,重導向登入頁
            String queryString = req.getQueryString();
            String targetUrl = req.getRequestURI() + (queryString != null ? "?" + queryString : "");
            session = req.getSession(true);
            session.setAttribute("redirectAfterLogin", targetUrl);
            resp.sendRedirect(req.getContextPath() + "/login");
            return;
        }
        
        // 3. 權限檢查 (簡易版:角色基礎)
        if (path.startsWith("/admin/") && !"ADMIN".equals(principal.getRole())) {
            resp.sendError(HttpServletResponse.SC_FORBIDDEN, "權限不足");
            return;
        }
        
        // 4. 通過:將主體放入 Request 屬性,方便 Controller 直接取用
        req.setAttribute("principal", principal);
        chain.doFilter(request, response);
    }
    
    private boolean isPublicPath(String path) {
        for (String publicPath : PUBLIC_PATHS) {
            if (path.startsWith(publicPath)) return true;
        }
        return false;
    }
}

登出 Servlet

@WebServlet("/logout")
public class LogoutServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp)
            throws ServletException, IOException {
        HttpSession session = req.getSession(false);
        if (session != null) {
            // 記錄登出審計日誌
            User user = (User) session.getAttribute("principal");
            if (user != null) auditLog.info("用戶登出: {}", user.getUsername());
            
            session.invalidate(); // 徹底銷毀 Session,清除伺服器端資料
        }
        resp.sendRedirect(req.getContextPath() + "/login");
    }
}

登入頁面 (login.jsp) - 含 CSRF Token

<%@ page contentType="text/html;charset=UTF-8" %>
<!DOCTYPE html>
<html>
<head>
    <title>用戶登入</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
    <form method="post" action="${pageContext.request.contextPath}/login">
        <input type="hidden" name="csrfToken" value="${sessionScope.csrfToken}">
        <div>
            <label>帳號:</label>
            <input type="text" name="username" required autocomplete="username">
        </div>
        <div>
            <label>密碼:</label>
            <input type="password" name="password" required autocomplete="current-password">
        </div>
        <button type="submit">登入</button>
    </form>
</body>
</html>

Session 安全加固清單 (web.xml 或 Spring Boot 配置)

配置項建議值作用
session-config/session-timeout30 (分鐘)限制無操作存活時間
cookie-config/http-onlytrue防 XSS 竊取 Session ID
cookie-config/securetrue (僅 HTTPS)防中間人竊取
cookie-config/same-siteSTRICT 或 LAX防 CSRF 攻擊
tracking-modeCOOKIE禁用 URL 重寫 (防 Session ID 洩露)

Spring Boot 配置範例

server.servlet.session:
  timeout: 30m
  cookie:
    http-only: true
    secure: true
    same-site: strict
  tracking-modes: cookie

進階安全議題

  • Session Fixation:登入成功後 必須 `invalidate()` 舊 Session 再 `getSession(true)` 建立新 Session
  • Session Hijacking:全站 HTTPS + `Secure` + `HttpOnly` Cookie + 短過期時間
  • CSRF:同步器 Token 模式 (雙重提交 Cookie) + `SameSite` Cookie
  • 暴力破解:登入失敗計數 + 帳號暫時鎖定 + 驗證碼
  • 分散式 Session:Spring Session + Redis 共享 Session 跨節點
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved