Skip to content

授权与访问控制

授权是确定用户可以执行哪些操作的过程,与认证(确认用户身份)相辅相成。Spring Security 提供了全面的授权功能,支持多种授权策略和控制粒度。

授权核心概念

授权的基本流程

  1. 用户通过认证,获取身份信息和权限
  2. 用户尝试访问受保护的资源或执行操作
  3. 授权决策器检查用户是否有足够的权限
  4. 允许或拒绝访问

核心组件和接口

1. GrantedAuthority

GrantedAuthority 表示授予认证主体的权限,通常是角色或权限:

java
public interface GrantedAuthority extends Serializable {
    String getAuthority();
}

最常用的实现是 SimpleGrantedAuthority,它接受一个字符串作为权限名称:

java
public class SimpleGrantedAuthority implements GrantedAuthority {
    private final String role;
    
    public SimpleGrantedAuthority(String role) {
        this.role = role;
    }
    
    @Override
    public String getAuthority() {
        return role;
    }
}

2. AccessDecisionManager

AccessDecisionManager 负责做出最终的授权决策:

java
public interface AccessDecisionManager {
    void decide(Authentication authentication, Object secureObject, 
                Collection<ConfigAttribute> attributes) throws AccessDeniedException;
    boolean supports(ConfigAttribute attribute);
    boolean supports(Class<?> clazz);
}

Spring Security 提供了三种主要的决策管理器:

  • AffirmativeBased:只要有一个投票者允许访问,就授权访问(默认)
  • ConsensusBased:基于多数投票者的决定
  • UnanimousBased:只有当所有投票者都允许时才授权访问

3. AccessDecisionVoter

AccessDecisionVoter 参与授权决策,对给定的认证主体和安全对象进行投票:

java
public interface AccessDecisionVoter<S> {
    int ACCESS_GRANTED = 1;
    int ACCESS_ABSTAIN = 0;
    int ACCESS_DENIED = -1;
    
    int vote(Authentication authentication, S object, Collection<ConfigAttribute> attributes);
    boolean supports(ConfigAttribute attribute);
    boolean supports(Class<?> clazz);
}

主要的投票者包括:

  • RoleVoter:基于角色的投票者,检查 ROLE_ 前缀权限
  • AuthenticatedVoter:检查用户是否已通过认证
  • WebExpressionVoter:使用 SpEL 表达式做决策

4. SecurityMetadataSource

SecurityMetadataSource 提供安全对象(URL、方法等)的配置属性:

java
public interface SecurityMetadataSource {
    Collection<ConfigAttribute> getAttributes(Object object);
    Collection<ConfigAttribute> getAllConfigAttributes();
    boolean supports(Class<?> clazz);
}

授权方式

Spring Security 支持多种授权方式,可根据应用需求选择合适的方式。

1. 基于 URL 的授权

基于 URL 的授权是最常见的方式,通过配置规则控制对特定 URL 的访问:

java
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(authorize -> authorize
                .requestMatchers("/", "/home", "/public/**").permitAll()
                .requestMatchers("/admin/**").hasRole("ADMIN")
                .requestMatchers("/manager/**").hasAnyRole("ADMIN", "MANAGER")
                .requestMatchers("/api/**").hasAuthority("API_ACCESS")
                .requestMatchers("/db/**").access(new WebExpressionAuthorizationManager(
                    "hasRole('DATABASE_ADMIN') and hasRole('BACKUP_ADMIN')"))
                .anyRequest().authenticated()
            );
        
        return http.build();
    }
}

常用的匹配器方法包括:

  • permitAll():允许所有访问
  • denyAll():拒绝所有访问
  • authenticated():要求已认证的用户
  • hasRole(role):要求特定角色
  • hasAnyRole(roles...):要求任一指定角色
  • hasAuthority(authority):要求特定权限
  • hasAnyAuthority(authorities...):要求任一指定权限
  • access(expression):使用SpEL表达式控制访问

2. 方法级安全性

方法级安全允许在方法调用级别应用授权规则,适合服务层安全控制:

首先需要启用方法安全:

java
@Configuration
@EnableMethodSecurity
public class MethodSecurityConfig {
    // 配置...
}

然后可以在方法上使用安全注解:

java
@Service
public class UserService {

    @PreAuthorize("hasRole('ADMIN')")
    public List<User> findAllUsers() {
        // 方法实现...
    }
    
    @PreAuthorize("hasRole('ADMIN') or #username == authentication.principal.username")
    public User findUserByUsername(String username) {
        // 方法实现...
    }
    
