首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

Spring Security 2 動態權限實戰:基於 AccessDecisionVoter 的資源角色動態匹配方案

     編輯:Java 相關知識

背景痛點

Spring Security 2 引入命名空間配置(<security:http>、<security:authentication-provider>)極大簡化了配置,但同時將資源-角色映射邏輯封裝在內部類(如 DefaultFilterInvocationSecurityMetadataSource)中。傳統 Acegi 時代通過繼承 AbstractFilterInvocationDefinitionSource 實現動態權限的擴展點被隱藏,導致:

  • 無法從數據庫/配置文件動態加載資源-角色映射
  • 強行繼承內部類違背開閉原則,侵入性強且版本脆弱
  • URL 配置與動態權限難以共存

解決方案:自定義 AccessDecisionVoter

Spring Security 的決策管理器 AccessDecisionManager 採用投票機制,支持多 AccessDecisionVoter 組合。我們實現 DynamicRoleVoter,在投票階段動態解析資源所需角色並與用戶權限匹配,完全不影響現有 URL 配置。

核心類設計

1. 動態角色提供者介面

public interface DynamicRoleProvider {
    /**
     * 根據請求資源獲取所需角色集合
     * @param resourceObject FilterInvocation 等資源對象
     * @return 角色名稱集合,如 {"ROLE_ADMIN", "ROLE_USER"}
     */
    Collection<String> getRequiredRoles(Object resourceObject);
}

2. 投票器實現

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;
    }
}

Spring 配置整合

<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
開閉原則違背遵循

最佳實踐建議

  1. 緩存策略:資源-角色映射變更頻率低,建議引入本地緩存(Guava/Caffeine)+ 定時刷新或消息驅動失效。
  2. 降級處理:DynamicRoleProvider 異常時返回空集合,投票器棄權,避免單點故障導致全系統拒訪。
  3. 權限表達式:若需支持 SpEL 表達式(如 hasRole('ADMIN') or @perm.check()),可改用 WebExpressionVoter 並注入自定義 SecurityExpressionHandler。

結語

通過 AccessDecisionVoter 扩展點實現動態權限,是 Spring Security 架構設計的精髓體現:核心流程固化,業務邏輯插件化。此方案既保留了命名空間配置的簡潔性,又賦予了權限系統極致的動態擴展能力,是中大型項目權限演進的推薦路徑。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved