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日

相关文章

  • 下载站常用的点击下载地址提示设hao123为首页的js代码

    下载站常用的点击下载地址提示设hao123为首页的js代码,可以帮助网站引导用户将hao123设为浏览器的主页,从而提升网站的用户使用体验。下面是这个js代码的完整攻略。 在HTML文件中引入js文件 在标签中添加以下代码: <script src="js/hao123.js"></script> 注意:这里的路径…

    Java 2023年6月16日
    00
  • Java编码摘要算法实例解析

    Java编码摘要算法实例解析 在Java编程语言中,提供了一种编码摘要算法,可以将一段文本或文件转换成一段固定长度的唯一代码,称作“哈希值”或“数字指纹”。这个算法被广泛应用于身份认证、数字签名、数据完整性验证等场景中。 哈希算法概述 哈希算法将任意长度的二进制文本信息转换成固定长度的数字(通常是32、64、128或256位等长度),这个数字就是哈希值,也称…

    Java 2023年5月20日
    00
  • Spring Boot中使用jdbctemplate 操作MYSQL数据库实例

    下面我来详细讲解一下在Spring Boot中如何使用jdbctemplate操作MySQL数据库的方法。 准备工作 首先,我们需要在pom.xml文件中将以下依赖项添加到项目中: <dependency> <groupId>org.springframework.boot</groupId> <artifactId…

    Java 2023年6月16日
    00
  • Bootstrap实现翻页效果

    大致步骤如下: 1. 引入Bootstrap库 在头部引入Bootstrap的css和js文件 <link rel="stylesheet" href="https://cdn.bootcss.com/bootstrap/4.3.1/css/bootstrap.min.css"> <script sr…

    Java 2023年6月15日
    00
  • Spring Security短信验证码实现详解

    Spring Security短信验证码实现详解 简介 Spring Security是一个功能强大的认证和授权框架。它提供了多种认证方案,包括用户名密码认证、OAuth2.0认证等。但是默认情况下,Spring Security没有提供短信验证码认证的实现。因此,如果我们需要在Spring Security中实现短信验证码认证,需要自己进行实现。 本文将详…

    Java 2023年6月3日
    00
  • Keycloak各种配置及API的使用说明

    Keycloak各种配置及API的使用说明 前言 Keycloak是一个完整的开源身份和访问管理解决方案,它提供了一组统一的API,可用于管理身份验证、授权和保护应用和服务。 本文将详细介绍如何配置Keycloak以及如何使用其API进行身份验证、授权等操作。 配置Keycloak 创建一个Keycloak Realm 登录Keycloak控制台,选择左侧的…

    Java 2023年5月20日
    00
  • SpringBoot 使用 Sa-Token 完成权限认证

    一、设计思路 所谓权限认证,核心逻辑就是判断一个账号是否拥有指定权限: 有,就让你通过。 没有?那么禁止访问! 深入到底层数据中,就是每个账号都会拥有一个权限码集合,框架来校验这个集合中是否包含指定的权限码。 例如:当前账号拥有权限码集合 [“user-add”, “user-delete”, “user-get”],这时候我来校验权限 “user-upda…

    Java 2023年4月25日
    00
  • 关于maven全局配置文件settings.xml解析

    我将为您详细讲解关于Maven全局配置文件settings.xml的解析攻略。 什么是Maven全局配置文件settings.xml? Maven全局配置文件settings.xml是Maven的主配置文件,位于Maven的安装目录下的conf目录中。默认情况下,该文件是不存在的,需要手动创建。该文件可以用于配置Maven的全局配置信息,如Maven安装仓库…

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