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

Java Web 登入驗證與自動登入完整實現:Filter 驗證、Cookie 持久化、安全最佳實踐

     編輯:Java 相關知識

核心需求與架構

實現兩大核心功能:

  1. 登入狀態驗證:攔截未登入用戶訪問受保護資源,重定向至登入頁
  2. 自動登入 (Remember Me):用戶勾選「記住我」,下次訪問無需輸入帳密自動識別身份

技術棧:Servlet 3.0+、Filter、Session、Cookie、AES 加密、BCrypt 密碼哈希。

一、登入驗證 Filter 實現


@WebFilter(filterName = "LoginFilter", urlPatterns = {"/protected/*", "/user/*", "/admin/*"})
public class LoginFilter implements Filter {
    private static final String LOGIN_URI = "/login.jsp";
    private static final String LOGIN_ACTION = "/login"; // 登入處理 Servlet 路徑
    
    @Override
    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) 
            throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) resp;
        HttpSession session = request.getSession(false); // 不自動創建
        
        // 1. 放行登入頁、靜態資源、登入動作
        String uri = request.getRequestURI();
        String ctx = request.getContextPath();
        if (uri.equals(ctx + LOGIN_URI) || uri.startsWith(ctx + "/static/") 
                || uri.equals(ctx + LOGIN_ACTION) || uri.equals(ctx + "/logout")) {
            chain.doFilter(request, response);
            return;
        }
        
        // 2. 檢查 Session 登入標識
        User loginUser = (session != null) ? (User) session.getAttribute("loginUser") : null;
        if (loginUser != null) {
            // 可選:刷新 Session 過期時間、更新最後活躍時間
            chain.doFilter(request, response);
            return;
        }
        
        // 3. 嘗試自動登入
        if (tryAutoLogin(request, response)) {
            chain.doFilter(request, response);
            return;
        }
        
        // 4. 全部失敗:重定向登入頁,攜帶原請求 URL 以便登入後回跳
        String targetUrl = request.getRequestURL().toString();
        String query = request.getQueryString();
        if (query != null) targetUrl += "?" + query;
        response.sendRedirect(ctx + LOGIN_URI + "?redirect=" + URLEncoder.encode(targetUrl, "UTF-8"));
    }
    
    // 自動登入核心邏輯
    private boolean tryAutoLogin(HttpServletRequest req, HttpServletResponse resp) {
        Cookie[] cookies = req.getCookies();
        if (cookies == null) return false;
        
        for (Cookie c : cookies) {
            if ("rememberMe".equals(c.getName())) {
                String token = c.getValue();
                // 解析 token:userId:expires:signature
                String[] parts = decryptAndSplit(token);
                if (parts == null || parts.length != 3) return false;
                
                long userId = Long.parseLong(parts[0]);
                long expires = Long.parseLong(parts[1]);
                String signature = parts[2];
                
                // 檢查過期
                if (System.currentTimeMillis() > expires) {
                    clearRememberMeCookie(resp); return false;
                }
                
                // 驗證簽名(防篡改)
                String expectedSig = hmacSha256(userId + ":" + expires, SECRET_KEY);
                if (!MessageDigest.isEqual(signature.getBytes(), expectedSig.getBytes())) {
                    clearRememberMeCookie(resp); return false; // 簽名不匹配,可能被竄改
                }
                
                // 從 DB 獲取用戶(檢查是否被封禁/刪除)
                User user = userService.findById(userId);
                if (user == null || user.isLocked()) {
                    clearRememberMeCookie(resp); return false;
                }
                
                // 登入成功:存入 Session
                req.getSession(true).setAttribute("loginUser", user);
                return true;
            }
        }
        return false;
    }
}

二、登入處理 Servlet(含自動登入 Token 生成)


