Spring Security 2 引入基於 Schema 的命名空間配置,將 Acegi 時代繁瑣的 Bean 定義與過濾器鏈手工排序簡化為聲明式標籤。本文系統梳理最小化配置、核心元素解析、過濾器鏈內部機制、數據庫用戶認證實現、以及資源授權的領域模型設計。
<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
注意:springSecurityFilterChain 為命名空間自動創建的 Bean ID,不可在業務配置中重複使用。
<http auto-config="true">
<intercept-url pattern="/**" access="ROLE_USER"/>
</http>
<authentication-provider>
<user-service>
<user name="admin" password="admin" authorities="ROLE_ADMIN,ROLE_USER"/>
<user name="user" password="user" authorities="ROLE_USER"/>
</user-service>
</authentication-provider>
auto-config="true" 等價於:
<http>
<intercept-url pattern="/**" access="ROLE_USER"/>
<form-login /> <!-- 表單登錄 -->
<anonymous /> <!-- 匿名訪問 -->
<http-basic /> <!-- HTTP Basic 認證 -->
<logout /> <!-- 登出處理 -->
<remember-me /> <!-- 記住我 -->
</http>
<form-login
login-page="/login.jsp"
authentication-failure-url="/login.jsp?error=true"
default-target-url="/dashboard"/>
<logout logout-success-url="/login.jsp"/>
<remember-me key="myAppKey" token-validity-seconds="1209600"/> <!-- 14 天 -->
| 維度 | Acegi 1.x (Bean) | Spring Security 2 (Namespace) |
|---|---|---|
| 代碼量 | 數十個 Bean 定義 | 幾行標籤 |
| 過濾器鏈排序 | 手工維護 FilterChainProxy 順序 | 框架內置強制順序 |
| 錯誤易發性 | 高 (順序錯誤難診斷) | 低 (結構約束) |
| 擴展性 | 完全可控 | 受限於內置鏈,但支援 custom-filter |
1. SecurityContextPersistenceFilter (HttpSession 上下文持久化)
2. LogoutFilter (登出處理)
3. AuthenticationProcessingFilter (表單登錄 /login_process)
4. SecurityContextHolderAwareRequestFilter (Servlet API 包裝)
5. ExceptionTranslationFilter (異常轉換、入口點重定向)
6. FilterSecurityInterceptor (URL 權限攔截 - 核心決策點)
不可替換:第 1、5、6 個過濾器。
可擴展:custom-filter position="AUTHENTICATION_PROCESSING_FILTER" 或 before/after 插入自定義過濾器。
<authentication-provider>
<jdbc-user-service data-source-ref="dataSource"
users-by-username-query="SELECT username, password, enabled FROM users WHERE username=?"
authorities-by-username-query="SELECT u.username, r.name FROM users u
JOIN user_role ur ON u.id=ur.user_id
JOIN roles r ON r.id=ur.role_id
WHERE u.username=?"/>
<password-encoder hash="md5"/>
</authentication-provider>
@Entity
public class User implements UserDetails {
@Id @GeneratedValue private Long id;
private String name, password;
private boolean disabled;
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(name="user_role", joinColumns=@JoinColumn(name="user_id"),
inverseJoinColumns=@JoinColumn(name="role_id"))
private Set roles;
// UserDetails 實現
public GrantedAuthority[] getAuthorities() {
return roles.stream()
.map(r -> new GrantedAuthorityImpl(r.getName()))
.toArray(GrantedAuthority[]::new);
}
public String getPassword() { return password; }
public String getUsername() { return name; }
public boolean isEnabled() { return !disabled; }
// isAccountNonExpired/Locked/CredentialsNonExpired 均返回 true
}
@Repository("securityManager")
public class SecurityManagerImpl extends HibernateDaoSupport implements UserDetailsService {
@Autowired public void init(SessionFactory sf) { setSessionFactory(sf); }
public UserDetails loadUserByUsername(String username) {
List users = getHibernateTemplate().find(
"FROM User u WHERE u.name=? AND u.disabled=false", username);
if (users.isEmpty()) throw new UsernameNotFoundException("User not found: " + username);
return users.get(0); // User 已實現 UserDetails
}
}
<authentication-provider user-service-ref="securityManager">
<password-encoder hash="md5"/>
</authentication-provider>
@Entity
public class Role {
@Id @GeneratedValue private Long id;
private String name, description;
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(name="role_resource", joinColumns=@JoinColumn(name="role_id"),
inverseJoinColumns=@JoinColumn(name="resource_id"))
private Set resources;
}
@Entity
public class Resource {
@Id @GeneratedValue private Long id;
private String type; // "URL", "METHOD", "MENU"
private String value; // "/admin/**", "userService.delete", "menu.userMgmt"
@ManyToMany(mappedBy="resources")
private Set roles;
}
問題:Spring Security 內置 FilterSecurityInterceptor 不可替換,無法直接注入動態資源決策邏輯。
突破:實現自定義 FilterInvocationDefinitionSource,通過 custom-filter after="LAST" 追加第二個 FilterSecurityInterceptor 實例,形成雙重攔截機制 (詳見下篇)。
auto-config,顯式聲明所需元素,避免匿名訪問、HTTP Basic 等非預期功能開啟BCryptPasswordEncoder (Spring Security 3.1+),棄用 MD5/SHA<csrf/> (3.2+ 默認開啟),REST API 可選擇性關閉<session-management session-fixation-protection="migrateSession"/>SecurityContextHolderStrategy.MODE_INHERITABLETHREADLOCAL掌握命名空間核心語義、內置鏈結構與擴展點,是驾驭 Spring Security 進行精細化安全控制的前提。