實現兩大核心功能:
技術棧:Servlet 3.0+、Filter、Session、Cookie、AES 加密、BCrypt 密碼哈希。
@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;
}
}
@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天)、敏感操作需重新驗證密碼、異地登入通知 |
// 明文結構(加密前)
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 */ }
@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 整合等企業級特性,避免重複造輪子引入安全漏洞。