@WebServlet("/login")
public class LoginServlet extends HttpServlet {
    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) 
            throws ServletException, IOException {
        String username = req.getParameter("username");
        String password = req.getParameter("password");
        boolean remember = "on".equals(req.getParameter("rememberMe"));
        String redirect = req.getParameter("redirect");
        
        // 1. 驗證帳密(BCrypt 比對)
        User user = userService.findByUsername(username);
        if (user == null || !BCrypt.checkpw(password, user.getPasswordHash())) {
            req.setAttribute("error", "用戶名或密碼錯誤");
            req.getRequestDispatcher("/login.jsp").forward(req, resp);
            return;
        }
        if (user.isLocked()) {
            req.setAttribute("error", "賬號已被鎖定");
            req.getRequestDispatcher("/login.jsp").forward(req, resp);
            return;
        }
        
        // 2. 登入成功:建立 Session
        HttpSession session = req.getSession(true);
        session.setAttribute("loginUser", user);
        session.setMaxInactiveInterval(30 * 60); // 30 分鐘無操作過期
        
        // 3. 自動登入 Token 生成
        if (remember) {
            long expires = System.currentTimeMillis() + 14 * 24 * 3600 * 1000L; // 14 天
            String token = user.getId() + ":" + expires;
            String signature = hmacSha256(token, SECRET_KEY);
            String cookieValue = encrypt(token + ":" + signature); // AES 加密
            
            Cookie cookie = new Cookie("rememberMe", cookieValue);
            cookie.setPath(req.getContextPath());
            cookie.setMaxAge(14 * 24 * 3600); // 14 天
            cookie.setHttpOnly(true);  // 防 XSS
            cookie.setSecure(true);    // 僅 HTTPS 傳輸(生產必須)
            resp.addCookie(cookie);
        }
        
        // 4. 重定向回目標頁或首頁
        resp.sendRedirect(redirect != null ? redirect : req.getContextPath() + "/index.jsp");
    }
}

三、登出處理


@WebServlet("/logout")
public class LogoutServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) 
            throws ServletException, IOException {
        // 1. 銷毀 Session
        HttpSession session = req.getSession(false);
        if (session != null) session.invalidate();
        
        // 2. 清除自動登入 Cookie
        Cookie cookie = new Cookie("rememberMe", "");
        cookie.setPath(req.getContextPath());
        cookie.setMaxAge(0); // 立即刪除
        cookie.setHttpOnly(true);
        resp.addCookie(cookie);
        
        resp.sendRedirect(req.getContextPath() + "/login.jsp");
    }
}

四、安全加固關鍵點

風險對策
Cookie 竄改/偽造HMAC-SHA256 簽名 + AES 加密,雙重保障
Cookie 竊取HttpOnly + Secure + SameSite=Strict/Lax
Session 劫持HTTPS 全站、Session ID 定期輪換、並發登入限制
暴力破解登入失敗計數 + 驗證碼 + 賬號鎖定 + IP 限流
CSRF關鍵操作雙重驗證、SameSite Cookie、CSRF Token
自動登入 Token 洩露短效期(14天)、敏感操作需重新驗證密碼、異地登入通知

五、自動登入 Token 結構設計


// 明文結構(加密前)
userId:expiresTimestamp:signature
// 例:1001:1735689600000:a3f2c1e9d4b8...

// 簽名算法
String hmacSha256(String data, String secret) {
    Mac mac = Mac.getInstance("HmacSHA256");
    mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
    return Bytes.toHex(mac.doFinal(data.getBytes(StandardCharsets.UTF_8)));
}

// AES-256-GCM 加密(推薦,含完整性校驗)
String encrypt(String plaintext) { /* 返回 Base64(nonce + ciphertext + tag) */ }
String decrypt(String ciphertext) { /* 解密並驗證 tag */ }

六、進階:Spring Security 整合(生產推薦)


@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            .antMatchers("/login", "/static/**").permitAll()
            .anyRequest().authenticated()
            .and()
            .formLogin().loginPage("/login.jsp").permitAll()
            .and()
            .rememberMe()
                .key("myAppSecretKey")           // 簽名密鑰
                .tokenValiditySeconds(14*24*3600) // 14 天
                .userDetailsService(userDetailsService)
                .rememberMeParameter("rememberMe")
            .and()
            .logout().logoutUrl("/logout").permitAll()
            .and()
            .csrf().disable(); // 前後端分離通常關閉,改用 Stateless JWT
    }
}

總結

手寫 Filter 實現登入驗證與自動登入能深度理解 Servlet 規範、Cookie/Session 機制、加密簽名原理。但生產環境強烈建議直接使用 Spring Security / Apache Shiro 等成熟框架,它們提供完整的認證授權體系、CSRF 防護、Session 管理、OAuth2/OIDC 整合等企業級特性,避免重複造輪子引入安全漏洞。

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