关于java:一个案例演示-Spring-Security-中粒度超细的权限控制

33次阅读

共计 10260 个字符,预计需要花费 26 分钟才能阅读完成。

想要细化权限管制粒度,方法很多。本文接着上文(Spring Security 中如何细化权限粒度?),通过一个具体的案例来向小伙伴们展现基于 Acl 的权限管制。其余的权限管制模型前面也会一一介绍。

1. 筹备工作

首先创立一个 Spring Boot 我的项目,因为咱们这里波及到数据库操作,所以除了 Spring Security 依赖之外,还须要退出数据库驱动以及 MyBatis 依赖。

因为没有 acl 相干的 starter,所以须要咱们手动增加 acl 依赖,另外 acl 还依赖于 ehcache 缓存,所以还须要加上缓存依赖。

最终的 pom.xml 文件如下:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-acl</artifactId>
    <version>5.3.4.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.1.3</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
    <groupId>net.sf.ehcache</groupId>
    <artifactId>ehcache</artifactId>
    <version>2.10.4</version>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.1.23</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context-support</artifactId>
</dependency>

我的项目创立胜利之后,咱们在 acl 的 jar 包中能够找到数据库脚本文件:

依据本人的数据库抉择适合的脚本执行,执行后一共创立了四张表,如下:

表的含意我就不做过多解释了,不分明的小伙伴能够参考上篇文章:Spring Security 中如何细化权限粒度?

最初,再在我的项目的 application.properties 文件中配置数据库信息,如下:

spring.datasource.url=jdbc:mysql:///acls?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=123
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

至此,筹备工作就算实现了。接下来咱们来看配置。

2.ACL 配置

这块配置代码量比拟大,我先把代码摆上来,咱们再一一剖析:

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class AclConfig {

    @Autowired
    DataSource dataSource;

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

    @Bean
    public PermissionGrantingStrategy permissionGrantingStrategy() {return new DefaultPermissionGrantingStrategy(new ConsoleAuditLogger());
    }

    @Bean
    public AclCache 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 LookupStrategy lookupStrategy() {return new BasicLookupStrategy(dataSource, aclCache(), aclAuthorizationStrategy(), new ConsoleAuditLogger()
        );
    }

    @Bean
    public AclService aclService() {return new JdbcMutableAclService(dataSource, lookupStrategy(), aclCache());
    }

    @Bean
    PermissionEvaluator permissionEvaluator() {AclPermissionEvaluator permissionEvaluator = new AclPermissionEvaluator(aclService());
        return permissionEvaluator;
    }
}
  1. @EnableGlobalMethodSecurity 注解的配置示意开启我的项目中 @PreAuthorize、@PostAuthorize 以及 @Secured 注解的应用,一会咱们要通过这些注解配置权限。
  2. 因为引入了数据库的一整套货色,并且配置了数据库连贯信息,所以这里能够注入 DataSource 实例以备后续应用。
  3. AclAuthorizationStrategy 实例用来判断以后的认证主体是否有批改 Acl 的权限,精确来说是三种权限:批改 Acl 的 owner;批改 Acl 的审计信息以及批改 ACE 自身。这个接口只有一个实现类就是 AclAuthorizationStrategyImpl,咱们在创立实例时,能够传入三个参数,别离对应了这三种权限,也能够传入一个参数,示意这一个角色能够干三件事。
  4. PermissionGrantingStrategy 接口提供了一个 isGranted 办法,这个办法就是最终真正进行权限比对的办法,该接口只有一个实现类 DefaultPermissionGrantingStrategy,间接 new 就行了。
  5. 在 ACL 体系中,因为权限比对总是要查询数据库,造成了性能问题,因而引入了 Ehcache 做缓存。AclCache 共有两个实现类:SpringCacheBasedAclCache 和 EhCacheBasedAclCache。咱们后面曾经引入了 ehcache 实例,所以这里配置 EhCacheBasedAclCache 实例即可。
  6. LookupStrategy 能够通过 ObjectIdentity 解析出对应的 Acl。LookupStrategy 只有一个实现类就是 BasicLookupStrategy,间接 new 即可。
  7. AclService 这个咱们在上文曾经介绍过了,这里不再赘述。
  8. PermissionEvaluator 是为表达式 hasPermission 提供反对的。因为本案例前面应用相似于 @PreAuthorize("hasPermission(#noticeMessage,'WRITE')") 这样的注解进行权限管制,因而之类须要配置一个 PermissionEvaluator 实例。

至此,这里的配置类就和大家介绍完了。

3. 情节设定

假如我当初有一个告诉音讯类 NoticeMessage,如下:

public class NoticeMessage {
    private Integer id;
    private String content;

    @Override
    public String toString() {
        return "NoticeMessage{" +
                "id=" + id +
                ", content='" + content + '\'' +
                '}';
    }

    public Integer getId() {return id;}

