Acegi (現 Spring Security) 核心承諾:無需編寫 Java 安全代碼,透過 XML 配置即可實現:
誤區澄清: 登入頁面由 Acegi 提供,而非 JSF。若 JSF 提供登入頁,需手工將憑證傳遞給 Acegi,違背「零代碼配置」初衷,引發整合複雜度爆炸。
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="https://java.sun.com/xml/ns/javaee">
<!-- 1. Spring 容器啟動監聽器 -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/acegi-config.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- 2. JSF 配置 -->
<context-param>
<param-name>javax.faces.STATE_SAVING_METHOD</param-name>
<param-value>server</param-value>
</context-param>
<context-param>
<param-name>javax.faces.CONFIG_FILES</param-name>
<param-value>/WEB-INF/faces-config.xml</param-value>
</context-param>
<listener>
<listener-class>com.sun.faces.config.ConfigureListener</listener-class>
</listener>
<!-- 3. FacesServlet 映射 -->
<servlet>
<servlet-name>Faces Servlet</servlet-name>
<servlet-class>javax.faces.webapp.FacesServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>Faces Servlet</servlet-name>
<url-pattern>*.faces</url-pattern>
</servlet-mapping>
<!-- 4. Acegi 過濾器鏈 -->
<filter>
<filter-name>Acegi Filter Chain Proxy</filter-name>
<filter-class>org.acegisecurity.util.FilterToBeanProxy</filter-class>
<init-param>
<param-name>targetClass</param-name>
<param-value>org.acegisecurity.util.FilterChainProxy</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>Acegi Filter Chain Proxy</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>
| 組件 | 角色 | 關鍵點 |
|---|---|---|
ContextLoaderListener | 啟動 Spring 根容器,載入 acegi-config.xml | Spring Bean 定義、事務、數據源在此初始化 |
ConfigureListener | 啟動 JSF 配置,掃描 Managed Bean、導航規則、轉換器 | JSF 運行時環境建立 |
FacesServlet | JSF 前端控制器,執行生命週期 | 映射 *.faces,load-on-startup=1 優先初始化 |
FilterToBeanProxy | 橋接 Servlet Filter 與 Spring Bean | 代理 FilterChainProxy,從 Spring 容器獲取真實 Filter 鏈 |
FilterChainProxy | 安全過濾器鏈核心 | 包含 AuthenticationProcessingFilter、ExceptionTranslationFilter、FilterSecurityInterceptor 等 |
filter-mapping /* | 攔截所有請求 | 確保 JSF 頁面 (*.faces) 先經過安全過濾 |
ContextLoaderListener.contextInitialized() → 建立 Spring 根容器 → 載入 acegi-config.xml 定義的認證管理器、用戶詳情服務、存取決策管理器、過濾器鏈。ConfigureListener.contextInitialized() → 建立 JSF Application 實例、掃描 faces-config.xml、註冊渲染器、驗證器。FacesServlet.init() → 初始化生命週期工廠、視圖處理器。/app/page.faces:
FilterToBeanProxy.doFilter() → 從 Spring 容器獲取 FilterChainProxy → 執行安全過濾器鏈。j_security_check),委派 AuthenticationManager 認證。AccessDeniedException、AuthenticationException,觸發登入頁重導向或 403 頁面。objectDefinitionSource (URL 模式 → 角色) 進行授權決策。chain.doFilter() → FacesServlet.service() → 執行 JSF 生命週期渲染頁面。
<beans>
<!-- 1. 認證管理器 -->
<bean id="authenticationManager" class="org.acegisecurity.providers.ProviderManager">
<property name="providers">
<list>
<ref bean="daoAuthenticationProvider"/>
</list>
</property>
</bean>
<!-- 2. DAO 認證提供者 (資料庫/LDAP) -->
<bean id="daoAuthenticationProvider" class="org.acegisecurity.providers.dao.DaoAuthenticationProvider">
<property name="userDetailsService"><ref bean="jdbcUserDetailsService"/></property>
<property name="passwordEncoder"><ref bean="passwordEncoder"/></property>
</bean>
<!-- 3. 用戶詳情服務 -->
<bean id="jdbcUserDetailsService" class="org.acegisecurity.providers.dao.jdbc.JdbcDaoImpl">
<property name="dataSource"><ref bean="dataSource"/></property>
<property name="usersByUsernameQuery">SELECT username, password, enabled FROM users WHERE username = ?</property>
<property name="authoritiesByUsernameQuery">SELECT username, authority FROM authorities WHERE username = ?</property>
</bean>
<!-- 4. 密碼編碼器 -->
<bean id="passwordEncoder" class="org.acegisecurity.providers.encoding.Md5PasswordEncoder"/>
<!-- 5. 過濾器鏈代理 -->
<bean id="filterChainProxy" class="org.acegisecurity.util.FilterChainProxy">
<property name="filterInvocationDefinitionSource">
<value>
CONVERT_URL_TO_LOWERCASE_BEFORE_COMPARISON
PATTERN_TYPE_APACHE_ANT
/login.jsp*=authenticationProcessingFilter
/login.faces*=authenticationProcessingFilter
/**=httpSessionContextIntegrationFilter,exceptionTranslationFilter,filterSecurityInterceptor
</value>
</property>
</bean>
<!-- 6. 關鍵過濾器 Bean 定義 -->
<bean id="authenticationProcessingFilter" class="org.acegisecurity.ui.webapp.AuthenticationProcessingFilter">
<property name="authenticationManager"><ref bean="authenticationManager"/></property>
<property name="authenticationFailureUrl">/login.jsp?error=true</property>
<property name="defaultTargetUrl">/dashboard.faces</property>
<property name="filterProcessesUrl">/j_security_check</property>
</bean>
<bean id="exceptionTranslationFilter" class="org.acegisecurity.ui.ExceptionTranslationFilter">
<property name="authenticationEntryPoint">
<bean class="org.acegisecurity.ui.webapp.AuthenticationProcessingFilterEntryPoint">
<property name="loginFormUrl">/login.jsp</property>
<property name="forceHttps">false</property>
</bean>
</property>
<property name="accessDeniedHandler">
<bean class="org.acegisecurity.ui.AccessDeniedHandlerImpl">
<property name="errorPage">/accessDenied.jsp</property>
</bean>
</property>
</bean>
<bean id="filterSecurityInterceptor" class="org.acegisecurity.intercept.web.FilterSecurityInterceptor">
<property name="authenticationManager"><ref bean="authenticationManager"/></property>
<property name="accessDecisionManager"><ref bean="accessDecisionManager"/></property>
<property name="objectDefinitionSource">
<value>
PATTERN_TYPE_APACHE_ANT
/admin/**=ROLE_ADMIN
/user/**=ROLE_USER
/dashboard.faces=ROLE_USER,ROLE_ADMIN
</value>
</property>
</bean>
<bean id="accessDecisionManager" class="org.acegisecurity.vote.AffirmativeBased">
<property name="decisionVoters">
<list><bean class="org.acegisecurity.vote.RoleVoter"/></list>
</property>
</bean>
<bean id="httpSessionContextIntegrationFilter" class="org.acegisecurity.context.HttpSessionContextIntegrationFilter"/>
</beans>
<form action="j_security_check" method="post">
帳號: <input type="text" name="j_username" /><br/>
密碼: <input type="password" name="j_password" /><br/>
<input type="submit" value="登入" />
<c:if test="${param.error}"><font color="red">帳號或密碼錯誤</font></c:if>
</form>
j_security_check (對應 filterProcessesUrl)。j_username、j_password。
<h:outputText value="歡迎, #{facesContext.externalContext.userPrincipal.name}" />
<h:commandButton value="登出" action="#{authBean.logout}" />
@Named
@RequestScoped
public class AuthBean {
public String logout() {
FacesContext.getCurrentInstance().getExternalContext().invalidateSession();
return "/login.jsp?faces-redirect=true";
}
}
*.faces 受保護,/login.jsp、/j_security_check、靜態資源 (/css/**, /js/**) 放行。objectDefinitionSource 使用 Ant 路徑模式,清晰可維護。h:form) 自動包含 Token。HttpSessionContextIntegrationFilter 配合容器 Session 複製或 Spring Session (Redis)。@EnableWebSecurity + WebSecurityConfigurerAdapter),功能更強 (OAuth2、JWT、Reactive)。Acegi/Spring Security 以「過濾器鏈 + 配置化安全」架構,實現了 JSF 應用的零代碼安全防護。核心在於 web.xml 正確串聯 ContextLoaderListener、ConfigureListener、FacesServlet、FilterToBeanProxy,並精心設計 acegi-config.xml 中的認證提供者、過濾器鏈順序、URL 授權規則。遵循「登入頁非 JSF、認證交給 Filter、授權基於角色、會話整合 Spring」原則,可構建企業級安全基線。現代專案建議直接採用 Spring Security 5+ Java Config,享受更豐富的認證協議支援與響應式編程模型。