    @PostAuthorize("returnObject.username == authentication.principal.username or hasRole('ADMIN')")
    public User findUserById(Long id) {
        // 方法实现...
    }
    
    @Secured("ROLE_ADMIN")
    public void deleteUser(Long id) {
        // 方法实现...
    }
    
    @PreAuthorize("hasPermission(#project, 'WRITE')")
    public void updateProject(Project project) {
        // 方法实现...
    }
}

主要的安全注解包括:

  • @PreAuthorize:在方法执行前进行授权检查
  • @PostAuthorize:在方法执行后进行授权检查,可以访问返回值
  • @Secured:简单的角色检查(旧 API)
  • @RolesAllowed:JSR-250 标准角色检查
  • @PreFilter:在执行方法前过滤集合参数
  • @PostFilter:在方法执行后过滤返回的集合

3. 领域对象安全 (ACL)

对于需要精细粒度控制的场景,Spring Security ACL 提供了对象级别的访问控制:

添加 ACL 依赖:

xml
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-acl</artifactId>
</dependency>

配置 ACL 服务:

java
@Configuration
@EnableMethodSecurity(prePostEnabled = true)
public class AclSecurityConfig {

    @Autowired
    private DataSource dataSource;

    @Bean
    public AclService aclService() {
        return new JdbcMutableAclService(dataSource, lookupStrategy(), aclCache());
    }
    
    @Bean
    public LookupStrategy lookupStrategy() {
        return new BasicLookupStrategy(dataSource, aclCache(), aclAuthorizationStrategy(), permissionGrantingStrategy());
    }
    
    @Bean
    public AclCache aclCache() {
        return new EhCacheBasedAclCache(aclEhCacheFactoryBean().getObject(), permissionGrantingStrategy(), aclAuthorizationStrategy());
    }
    
    @Bean
    public EhCacheFactoryBean aclEhCacheFactoryBean() {
        EhCacheFactoryBean factoryBean = new EhCacheFactoryBean();
        factoryBean.setCacheName("aclCache");
        return factoryBean;
    }
    
    @Bean
    public PermissionGrantingStrategy permissionGrantingStrategy() {
        return new DefaultPermissionGrantingStrategy(new ConsoleAuditLogger());
    }
    
    @Bean
    public AclAuthorizationStrategy aclAuthorizationStrategy() {
        return new AclAuthorizationStrategyImpl(new SimpleGrantedAuthority("ROLE_ADMIN"));
    }
    
    @Bean
    public MethodSecurityExpressionHandler expressionHandler() {
        DefaultMethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
        expressionHandler.setPermissionEvaluator(permissionEvaluator());
        return expressionHandler;
    }
    
    @Bean
    public PermissionEvaluator permissionEvaluator() {
        return new AclPermissionEvaluator(aclService());
    }
}

在服务方法中使用 ACL:

java
@Service
public class DocumentService {

    @PreAuthorize("hasPermission(#document, 'READ')")
    public Document getDocument(Document document) {
        // 方法实现...
    }
    
    @PreAuthorize("hasPermission(#document, 'WRITE')")
    public void updateDocument(Document document) {
        // 方法实现...
    }
    
    @PreAuthorize("hasPermission(#id, 'com.example.Document', 'READ')")
    public Document getDocumentById(Long id) {
        // 方法实现...
    }
}

为对象设置 ACL 权限:

java
@Service
public class DocumentManager {

    @Autowired
    private MutableAclService aclService;
    
    @Transactional
    public void createDocument(Document document, String username) {
        // 保存文档...
        
        // 创建 ACL
        ObjectIdentity objectIdentity = new ObjectIdentityImpl(Document.class, document.getId());
        MutableAcl acl = aclService.createAcl(objectIdentity);
        
        // 授予所有者完全控制权限
        Sid owner = new PrincipalSid(username);
        acl.insertAce(0, BasePermission.ADMINISTRATION, owner, true);
        acl.insertAce(1, BasePermission.READ, owner, true);
        acl.insertAce(2, BasePermission.WRITE, owner, true);
        acl.insertAce(3, BasePermission.DELETE, owner, true);
        
        // 设置所有者
        acl.setOwner(owner);
        
        // 保存 ACL
        aclService.updateAcl(acl);
    }
    
    @Transactional
    public void grantAccess(Document document, String username, Permission permission) {
        ObjectIdentity objectIdentity = new ObjectIdentityImpl(Document.class, document.getId());
        MutableAcl acl = (MutableAcl) aclService.readAclById(objectIdentity);
        
        Sid sid = new PrincipalSid(username);
        acl.insertAce(acl.getEntries().size(), permission, sid, true);
        
        aclService.updateAcl(acl);
    }
}

自定义授权

1. 自定义投票者

可以创建自定义投票者实现特殊的授权逻辑:

java
public class CustomerTypeVoter implements AccessDecisionVoter<Object> {

    @Override
    public boolean supports(ConfigAttribute attribute) {
        return attribute.getAttribute() != null && 
               attribute.getAttribute().startsWith("CUSTOMER_TYPE_");
    }
    
    @Override
    public boolean supports(Class<?> clazz) {
        return true;
    }
    
    @Override
    public int vote(Authentication authentication, Object object, 
                   Collection<ConfigAttribute> attributes) {
        if (!(authentication.getPrincipal() instanceof UserDetails)) {
            return ACCESS_ABSTAIN;
        }
        
        UserDetails userDetails = (UserDetails) authentication.getPrincipal();
        
        // 假设用户详情中包含客户类型
        String customerType = ((CustomUserDetails) userDetails).getCustomerType();
        
        for (ConfigAttribute attribute : attributes) {
            if (attribute.getAttribute() == null) {
                continue;
            }
            
            if (attribute.getAttribute().startsWith("CUSTOMER_TYPE_")) {
                String requiredType = attribute.getAttribute().replace("CUSTOMER_TYPE_", "");
                
                if (customerType.equals(requiredType)) {
                    return ACCESS_GRANTED;
                }
            }
        }
        
        return ACCESS_DENIED;
    }
}

注册自定义投票者:

java
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.authorizeHttpRequests().anyRequest().authenticated();
        return http.build();
    }
    
    @Bean
    public AccessDecisionManager accessDecisionManager() {
        List<AccessDecisionVoter<?>> voters = new ArrayList<>();
        voters.add(new RoleVoter());
        voters.add(new AuthenticatedVoter());
        voters.add(new CustomerTypeVoter());
        voters.add(new WebExpressionVoter());
        
        return new AffirmativeBased(voters);
    }
}

2. 自定义权限评估器

自定义权限评估器可以实现复杂的权限逻辑:

java
public class CustomPermissionEvaluator implements PermissionEvaluator {

    @Autowired
    private ProjectRepository projectRepository;
    
    @Autowired
    private TaskRepository taskRepository;

    @Override
    public boolean hasPermission(Authentication authentication, Object targetDomainObject, 
                                Object permission) {
        if (authentication == null || targetDomainObject == null || permission == null) {
            return false;
        }
        
        String permissionString = permission.toString();
        
        if (targetDomainObject instanceof Project) {
            return hasProjectPermission(authentication, (Project) targetDomainObject, permissionString);
        }
        
        if (targetDomainObject instanceof Task) {
            return hasTaskPermission(authentication, (Task) targetDomainObject, permissionString);
        }
        
        return false;
    }
    
    @Override
    public boolean hasPermission(Authentication authentication, Serializable targetId, 
                                String targetType, Object permission) {
        if (authentication == null || targetId == null || targetType == null || permission == null) {
            return false;
        }
        
        String permissionString = permission.toString();
        
        if (targetType.equals("com.example.Project")) {
            Project project = projectRepository.findById((Long) targetId).orElse(null);
            if (project == null) {
                return false;
            }
            return hasProjectPermission(authentication, project, permissionString);
        }
        
        if (targetType.equals("com.example.Task")) {
            Task task = taskRepository.findById((Long) targetId).orElse(null);
            if (task == null) {
                return false;
            }
            return hasTaskPermission(authentication, task, permissionString);
        }
        
        return false;
    }
    
    private boolean hasProjectPermission(Authentication authentication, Project project, 
                                        String permission) {
        String username = authentication.getName();
        
        // 项目拥有者有完全权限
        if (project.getOwner().equals(username)) {
            return true;
        }
        
        // 项目成员权限
        if (project.getMembers().contains(username)) {
            return permission.equals("READ") || permission.equals("COMMENT");
        }
        
        // 管理员权限
        return authentication.getAuthorities().stream()
                .anyMatch(auth -> auth.getAuthority().equals("ROLE_ADMIN"));
    }
    
    private boolean hasTaskPermission(Authentication authentication, Task task, 
                                     String permission) {
        // 任务权限逻辑
        // ...
        
        return false;
    }
}

注册自定义权限评估器:

java
@Configuration
@EnableMethodSecurity
public class MethodSecurityConfig {

    @Bean
    public MethodSecurityExpressionHandler methodSecurityExpressionHandler() {
        DefaultMethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
        expressionHandler.setPermissionEvaluator(customPermissionEvaluator());
        return expressionHandler;
    }
    
    @Bean
    public PermissionEvaluator customPermissionEvaluator() {
        return new CustomPermissionEvaluator();
    }
}

3. 自定义安全表达式

可以创建自定义表达式方法扩展 SpEL 安全表达式:

java
public class CustomSecurityExpressions {

    public boolean isProjectMember(Authentication authentication, Project project) {
        String username = authentication.getName();
        return project.getMembers().contains(username);
    }
    
    public boolean hasHigherRankThan(Authentication authentication, User otherUser) {
        User currentUser = ((UserDetails) authentication.getPrincipal()).getUser();
        return currentUser.getRank() > otherUser.getRank();
    }
    
    public boolean canAccessCompanyData(Authentication authentication, String companyCode) {
        UserDetails userDetails = (UserDetails) authentication.getPrincipal();
        if (userDetails instanceof EmployeeDetails) {
            return ((EmployeeDetails) userDetails).getCompanyCode().equals(companyCode);
        }
        return false;
    }
}

注册自定义表达式根:

java
@Configuration
@EnableMethodSecurity
public class MethodSecurityConfig {

    @Bean
    public MethodSecurityExpressionHandler methodSecurityExpressionHandler() {
        DefaultMethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
        expressionHandler.setPermissionEvaluator(permissionEvaluator());
        
        // 设置自定义表达式根
        expressionHandler.setExpressionParser(
            new SpelExpressionParser(new SpelParserConfiguration(true, true))
        );
        expressionHandler.setPermissionCacheOptimizer(
            new DefaultMethodSecurityExpressionHandler().getPermissionCacheOptimizer());
        
        return expressionHandler;
    }
    
    @Bean
    public SecurityExpressionRoot customSecurityExpressionRoot() {
        return new CustomMethodSecurityExpressionRoot(new CustomSecurityExpressions());
    }
}

public class CustomMethodSecurityExpressionRoot extends SecurityExpressionRoot 
        implements MethodSecurityExpressionOperations {
    
    private final CustomSecurityExpressions customExpressions;
    private Object filterObject;
    private Object returnObject;
    private Object target;
    
    public CustomMethodSecurityExpressionRoot(Authentication authentication, 
                                             CustomSecurityExpressions customExpressions) {
        super(authentication);
        this.customExpressions = customExpressions;
    }
    
    public boolean isProjectMember(Project project) {
        return customExpressions.isProjectMember(getAuthentication(), project);
    }
    
    public boolean hasHigherRankThan(User otherUser) {
        return customExpressions.hasHigherRankThan(getAuthentication(), otherUser);
    }
    
    public boolean canAccessCompanyData(String companyCode) {
        return customExpressions.canAccessCompanyData(getAuthentication(), companyCode);
    }
    
    // MethodSecurityExpressionOperations接口实现
    @Override
    public void setFilterObject(Object filterObject) {
        this.filterObject = filterObject;
    }

    @Override
    public Object getFilterObject() {
        return filterObject;
    }

    @Override
    public void setReturnObject(Object returnObject) {
        this.returnObject = returnObject;
    }

    @Override
    public Object getReturnObject() {
        return returnObject;
    }

    @Override
    public Object getThis() {
        return target;
    }
    
    public void setThis(Object target) {
        this.target = target;
    }
}

授权相关的高级特性

1. 基于层次的角色

角色层次允许高级角色自动拥有低级角色的所有权限:

java
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public RoleHierarchy roleHierarchy() {
        RoleHierarchyImpl hierarchy = new RoleHierarchyImpl();
        hierarchy.setHierarchy(
            "ROLE_ADMIN > ROLE_MANAGER\n" +
            "ROLE_MANAGER > ROLE_USER\n" +
            "ROLE_USER > ROLE_GUEST"
        );
        return hierarchy;
    }
    
    @Bean
    public SecurityExpressionHandler<FilterInvocation> expressionHandler() {
        DefaultWebSecurityExpressionHandler handler = new DefaultWebSecurityExpressionHandler();
        handler.setRoleHierarchy(roleHierarchy());
        return handler;
    }
    
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(authorize -> authorize
                .expressionHandler(expressionHandler())
                .requestMatchers("/admin/**").hasRole("ADMIN")
                .requestMatchers("/manager/**").hasRole("MANAGER")
                .requestMatchers("/user/**").hasRole("USER")
                .requestMatchers("/guest/**").hasRole("GUEST")
                .anyRequest().authenticated()
            );
        
        return http.build();
    }
}

2. 运行时权限管理

有时需要动态调整权限,而不是硬编码在配置中:

java
@Service
public class DynamicPermissionService {

    @Autowired
    private RolePermissionRepository rolePermissionRepository;
    
    private Map<String, List<String>> rolePermissionsCache = new ConcurrentHashMap<>();
    
    @PostConstruct
    public void init() {
        refreshPermissionsCache();
    }
    
    public void refreshPermissionsCache() {
        Map<String, List<String>> newCache = rolePermissionRepository.findAll().stream()
            .collect(Collectors.groupingBy(
                RolePermission::getRole,
                Collectors.mapping(RolePermission::getPermission, Collectors.toList())
            ));
        
        rolePermissionsCache = newCache;
    }
    
    public boolean hasPermission(Authentication auth, String requiredPermission) {
        return auth.getAuthorities().stream()
            .map(GrantedAuthority::getAuthority)
            .filter(role -> role.startsWith("ROLE_"))
            .anyMatch(role -> {
                List<String> permissions = rolePermissionsCache.getOrDefault(role, Collections.emptyList());
                return permissions.contains(requiredPermission);
            });
    }
}

@Component
public class DynamicPermissionVoter implements AccessDecisionVoter<Object> {

    @Autowired
    private DynamicPermissionService permissionService;
    
    @Override
    public boolean supports(ConfigAttribute attribute) {
        return attribute.getAttribute() != null && 
               attribute.getAttribute().startsWith("PERM_");
    }
    
    @Override
    public boolean supports(Class<?> clazz) {
        return true;
    }
    
    @Override
    public int vote(Authentication authentication, Object object, 
                   Collection<ConfigAttribute> attributes) {
        if (authentication == null) {
            return ACCESS_DENIED;
        }
        
        for (ConfigAttribute attribute : attributes) {
            if (attribute.getAttribute() == null) {
                continue;
            }
            
            if (attribute.getAttribute().startsWith("PERM_")) {
                String permission = attribute.getAttribute().substring(5);
                
                if (permissionService.hasPermission(authentication, permission)) {
                    return ACCESS_GRANTED;
                }
            }
        }
        
        return ACCESS_DENIED;
    }
}

3. OAuth2 资源服务器授权

对于 OAuth2 保护的 API,可以配置资源服务器授权:

java
@Configuration
@EnableWebSecurity
public class ResourceServerConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(authorize -> authorize
                .requestMatchers("/api/public/**").permitAll()
                .requestMatchers("/api/user/**").hasAuthority("SCOPE_read")
                .requestMatchers("/api/admin/**").hasAuthority("SCOPE_admin")
                .anyRequest().authenticated()
            )
            .oauth2ResourceServer(oauth2 -> oauth2
                .jwt(jwt -> jwt
                    .jwtAuthenticationConverter(jwtAuthenticationConverter())
                )
            );
        
        return http.build();
    }
    
    @Bean
    public JwtAuthenticationConverter jwtAuthenticationConverter() {
        JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
        grantedAuthoritiesConverter.setAuthorityPrefix("SCOPE_");
        
        JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
        jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter);
        
        return jwtAuthenticationConverter;
    }
}

授权最佳实践

  1. 粒度适当原则:选择合适的授权粒度,过细会增加复杂度,过粗则缺乏灵活性
  2. 最小权限原则:用户只应被授予完成其工作所需的最小权限集
  3. 职责分离:将敏感操作分解为多个步骤,由不同角色的用户执行
  4. 显式拒绝优先:在安全策略冲突时,拒绝访问的规则应优先于允许访问的规则
  5. 定期审查权限:定期审查和清理授权规则,移除不再需要的权限
  6. 详细记录授权决策:记录所有重要的授权决策,便于审计和问题排查
  7. 优先使用声明式安全:声明式安全(注解)比编程式安全更清晰、可维护
  8. 避免硬编码权限:使用配置或数据库存储权限规则,便于管理和调整

总结

Spring Security 提供了全面的授权功能,从简单的基于角色的访问控制到复杂的领域对象安全。通过合理配置和扩展这些功能,可以实现从粗粒度的 URL 安全到细粒度的方法和对象级安全控制。在实际应用中,应权衡安全需求和复杂度,选择合适的授权策略,并遵循安全最佳实践,构建既安全又易于使用的系统。