    public void setId(Integer id) {this.id = id;}

    public String getContent() {return content;}

    public void setContent(String content) {this.content = content;}
}

而后依据该类创立了数据表:

CREATE TABLE `system_message` (`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `content` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

那么接下来的权限管制就是针对这个 NoticeMessage 的。

创立 NoticeMessageMapper,并增加几个测试方法:

@Mapper
public interface NoticeMessageMapper {List<NoticeMessage> findAll();

    NoticeMessage findById(Integer id);

    void save(NoticeMessage noticeMessage);

    void update(NoticeMessage noticeMessage);
}

NoticeMessageMapper.xml 内容如下:

<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.javaboy.acls.mapper.NoticeMessageMapper">


    <select id="findAll" resultType="org.javaboy.acls.model.NoticeMessage">
        select * from system_message;
    </select>

    <select id="findById" resultType="org.javaboy.acls.model.NoticeMessage">
        select * from system_message where id=#{id};
    </select>

    <insert id="save" parameterType="org.javaboy.acls.model.NoticeMessage">
        insert into system_message (id,content) values (#{id},#{content});
    </insert>

    <update id="update" parameterType="org.javaboy.acls.model.NoticeMessage">
        update system_message set content = #{content} where id=#{id};
    </update>
</mapper>

这些应该都好了解,没啥好说的。

接下来创立 NoticeMessageService,如下:

@Service
public class NoticeMessageService {
    @Autowired
    NoticeMessageMapper noticeMessageMapper;

    @PostFilter("hasPermission(filterObject,'READ')")
    public List<NoticeMessage> findAll() {List<NoticeMessage> all = noticeMessageMapper.findAll();
        return all;
    }

    @PostAuthorize("hasPermission(returnObject,'READ')")
    public NoticeMessage findById(Integer id) {return noticeMessageMapper.findById(id);
    }

    @PreAuthorize("hasPermission(#noticeMessage,'CREATE')")
    public NoticeMessage save(NoticeMessage noticeMessage) {noticeMessageMapper.save(noticeMessage);
        return noticeMessage;
    }
    
    @PreAuthorize("hasPermission(#noticeMessage,'WRITE')")
    public void update(NoticeMessage noticeMessage) {noticeMessageMapper.update(noticeMessage);
    }

}

波及到了两个新注解,略微说下:

  • @PostFilter:在执行办法后过滤返回的汇合或数组(筛选出以后用户具备 READ 权限的数据),returnObject 就示意办法的返回值。有一个和它对应的注解 @PreFilter,这个注解容许办法调用,但必须在进入办法之前对参数进行过滤。
  • @PostAuthorize:容许办法调用,然而如果表达式计算结果为 false,将抛出一个安全性异样,#noticeMessage 对应了办法的参数。
  • @PreAuthorize:在办法调用之前,基于表达式的计算结果来限度对办法的拜访。

明确了注解的含意,那么下面的办法应该就不必多做解释了吧。

配置实现,接下来咱们进行测试。

4. 测试

为了不便测试,咱们首先筹备几条测试数据,如下:

INSERT INTO `acl_class` (`id`, `class`)
VALUES
    (1,'org.javaboy.acls.model.NoticeMessage');
INSERT INTO `acl_sid` (`id`, `principal`, `sid`)
VALUES
    (2,1,'hr'),
    (1,1,'manager'),
    (3,0,'ROLE_EDITOR');
INSERT INTO `system_message` (`id`, `content`)
VALUES
    (1,'111'),
    (2,'222'),
    (3,'333');

首先增加了 acl_class,而后增加了三个 Sid,两个是用户,一个是角色,最初增加了三个 NoticeMessage 实例。

目前没有任何用户 / 角色可能拜访到 system_message 中的三条数据。例如执行如下代码获取不到任何数据:

@Test
@WithMockUser(roles = "EDITOR")
public void test01() {List<NoticeMessage> all = noticeMessageService.findAll();
    System.out.println("all =" + all);
}

@WithMockUser(roles = “EDITOR”) 示意应用 EDITOR 角色拜访。松哥这里是为了不便。小伙伴们也能够本人给 Spring Security 配置用户,设置相干接口,而后 Controller 中增加接口进行测试,我这里就不那么麻烦了。

当初咱们对其进行配置。

首先我想设置让 hr 这个用户能够读取 system_message 表中 id 为 1 的记录,形式如下:

@Autowired
NoticeMessageService noticeMessageService;
@Autowired
JdbcMutableAclService jdbcMutableAclService;
@Test
@WithMockUser(username = "javaboy")
@Transactional
@Rollback(value = false)
public void test02() {ObjectIdentity objectIdentity = new ObjectIdentityImpl(NoticeMessage.class, 1);
    Permission p = BasePermission.READ;
    MutableAcl acl = jdbcMutableAclService.createAcl(objectIdentity);
    acl.insertAce(acl.getEntries().size(), p, new PrincipalSid("hr"), true);
    jdbcMutableAclService.updateAcl(acl);
}

咱们设置了 mock user 是 javaboy,也就是这个 acl 创立好之后,它的 owner 是 javaboy,然而咱们后面预设数据中 Sid 没有 javaboy,所以会主动向 acl_sid 表中增加一条记录,值为 javaboy。

在这个过程中,会别离向 acl_entry、acl_object_identity 以及 acl_sid 三张表中增加记录,因而须要增加事务,同时因为咱们是在单元测试中执行,为了确保可能看到数据库中数据的变动,所以须要增加 @Rollback(value = false) 注解让事务不要主动回滚。

在办法外部,首先别离创立 ObjectIdentity 和 Permission 对象,而后创立一个 acl 对象进去,这个过程中会将 javaboy 增加到 acl_sid 表中。

接下来调用 acl_insertAce 办法,将 ace 存入 acl 中,最初调用 updateAcl 办法去更新 acl 对象即可。

配置实现后,执行该办法,执行实现后,数据库中就会有相应的记录了。

接下来,应用 hr 这个用户就能够读取到 id 为 1 的记录了。如下:

@Test
@WithMockUser(username = "hr")
public void test03() {List<NoticeMessage> all = noticeMessageService.findAll();
    assertNotNull(all);
    assertEquals(1, all.size());
    assertEquals(1, all.get(0).getId());
    NoticeMessage byId = noticeMessageService.findById(1);
    assertNotNull(byId);
    assertEquals(1, byId.getId());
}

松哥这里用了两个办法来和大家演示。首先咱们调用了 findAll,这个办法会查问出所有的数据,而后返回后果会被主动过滤,只剩下 hr 用户具备读取权限的数据,即 id 为 1 的数据;另一个调用的就是 findById 办法,传入参数为 1,这个好了解。

如果此时想利用 hr 这个用户批改对象,则是不能够的。咱们能够持续应用下面的代码,让 hr 这个用户能够批改 id 为 1 的记录,如下:

@Test
@WithMockUser(username = "javaboy")
@Transactional
@Rollback(value = false)
public void test02() {ObjectIdentity objectIdentity = new ObjectIdentityImpl(NoticeMessage.class, 1);
    Permission p = BasePermission.WRITE;
    MutableAcl acl = (MutableAcl) jdbcMutableAclService.readAclById(objectIdentity);
    acl.insertAce(acl.getEntries().size(), p, new PrincipalSid("hr"), true);
    jdbcMutableAclService.updateAcl(acl);
}

留神这里权限改为 WRITE 权限。因为 acl 中曾经存在这个 ObjectIdentity 了,所以这里通过 readAclById 办法间接读取已有的 acl 即可。办法执行结束后,咱们再进行 hr 用户写权限的测试:

@Test
@WithMockUser(username = "hr")
public void test04() {NoticeMessage msg = noticeMessageService.findById(1);
    assertNotNull(msg);
    assertEquals(1, msg.getId());
    msg.setContent("javaboy-1111");
    noticeMessageService.update(msg);
    msg = noticeMessageService.findById(1);
    assertNotNull(msg);
    assertEquals("javaboy-1111", msg.getContent());
}

此时,hr 就能够应用 WRITE 权限去批改对象了。

假如我当初想让 manager 这个用户去创立一个 id 为 99 的 NoticeMessage,默认状况下,manager 是没有这个权限的。咱们当初能够给他赋权:

@Test
@WithMockUser(username = "javaboy")
@Transactional
@Rollback(value = false)
public void test02() {ObjectIdentity objectIdentity = new ObjectIdentityImpl(NoticeMessage.class, 99);
    Permission p = BasePermission.CREATE;
    MutableAcl acl = jdbcMutableAclService.createAcl(objectIdentity);
    acl.insertAce(acl.getEntries().size(), p, new PrincipalSid("manager"), true);
    jdbcMutableAclService.updateAcl(acl);
}

留神,这里的权限是 CREATE。

接下来应用 manager 用户就能够增加数据了:

@Test
@WithMockUser(username = "manager")
public void test05() {NoticeMessage noticeMessage = new NoticeMessage();
    noticeMessage.setId(99);
    noticeMessage.setContent("999");
    noticeMessageService.save(noticeMessage);
}

此时就能够增加胜利了。增加胜利后,manager 这个用户没有读 id 为 99 的数据的权限,能够参考后面案例自行添加。

5. 小结

从下面的案例中大家能够看到,ACL 权限模型中的权限管制真的是十分十分细,细到每一个对象的 CURD。

长处就不用说了,够细!同时将业务和权限胜利拆散。毛病也很显著,权限数据量宏大,扩展性弱。

最初,公号后盾回复 acl 获取本文案例下载链接。

正文完
 0