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日

相关文章

  • Spring Boot thymeleaf模板引擎的使用详解

    感谢你对Spring Boot和Thymeleaf模板引擎的关注。下面是Spring Boot Thymeleaf模板引擎的使用详解攻略: 1. Thymeleaf简介 Thymeleaf是一个现代化的服务器端Java模板引擎,可以将模板渲染成HTML、XML、JavaScript等格式,并提供模板缓存机制,允许HTML页面的热部署。 2. Spring B…

    Java 2023年6月15日
    00
  • java比较两个json文件的差异及说明

    Java比较两个JSON文件的差异及说明 在日常开发中,我们经常需要比较两个JSON文件之间的差异,以判断其中的数据是否有更新或者变化。Java提供了许多方式来实现JSON文件的比较,下面将详细介绍其中的常用方法。 一、JSON文件的读取 在对JSON文件进行比较之前,我们需要先读取这两个JSON文件中的数据。 // 读取JSON文件中的内容 public …

    Java 2023年5月26日
    00
  • 【Jmeter】Request1输出作为Request2输入-后置处理器

    【Jmeter】基础介绍-详细 接上文,继续介绍Jmeter,本文关注点为如何解决上文中提到的第一个问题,即: 需要实现Request1的返回作为Request2的RequestBody或Header Jmeter支持后置处理器,即对http请求(或其他取样器)的返回值进行提取并赋值给变量。 本例中从Request1的ResponseBody中提取token…

    Java 2023年4月22日
    00
  • 详解IDEA创建Tomcat8源码工程流程

    下面是详解IDEA创建Tomcat8源码工程流程的完整攻略。 1. 下载并导入Tomcat8源码 首先,需要前往Tomcat官网下载Tomcat8源码,并解压到本地。然后,在IntelliJ IDEA中选择“File” > “New” > “Project from Existing Sources”打开源码文件夹,依次点击“Next”,在询问是…

    Java 2023年5月19日
    00
  • Layer弹出层动态获取数据的方法

    Layer弹出层是一款基于jQuery的Web弹出组件,它具有美观、易用、功能强大的特点。在开发时,可能需要在弹出层中展示动态获取的数据。本攻略将详细说明“Layer弹出层动态获取数据的方法”。 步骤1:引入jQuery库和layer.js文件 Layer弹出层组件基于jQuery,使用前需要先确认页面中已经引入了jQuery库,以便后续使用。 <!-…

    Java 2023年6月16日
    00
  • Spring Boot部署到Tomcat过程中遇到的问题汇总

    下面我将为你详细讲解“Spring Boot部署到Tomcat过程中遇到的问题汇总”的完整攻略。 一、背景知识 在部署Spring Boot应用程序的时候,通过打包为war包的方式将程序部署到Tomcat服务器上是一个常用的方式。但是在这个过程中会遇到一些问题,比如资源文件的路径问题、类加载器的问题等。 二、部署过程中应注意的问题 2.1 静态资源文件路径问…

    Java 2023年5月19日
    00
  • Spring Data JPA实现审计功能过程详解

    下面我将详细讲解“Spring Data JPA实现审计功能过程详解”的完整攻略,具体步骤如下: 第一步:添加依赖 在pom.xml文件中添加以下依赖: <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boo…

    Java 2023年5月20日
    00
  • CAS的service参数验证

    CAS登录成功后会跳转到service参数提供的url,目前系统中这个参数是没有任何验证的,service参数随便赋一个网址就可以。为安全起见现在对这个service要作一下限制,比如只能是同源url才可以重定向。 下面是基于CAS 3.5.2对系统的改造过程。 系统比较老旧,之前也作过CAS方面的改造,基本思路是从login-webflow.xml中找到切…

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