public class User {
private Integer id;
private String username;
private String password; // 加密存儲
private String email;
private boolean enabled = true;
// getter/setter/toString
}
public interface UserService {
User login(String username, String password); // 驗證並返回用戶 (含權限)
User findById(Integer id);
void updateLastLoginTime(Integer userId);
}
@Controller
@Scope("prototype")
@ParentPackage("shop")
@Namespace("/user")
public class LoginAction extends ActionSupport {
private String username;
private String password;
private boolean rememberMe;
private String errorMessage;
@Autowired private UserService userService;
@Action(value = "login", results = {
@Result(name = "success", type = "redirectAction", params = {"actionName", "dashboard"}),
@Result(name = "input", location = "/login.jsp")
})
public String login() {
User user = userService.login(username, password);
if (user == null) {
errorMessage = "帳號或密碼錯誤";
return INPUT;
}
if (!user.isEnabled()) {
errorMessage = "帳號已停用";
return INPUT;
}
// 建立 Session
HttpSession session = ServletActionContext.getRequest().getSession(true);
session.setAttribute("LOGIN_USER", user);
session.setMaxInactiveInterval(30 * 60); // 30 分鐘
// 記住我:設定 Cookie (加密 Token,實際專案應用 JWT 或持久化 Token)
if (rememberMe) {
String token = TokenUtil.generatePersistentToken(user.getId());
Cookie cookie = new Cookie("REMEMBER_TOKEN", token);
cookie.setMaxAge(7 * 24 * 60 * 60); // 7 天
cookie.setPath("/");
cookie.setHttpOnly(true);
cookie.setSecure(true); // 僅 HTTPS
ServletActionContext.getResponse().addCookie(cookie);
}
userService.updateLastLoginTime(user.getId());
return SUCCESS;
}
@Action(value = "logout", results = @Result(name = "success", type = "redirectAction", params = {"actionName", "login", "namespace", "/"}))
public String logout() {
HttpSession session = ServletActionContext.getRequest().getSession(false);
if (session != null) session.invalidate();
// 清除記住我 Cookie
Cookie cookie = new Cookie("REMEMBER_TOKEN", "");
cookie.setMaxAge(0);
cookie.setPath("/");
ServletActionContext.getResponse().addCookie(cookie);
return SUCCESS;
}
@Action(value = "dashboard", results = @Result(name = "success", location = "/dashboard.jsp"))
public String dashboard() {
return SUCCESS;
}
// getter/setter
}
public class AuthenticationInterceptor extends AbstractInterceptor {
private static final String[] EXCLUDE_PATTERNS = {"/user/login", "/user/logout", "/static/", "/error/"};
@Override
public String intercept(ActionInvocation invocation) throws Exception {
HttpServletRequest request = ServletActionContext.getRequest();
String uri = request.getRequestURI();
// 排除靜態資源、登入頁等
for (String pattern : EXCLUDE_PATTERNS) {
if (uri.contains(pattern)) return invocation.invoke();
}
HttpSession session = request.getSession(false);
User user = (session != null) ? (User) session.getAttribute("LOGIN_USER") : null;
// 嘗試 Cookie 自動登入
if (user == null) {
user = tryAutoLogin(request);
if (user != null) {
request.getSession(true).setAttribute("LOGIN_USER", user);
}
}
if (user == null) {
// AJAX 請求返回 401,頁面請求重定向
if ("XMLHttpRequest".equals(request.getHeader("X-Requested-With"))) {
HttpServletResponse response = ServletActionContext.getResponse();
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
return null;
}
return "loginRedirect"; // struts.xml 定義全域結果
}
return invocation.invoke();
}
private User tryAutoLogin(HttpServletRequest request) {
Cookie[] cookies = request.getCookies();
if (cookies == null) return null;
for (Cookie cookie : cookies) {
if ("REMEMBER_TOKEN".equals(cookie.getName())) {
String token = cookie.getValue();
// 驗證 Token (解析用戶 ID、檢查簽名、查詢資料庫、檢查過期)
return tokenService.validateAndGetUser(token);
}
}
return null;
}
}
<package name="shop" extends="struts-default">
<interceptors>
<interceptor name="auth" class="cn.it.shop.interceptor.AuthenticationInterceptor"/>
<interceptor-stack name="defaultStackWithAuth">
<interceptor-ref name="defaultStack"/>
<interceptor-ref name="auth"/>
</interceptor-stack>
</interceptors>
<default-interceptor-ref name="defaultStackWithAuth"/>
<global-results>
<result name="loginRedirect" type="redirectAction">
<param name="actionName">login</param>
<param name="namespace">/user</param>
</result>
</global-results>
<!-- Action 定義 -->
</package>
<%@ page contentType="text/html;charset=UTF-8" %>
<%@ taglib prefix="s" uri="/struts-tags" %>
<html>
<head>
<title>用戶登入</title>
<link rel="stylesheet" href="${pageContext.request.contextPath}/css/login.css">
</head>
<body>
<div class="login-container">
<h2>後台管理系統</h2>
<s:if test="errorMessage != null">
<div class="alert alert-danger"><s:property value="errorMessage"/></div>
</s:if>
<s:form action="login" namespace="/user" method="post" theme="simple">
<div class="form-group">
<label>帳號</label>
<s:textfield name="username" id="username" cssClass="form-control" required="true"/>
</div>
<div class="form-group">
<label>密碼</label>
<s:password name="password" id="password" cssClass="form-control" required="true"/>
</div>
<div class="form-group checkbox">
<s:checkbox name="rememberMe" fieldValue="true" label="記住我 (7天)" />
</div>
<button type="submit" class="btn btn-primary btn-block">登入</button>
</s:form>
</div>
</body>
</html>
<%@ page contentType="text/html;charset=UTF-8" %>
<%@ taglib prefix="c" uri="https://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="s" uri="/struts-tags" %>
<html>
<head><title>儀表板</title></head>
<body>
<div class="header">
<span class="welcome">
歡迎,<c:out value="${sessionScope.LOGIN_USER.username}"/>
(<c:out value="${sessionScope.LOGIN_USER.email}"/>)
</span>
<a href="<c:url value='/user/logout.action'/>" class="btn btn-link">登出</a>
</div>
<div class="content">
<h3>儀表板內容</h3>
<!-- 業務模組入口 -->
</div>
</body>
</html>
| 措施 | 實現要點 |
|---|---|
| 密碼加密 | BCrypt/SCrypt/Argon2,不可用 MD5/SHA1,Spring Security PasswordEncoder |
| 防暴力破解 | 登入失敗計數 + IP 封鎖 (Redis 計數器)、驗證碼 |
| Session 安全 | HttpOnly、Secure、SameSite=Strict、定期換 Session ID |
| CSRF 防護 | Struts2 內建 <s:token/>、雙重提交 Cookie |
| 記住我 Token | JWT 或隨機字串 + 資料庫持久化、簽名驗證、過期時間、單設備/多設備策略 |
| HTTPS 強制 | 生產環境全站 HTTPS、HSTS、Secure Cookie |
若專案規模較大,建議遷移至 Spring Security:
@PreAuthorize, hasRole)