Spring Security 2 引入命名空間配置(<security:http>、<security:authentication-provider>)極大簡化了配置,但同時將資源-角色映射邏輯封裝在內部類(如 DefaultFilterInvocationSecurityMetadataSource)中。傳統 Acegi 時代通過繼承 AbstractFilterInvocationDefinitionSource 實現動態權限的擴展點被隱藏,導致:
Spring Security 的決策管理器 AccessDecisionManager 採用投票機制,支持多 AccessDecisionVoter 組合。我們實現 DynamicRoleVoter,在投票階段動態解析資源所需角色並與用戶權限匹配,完全不影響現有 URL 配置。
public interface DynamicRoleProvider {
/**
* 根據請求資源獲取所需角色集合
* @param resourceObject FilterInvocation 等資源對象
* @return 角色名稱集合,如 {"ROLE_ADMIN", "ROLE_USER"}
*/
Collection<String> getRequiredRoles(Object resourceObject);
}
public class DynamicRoleVoter implements AccessDecisionVoter {
private DynamicRoleProvider dynamicRoleProvider;
public void setDynamicRoleProvider(DynamicRoleProvider provider) {
this.dynamicRoleProvider = provider;
}
@Override
public boolean supports(ConfigAttribute attribute) {
return true; // 支持所有屬性
}
@Override
public boolean supports(Class<?> clazz) {
return true; // 支持所有資源類型
}
@Override
public int vote(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) {
// 1. 獲取用戶擁有的角色
Collection<GrantedAuthority> userAuthorities = authentication.getAuthorities();
Set<String> userRoles = new HashSet<>();
for (GrantedAuthority auth : userAuthorities) {
userRoles.add(auth.getAuthority());
}
// 2. 通過提供者獲取資源動態所需角色
Collection<String> requiredRoles = dynamicRoleProvider.getRequiredRoles(object);
if (requiredRoles == null || requiredRoles.isEmpty()) {
return ACCESS_ABSTAIN; // 無動態要求,棄權
}
// 3. 角色匹配:任一角色滿足即通過
for (String role : requiredRoles) {
if (userRoles.contains(role)) {
return ACCESS_GRANTED;
}
}
return ACCESS_DENIED;
}
}
<beans:bean id="accessDecisionManager" class="org.springframework.security.vote.AffirmativeBased">
<beans:property name="decisionVoters">
<beans:list>
<beans:bean class="org.springframework.security.vote.RoleVoter" />
<beans:bean class="org.springframework.security.vote.AuthenticatedVoter" />
<beans:bean class="com.example.security.DynamicRoleVoter">
<beans:property name="dynamicRoleProvider" ref="dynamicRoleProvider" />
</beans:bean>
</beans:list>
</beans:property>
</beans:bean>
<beans:bean id="dynamicRoleProvider" class="com.example.security.DBDynamicRoleProvider">
<!-- 注入 DataSource、Cache 等依賴 -->
</beans:bean>
public class DBDynamicRoleProvider implements DynamicRoleProvider {
private JdbcTemplate jdbcTemplate;
private LoadingCache<String, Collection<String>> resourceRoleCache;
public DBDynamicRoleProvider(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
this.resourceRoleCache = CacheBuilder.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.build(new CacheLoader<String, Collection<String>>() {
@Override
public Collection<String> load(String resourceKey) {
return loadRolesFromDB(resourceKey);
}
});
}
@Override
public Collection<String> getRequiredRoles(Object resourceObject) {
if (resourceObject instanceof FilterInvocation) {
FilterInvocation fi = (FilterInvocation) resourceObject;
String url = fi.getRequestUrl(); // 如 /api/users/**
try {
return resourceRoleCache.get(url);
} catch (ExecutionException e) {
return Collections.emptyList();
}
}
return Collections.emptyList();
}
private Collection<String> loadRolesFromDB(String url) {
String sql = "SELECT role_name FROM resource_roles WHERE url_pattern = ?";
return jdbcTemplate.queryForList(sql, String.class, url);
}
}
| 維度 | 傳統繼承方案 | Voter 組合方案 |
|---|---|---|
| 侵入性 | 高(繼承內部類) | 零(標準介面實現) |
| 兼容性 | 破壞命名空間配置 | 完美共存 |
| 擴展性 | 單一繼承鏈限制 | 多 Voter 靈活組合 |
| 緩存支持 | 需自行實現 | 自然集成 Guava/Caffeine |
| 開閉原則 | 違背 | 遵循 |
DynamicRoleProvider 異常時返回空集合,投票器棄權,避免單點故障導致全系統拒訪。hasRole('ADMIN') or @perm.check()),可改用 WebExpressionVoter 並注入自定義 SecurityExpressionHandler。通過 AccessDecisionVoter 扩展點實現動態權限,是 Spring Security 架構設計的精髓體現:核心流程固化,業務邏輯插件化。此方案既保留了命名空間配置的簡潔性,又賦予了權限系統極致的動態擴展能力,是中大型項目權限演進的推薦路徑。