Struts 2.5.x 版本對核心控制器、配置結構進行了重大調整。本文基於 2.5.5 版本,系統講解攔截器配置新語法、自定義攔截器開發、攔截器棧組合,並以「登錄認證攔截器」為例提供生產級實現代碼。
| 項目 | Struts 2.3 | Struts 2.5+ |
|---|---|---|
| 核心過濾器 | org.apache.struts2.dispatcher.ng.filter.StrutsPrepareAndExecuteFilter | org.apache.struts2.dispatcher.filter.StrutsPrepareAndExecuteFilter |
| 配置文件 DTD | struts-2.3.dtd | struts-2.5.dtd |
| 包結構 | org.apache.struts2.* | 基本兼容,部分內部類移動 |
| 最低 JDK | JDK 6 | JDK 7+ |
<web-app xmlns="https://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://xmlns.jcp.org/xml/ns/javaee
https://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1">
<!-- Struts 2.5 核心過濾器 -->
<filter>
<filter-name>struts2</filter-name>
<filter-class>org.apache.struts2.dispatcher.filter.StrutsPrepareAndExecuteFilter</filter-class>
<!-- 編碼設置 -->
<init-param>
<param-name>struts.i18n.encoding</param-name> <!-- 注意: i18n 非 il8 -->
<param-value>UTF-8</param-value>
</init-param>
<!-- struts.xml 路徑 (可選,默認 classpath:struts.xml) -->
<init-param>
<param-name>filterConfig</param-name>
<param-value>classpath:struts.xml</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>struts2</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>
<struts>
<!-- 全局常量配置 -->
<constant name="struts.i18n.encoding" value="UTF-8" />
<constant name="struts.devMode" value="true" /> <!-- 開發模式:配置修改熱加載 -->
<constant name="struts.configuration.xml.reload" value="true" />
<constant name="struts.action.extension" value="action," /> <!-- 支持無後綴訪問 -->
<constant name="struts.enable.DynamicMethodInvocation" value="false" /> <!-- 禁用動態方法調用 (!) -->
<constant name="struts.serve.static.browserCache" value="false" /> <!-- 開發期禁用靜態資源緩存 -->
<constant name="struts.convention.default.parent.package" value="common"/>
<!-- 公共包:定義攔截器、全局結果、通用配置 -->
<package name="common" namespace="/" extends="struts-default">
<!-- 1. 註冊自定義攔截器 -->
<interceptors>
<interceptor name="loginInterceptor"
class="com.demo.action.LoginInterceptor" />
<!-- 2. 組合自定義攔截器棧 (必須包含 defaultStack) -->
<interceptor-stack name="loginStack">
<interceptor-ref name="defaultStack" /> <!-- 繼承默認棧 (params, validation, workflow 等) -->
<interceptor-ref name="loginInterceptor" /> <!-- 添加自定義攔截器 -->
</interceptor-stack>
</interceptors>
<!-- 3. 設置包默認攔截器棧 -->
<default-interceptor-ref name="loginStack" />
<!-- 4. 全局結果映射 -->
<global-results>
<result name="noLogin">/WEB-INF/pages/sweet/404.jsp</result>
<result name="error">/WEB-INF/pages/error.jsp</result>
</global-results>
<!-- Action 定義 (或在子包中定義) -->
<action name="login" class="com.demo.action.LoginAction">
<result name="success">/main.jsp</result>
<result name="input">/login.jsp</result>
</action>
</package>
</struts>
package com.demo.action;
import com.opensymphony.xwork2.ActionInvocation;
import com.opensymphony.xwork2.ActionSupport;
import com.opensymphony.xwork2.interceptor.Interceptor;
import org.apache.struts2.ServletActionContext;
public class LoginInterceptor implements Interceptor {
@Override
public void init() {
// 初始化邏輯 (如加載白名單配置)
System.out.println("🔧 LoginInterceptor 初始化");
}
@Override
public String intercept(ActionInvocation invocation) throws Exception {
// 1. 獲取當前 Action 實例
Object action = invocation.getAction();
// 2. 放行登錄相關 Action (避免死循環)
if (action instanceof LoginAction) {
return invocation.invoke();
}
// 3. 白名單放行 (可配置化)
String actionName = invocation.getProxy().getActionName();
if (isExcluded(actionName)) {
return invocation.invoke();
}
// 4. 檢查 Session 登錄狀態
Object user = ServletActionContext.getRequest().getSession().getAttribute("user");
if (user == null) {
System.out.println("🚫 未登錄訪問: " + actionName);
return "noLogin"; // 對應 global-results 中的 noLogin
}
// 5. 放行
return invocation.invoke();
}
@Override
public void destroy() {
// 銷毀邏輯 (如釋放資源)
System.out.println("🗑️ LoginInterceptor 銷毀");
}
// 白名單判斷 (建議從配置文件讀取)
private boolean isExcluded(String actionName) {
return actionName.startsWith("static/") ||
actionName.equals("login") ||
actionName.equals("register") ||
actionName.equals("captcha");
}
}
package com.demo.action;
import com.opensymphony.xwork2.ActionSupport;
import com.opensymphony.xwork2.ModelDriven;
import org.apache.struts2.convention.annotation.Action;
import org.apache.struts2.convention.annotation.Result;
import org.apache.struts2.convention.annotation.Results;
@Results({
@Result(name="success", location="/main.jsp"),
@Result(name="input", location="/login.jsp")
})
public class LoginAction extends ActionSupport implements ModelDriven<User> {
private User user = new User();
@Override
public User getModel() {
return user; // 表單參數自動注入 user 對象
}
@Action(value = "login")
public String execute() throws Exception {
// 業務邏輯:驗證用戶名密碼
if ("admin".equals(user.getUsername()) && "admin".equals(user.getPassword())) {
// 登錄成功:存入 Session
ServletActionContext.getRequest().getSession().setAttribute("user", user);
System.out.println("✅ 登錄成功: " + user.getUsername());
return SUCCESS;
} else {
addFieldError("username", "用戶名或密碼錯誤");
System.out.println("❌ 登錄失敗: " + user.getUsername());
return INPUT;
}
}
// 登出 Action
@Action(value = "logout", results = @Result(name="success", location="/login.jsp", type="redirect"))
public String logout() {
ServletActionContext.getRequest().getSession().invalidate();
return SUCCESS;
}
}
package com.demo.entity;
public class User {
private String username;
private String password;
// Getter/Setter 省略
}
<action name="user/*" class="com.demo.action.UserAction" method="{1}">
<interceptor-ref name="loginStack">
<!-- 排除特定方法 -->
<param name="excludeMethods">login,register,logout</param>
</interceptor-ref>
<result name="noLogin">/login.jsp</result>
</action>
@InterceptorRefs({
@InterceptorRef(value = "defaultStack", params = {"excludeMethods", "login,register"}),
@InterceptorRef("loginInterceptor")
})
@ParentPackage("common")
@Namespace("/")
@Results({...})
public class UserAction extends ActionSupport { ... }
// 在 LoginInterceptor.intercept() 中增強
HttpServletRequest request = ServletActionContext.getRequest();
HttpSession session = request.getSession(false); // 不創建新 Session
if (session == null || session.getAttribute("user") == null) {
// AJAX 請求返回 JSON
if ("XMLHttpRequest".equals(request.getHeader("X-Requested-With"))) {
HttpServletResponse response = ServletActionContext.getResponse();
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":401,\"msg\":\"會話過期,請重新登錄\"}");
return null; // 重要:返回 null 表示已手寫響應
}
return "noLogin";
}
| 現象 | 原因 | 解決 |
|---|---|---|
| 攔截器不生效 | 未加入 defaultStack 或 default-interceptor-ref 配置錯誤 | 檢查 interceptor-stack 包含 defaultStack、default-interceptor-ref 指向正確棧名 |
| 靜態資源 (CSS/JS) 被攔截 | url-pattern 為 /* 攔截所有請求 | 1. 白名單排除 2. 配置 <constant name="struts.action.excludePattern" value="/static/.*" /> |
| 無限循環跳轉登錄頁 | 登錄 Action 被攔截 | 攔截器中判斷 action instanceof LoginAction 放行 |
| Session 取值為 null | 請求未經過 Struts 過濾器或 Session 已失效 | 檢查 filter-mapping url-pattern、Session 超時配置 |
| 中文參數亂碼 | 編碼設置錯誤 | web.xml 和 struts.xml 同時設置 struts.i18n.encoding=UTF-8 |
request.getSession(false) 避免創建無用 SessiondefaultStack 先執行參數注入,自定義攔截器後執行業務邏輯Struts 2.5 的攔截器機制是其核心優勢所在。掌握 註冊→組合棧→設置默認→白名單放行→業務邏輯 這套標準流程,配合註解式配置與約定優於配置原則,即可構建清晰、可維護、安全的 Web 應用安全防線。