Spring Security动态权限的实现方法详解

Spring Security动态权限的实现方法详解

什么是动态权限?

在传统的企业应用中,权限被存储在静态的权限表中,着重强调的是用户拥有哪些权限。但是在现实生活中,我们会发现企业的角色是十分复杂的,拥有权限表面看起来是不够的。例如,对于一个CRM系统,管理员可能需要对某些用户进行一些特殊的操作。这种情况下,我们需要实现动态权限,即在运行时动态授权,而不是在静态权限表中授权。

Spring Security如何实现动态权限?

Spring Security 是 Spring 生态中很重要的一部分,它是一个安全框架,提供了认证和授权等功能。在 Spring Security 中,我们可以通过定义一个自定义的 AccessDecisionVoter 实现访问决策的投票。具体来说,我们需要基于自己的业务逻辑和数据访问层,实现 AccessDecisionVoter 中的两个方法:

  1. supports(ConfigAttribute attribute),支持哪些 ConfigAttribute

  2. vote(Authentication authentication, Object object, Collection list) 检查当前登录用户是否满足访问保护规则

我们可以将自己的业务逻辑和数据库查询放到 supports(ConfigAttribute attribute) 方法中,检查当前用户是否允许访问某个资源,如果允许返回 ACCESS_GRANTED ,如果不允许返回 ACCESS_DENIED 。具体实现请看以下示例。

在这个例子中,假设我们需要实现一个 CRM 系统,某些操作需要特殊的权限,例如管理员和客户经理可以删除用户,而普通员工不能删除用户。

示例 1:动态权限实现方案一

首先我们需要创建一个接口来列出我们需要动态授权的资源,例如需要删除的用户:

public interface PermissionService {
    boolean hasPermission(Long userId, String permission);
}

这个接口有两个参数:用户 ID 和需要授权的操作名称。真正的授权操作会在业务逻辑层实现。下面我们实现这个接口:

@Service
public class PermissionServiceImpl implements PermissionService {

    private final UserRepository userRepository;

    @Autowired
    public PermissionServiceImpl(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public boolean hasPermission(Long userId, String permission) {
        User user = userRepository.findById(userId)
                .orElseThrow(() -> new UsernameNotFoundException("User not found"));

        // 管理员和客户经理可以删除用户
        return (user.getRole().equals(Role.ROLE_ADMIN) || user.getRole().equals(Role.ROLE_MANAGER))
                && permission.equals("delete_user");
    }
}

在这个实现中,我们首先从数据库中获取用户信息,然后判断用户是否是管理员或客户经理,并且是否有删除用户的权限。如果满足条件,返回 true ,否则返回 false 。

然后,我们需要实现一个 AccessDecisionVoter ,以便能够进行投票。

@Component
public class PermissionBasedVoter implements AccessDecisionVoter<Object> {

    private final PermissionService permissionService;

    public PermissionBasedVoter(PermissionService permissionService) {
        this.permissionService = permissionService;
    }

    @Override
    public boolean supports(ConfigAttribute attribute) {
        return attribute.getAttribute() != null && attribute.getAttribute().startsWith("PERMISSION_");
    }

    @Override
    public boolean supports(Class<?> clazz) {
        return true;
    }

    @Override
    public int vote(Authentication authentication, Object object, Collection<ConfigAttribute> attributes) {
        int result = ACCESS_ABSTAIN;

        for (ConfigAttribute attribute : attributes) {
            if (this.supports(attribute)) {
                result = ACCESS_DENIED;

                String permission = attribute.getAttribute().substring("PERMISSION_".length());

                if (authentication.isAuthenticated()
                        && authentication.getPrincipal() instanceof UserDetails) {
                    UserDetails userDetails = (UserDetails) authentication.getPrincipal();
                    Long userId = Long.parseLong(userDetails.getUsername());
                    if (permissionService.hasPermission(userId, permission)) {
                        return ACCESS_GRANTED;
                    }
                }
            }
        }

        return result;
    }
}

在这个实现中,我们主要是实现了两个方法 supports 和 vote 。supports 方法用于判断当前的 ConfigAttribute 是否是我们支持的类型,这里我们支持所有 ConfigAttribute 类型,只要它以 "PERMISSION_" 开头即可。

vote 方法用于检查当前登录用户是否授权访问某个资源。注意,这里我们将用户 ID 存放在登录凭据的用户名字段中。具体实现如下:

if (authentication.isAuthenticated()
        && authentication.getPrincipal() instanceof UserDetails) {
    UserDetails userDetails = (UserDetails) authentication.getPrincipal();
    Long userId = Long.parseLong(userDetails.getUsername());
    if (permissionService.hasPermission(userId, permission)) {
        return ACCESS_GRANTED;
    }
}

这段代码中,我们从凭据中获取用户 ID ,并且调用 PermissionService 中的 hasPermission 方法检查是否授权。如果授权,返回 ACCESS_GRANTED ;如果不授权,返回 ACCESS_DENIED 。

最后,我们需要在 Spring Security 中注册我们的投票器 PermissionBasedVoter 。

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private final PermissionBasedVoter permissionBasedVoter;

    public SecurityConfig(PermissionBasedVoter permissionBasedVoter) {
        this.permissionBasedVoter = permissionBasedVoter;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers(HttpMethod.DELETE, "/users/**").hasAuthority("PERMISSION_DELETE_USER")
                .anyRequest().authenticated()
                .accessDecisionManager(accessDecisionManager())
                .and().httpBasic()
                .and().csrf().disable();
    }

    @Bean
    public AccessDecisionManager accessDecisionManager() {
        List<AccessDecisionVoter<?>> decisionVoters
                = Arrays.asList(permissionBasedVoter, new RoleVoter(), new AuthenticatedVoter());
        return new AffirmativeBased(decisionVoters);
    }
}

在上面的代码中,我们将我们创建的投票器 PermissionBasedVoter 添加到投票器列表中。然后我们对 DELETE 访问授予 "PERMISSION_DELETE_USER" 权限,并将访问决策管理器设置为 accessDecisionManager() 。最后开启 HTTP Basic 认证,并禁用 CSRF 保护。

示例 2:动态权限实现方案二

除了上面的方案,我们还可以使用 Spring Expression Language(SpEL)和数据库支持来实现动态权限。这个方法要略微简单一些。

首先我们需要定义一个访问控制表,存储每个资源和需要的权限:

CREATE TABLE `access_control` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `resource` varchar(255) NOT NULL,
  `permission` varchar(255) NOT NULL,
  PRIMARY KEY (`id`)
);

然后我们需要在 Spring Security 配置中使用 SpEL 表达式定义访问规则:

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private final DataSource dataSource;

    public SecurityConfig(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers(HttpMethod.DELETE, "/users/**").access("hasPermissionToDeleteUser()")
                .anyRequest().authenticated()
                .accessDecisionManager(accessDecisionManager())
                .and().httpBasic()
                .and().csrf().disable();
    }

    @Bean
    public AccessDecisionManager accessDecisionManager() {
        List<AccessDecisionVoter<?>> decisionVoters
                = Arrays.asList(new RoleVoter(), new AuthenticatedVoter());
        return new AffirmativeBased(decisionVoters);
    }

    @Bean
    public JdbcMutableAclService aclService() {
        return new JdbcMutableAclService(dataSource, new LookupStrategyImpl(dataSource, aclCache(), aclAuthorizationStrategy(), consoleAuditLogger()));
    }

    @Bean
    public AclAuthorizationStrategy aclAuthorizationStrategy() {
        return new AclAuthorizationStrategyImpl(new SimpleGrantedAuthority("ROLE_ADMIN"));
    }

    @Bean
    public AuditLogger consoleAuditLogger() {
        return new ConsoleAuditLogger();
    }

    @Bean
    public EhCacheBasedAclCache aclCache() {
        return new EhCacheBasedAclCache(aclEhCacheFactoryBean().getObject(), permissionGrantingStrategy(), aclAuthorizationStrategy());
    }

    @Bean
    public EhCacheFactoryBean aclEhCacheFactoryBean() {
        EhCacheFactoryBean ehCacheFactoryBean = new EhCacheFactoryBean();
        ehCacheFactoryBean.setCacheManager(aclCacheManager().getObject());
        ehCacheFactoryBean.setCacheName("aclCache");
        return ehCacheFactoryBean;
    }

    @Bean
    public EhCacheManagerFactoryBean aclCacheManager() {
        return new EhCacheManagerFactoryBean();
    }

    @Bean
    public AclPermissionEvaluator aclPermissionEvaluator() {
        return new AclPermissionEvaluator(aclService());
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("user").password("{noop}password").roles("USER")
                .and()
                .withUser("admin").password("{noop}password").roles("ADMIN");
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        web
                .ignoring()
                .antMatchers("/resources/**");
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http
                .formLogin()
                    .loginPage("/login")
                    .permitAll()
                    .defaultSuccessUrl("/home")
                .and()
                    .logout()
                    .permitAll()
                    .logoutSuccessUrl("/login")
                .and()
                .authorizeRequests();
    }

    @Override
    public void configure(ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry) throws Exception {
        JdbcMutableAclService aclService = aclService();
        ObjectIdentityGenerator<Object> generator = new DefaultObjectIdentityGenerator<>();
        List<Acl> acls = new ArrayList<>();

        // 添加默认权限
        acls.add(aclService.createAcl(new ObjectIdentityImpl(SampleObject.class, generator.next()),
                Arrays.asList(new PrincipalSid("user"), new GrantedAuthoritySid("ROLE_USER")),
                Arrays.asList(BasePermission.READ, BasePermission.WRITE, BasePermission.DELETE)));

        // 添加动态权限
        acls.add(aclService.createAcl(new ObjectIdentityImpl(SampleObject.class, generator.next()),
                Arrays.asList(new PrincipalSid("admin"), new GrantedAuthoritySid("ROLE_ADMIN")),
                Arrays.asList(BasePermission.READ, BasePermission.WRITE)));

        AccessControlList acl = new AccessControlList();
        acl.setEntries(acls);
        acl.setOwner(new PrincipalSid("admin"));
        acl.setAuditLogger(consoleAuditLogger());

        // 将访问控制列表保存到数据库中
        JdbcMutableAclService jdbcMutableAclService = aclService();
        jdbcMutableAclService.setClassIdentityQuery("SELECT @@IDENTITY");
        jdbcMutableAclService.setSidIdentityQuery("SELECT @@IDENTITY");

        jdbcMutableAclService.save(acl);

        // 将 SpEL 所需的 bean 注册到 Spring 容器中
        registry
                .antMatchers(HttpMethod.GET, "/foo").access("hasRole('ROLE_USER')")
                .antMatchers(HttpMethod.DELETE, "/foo").access("hasPermissionToDelete()");
    }

}

在这个示例中,我们创建了一个访问控制表来存储资源和所需的权限信息。然后,在 configure(HttpSecurity http) 方法中,我们使用 SpEL 表达式来使用动态权限。其中,"hasPermissionToDelete()" 会调用我们先前定义的 aclPermissionEvaluator() 方法。最后,我们使用 JdbcMutableAclService 将访问控制列表保存到数据库中。

总结

通过上述两个示例,我们了解到了 Spring Security 如何实现动态权限。我们可以根据不同的业务需求,使用不同的方法来实现动态权限。我们也可以将访问控制列表保存在数据库中,以便随时更改资源和权限。

本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:Spring Security动态权限的实现方法详解 - Python技术站

(1)
上一篇 2023年5月20日
下一篇 2023年5月20日

相关文章

  • Java自定义函数调用方法解析

    Java自定义函数调用方法解析 在Java中,可以使用自定义函数实现对某些操作的封装,实现代码复用和简化调用。自定义函数的调用方法与Java内置函数的调用方法略有不同,需要注意以下几个方面。 一、函数定义 Java自定义函数的定义需要指定函数名和参数列表,可以有返回值也可以没有。 下面是一个无参数、无返回值的函数定义示例: public static voi…

    Java 2023年5月26日
    00
  • Java中的日期和时间类以及Calendar类用法详解

    Java中日期和时间类以及Calendar类用法详解 Java中有三个主要的日期时间类:Date、Calendar和SimpleDateFormat。在Java 8及以上版本中,还增加了新的日期时间API(即java.time包)。 1. Date类 日期类java.util.Date最初设计用于表示当前时间。Date自基准时间(1970年1月1日)以来的毫…

    Java 2023年5月20日
    00
  • 由浅入深快速掌握Java 数组的使用

    一、前言 Java数组是一种非常常用的数据结构,用于存储相同类型数据的集合。熟练掌握数组的使用对Java开发非常重要。本文将从浅入深,逐步介绍Java数组的基本概念,创建和初始化数组,访问数组元素,以及数组的遍历和排序等内容。 二、什么是Java数组 Java数组是存储同一数据类型的固定大小的顺序集合。它是由相同数据类型的元素构成的,这些元素可以通过索引进行…

    Java 2023年5月26日
    00
  • java中建立0-10m的消息(字符串)实现方法

    当需要在Java应用程序中建立0-10m的消息时,可以考虑使用下面三个步骤: 定义并使用字符串类 在Java中,我们可以使用String类来定义、操作和处理字符串。使用String类,我们可以通过构造函数、字符串字面值或者选择合适的字符串方法来创建、处理和操作字符串。如果需要连接两个字符串,可以使用+号操作符;如果要将字符串转换为整数、浮点数,可以使用各种强…

    Java 2023年5月27日
    00
  • 一篇文章彻底弄懂SpringBoot项目jdk版本及依赖不兼容问题

    下面是详细讲解“一篇文章彻底弄懂SpringBoot项目jdk版本及依赖不兼容问题”的完整攻略。 什么是SpringBoot项目? SpringBoot是一款基于Spring框架的轻量级Java开发框架,它使用了约定优于配置的方式,能够快速构建可独立运行的Spring应用程序。在SpringBoot框架中,它的依赖管理使用了maven或gradle进行版本控…

    Java 2023年5月19日
    00
  • 使用纯java config来配置spring mvc方式

    使用纯Java配置Spring MVC的方式需要借助于Spring的WebApplicationInitializer接口。WebApplicationInitializer是一个接口,它被用来实现ServletContextInitializer,在servlet3.0+容器中被自动使用。在这里,我们将WebApplicationInitializer用于…

    Java 2023年5月16日
    00
  • springboot配置templates直接访问的实现

    下面是springboot配置templates直接访问的实现攻略: 1、添加Maven依赖 在pom.xml文件中添加以下Maven依赖: <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-star…

    Java 2023年5月19日
    00
  • Java线程池高频面试题总结

    Java线程池高频面试题总结 线程池是什么 线程池是一种用于管理多个线程的机制,它能够根据应用程序需要动态地增减线程。线程池在执行完任务后并不会立即销毁线程,而是将线程放入池中等待下一次使用。线程池通常会预先准备好一定数量的线程,这些线程被称为核心线程,在需要时更多的线程将被创建。 为什么使用线程池 线程池有以下优点: 减少线程创建的开销: 创建线程需要花费…

    Java 2023年5月19日
    00
合作推广
合作推广
分享本页
返回顶部