ACL
ACL 权限模型介绍
ACL(Access ControlList,访问控制列表)是一种比较古老的权限控制模型。它是一种面资源的访问控制模型,在 ACL 中我们所做的所有权限配置都是针对资源的。
在 ACL 权限模型中,会对系统中的每一个资源配置一个访问控制列表,这个列表记录了用户/角色对于资源的操作权限,当需要访问这些资源时,会首先检查访问控制列表中是否存在当前用角色的访问限,如果存在,则允许相应的操作,否则就拒绝相应的操作。
ACL 的一个核心思路就是:将某个对象的某种权限授予某个用户或某种角色,它们之间的关系是多对多,即一个用户/角色可以具备某个对象的多种权限,某个对象的权限也可以被多个用户/角色所持有。
举人简单例子:现在有一个 User 对象,针对该对象有查询、修改、删除等权限,可以将这些权限赋值给某一个用户 javaboy,也可以将这些权限赋值给某一个角色 ADMIN,那么 javaboy 用户或者具备 ADMIN 角色的其他用户就具有执行相应操作的权限。从这个角度看,ACL 是一种粒度非常细的权限控制,它可以精确到某一个资源的某一个权限。这些权限数据都记录在数据库中,这带来的另外一个问题就是需要维护的权限数据量非常庞大,特别是对于一些大型系统而言,这一问题尤其突出,大量的数据需要维护可能会造成系统性能下降。不过对于一些简单的小型系统而言,使用 ACL 还是可以的,没有任何问题。
ACL的使用非常简单,在搞明白实现原理的基础上,开发者可以不使用任何权限框架也能快速实现 ACL 权限模型。当然 Spring Security 也为 ACL 提供了相应的依赖 Spring-security-acl,如果项目中用到了 ACL,这个依赖需要自己额外添加。
ACL 核心概念介绍
接下来介绍一些 ACL 中的核心概念。
Sid
Sid 代表了用户和角色(注意是和,不是或),它有两种:
-
GrantedAuthoritySid
-
PrincipalSid
前者代表角色,后者代表用户。根据本书前面章节的介绍,大家已经知道,在 Spring Security 中,用户和角色信息都是保存在 Authentication 对象中的,即 ACL 中的 Sid 我们可以从 Authentication 对象中提取出来的,具体的提取方法是 SidRetrievalStrategyImpl#getSids,我们来看一下相关源码:
public List<Sid> getSids(Authentication authentication) {
Collection<? extends GrantedAuthority> authorities = roleHierarchy
.getReachableGrantedAuthorities(authentication.getAuthorities());
List<Sid> sids = new ArrayList<>(authorities.size() + 1);
sids.add(new PrincipalSid(authentication));
for (GrantedAuthority authority : authorities) {
sids.add(new GrantedAuthoritySid(authority));
}
return sids;
}
可以看到,首先从 authentication 对象中提取出当前用户的角色,然后构造代表当前用户的 PrincipalSid 存入 sids 集合中,接下来构造代表用户角色的 GrantedAuthoritySid 存入 sids 集合中,最后将 sids 集合返回。
在 ACL 中,查看一个用户的身份,就是查看他的 Sid。
ObjectIdentity
ObjectIdentity 是一个域对象,这是官方的说法,有点拗口。实际上这就是我们要操作的对象。
例如有一个 User 对象,如果直接去记录能够对 User 对象执行哪些操作,这就会导致高耦合。所以我们需要对其解耦,将所有需要操作的对象通过 ObjectIdentity 抽象出来,用 ObjectIdentity 代表所有需要控制的对象,这样就能实现权限管理系统和具体业务解耦。
ObjectIdentity 中有两个关键方法,getType 和 getIdentifier。一般来说,getType 方法返回资源类的全路径,例如 org.javaboy.acl.model.User;getIdentifier 方法则返回资源的 id。通过这两个方法,就能够锁定一个具体的对象。
Acl(Acl对象)
看名字就知道,这是整个系统的核心调度部分
一个 Acl 对象关联一个 ObjectIdentity 和多个 ACE,同时一个 Acl 对象还拥有一个 Sid,这个 Sid 表示这个 Acl 对象的所有者,Acl 对象的所有者可以修改甚至删除该 Acl 对象。
AccessControlEntry
AccessControlEntry 简写为 ACE,一个 AccessControlEntry 对象代表一条权限记录,每一个 AccessControlEntry 中包含一个 Sid 和一个 Permission 对象,表示某个 Sid 具备某种权限。同时每一个 AccessControlEntry 对象都对应了一个 Acl 对象,一个 Acl 对象则可以对应多个 AccessControlEntry。
由于 Acl 对象还关联了一个 ObjectIdentity 对象,有了这层对应关系,就可以知道某个 Sid 具备某个 ObjectIdentity 的某种 Permission,如图 14-1 所示。

Permission
Permission 就是具体的权限对象,在 Spring Security 的默认实现中,最多支持 32 种权限。
Spring Security 中默认定义了五种权限:
public class BasePermission extends AbstractPermission {
public static final Permission READ = new BasePermission(1 << 0, 'R'); // 1
public static final Permission WRITE = new BasePermission(1 << 1, 'W'); // 2
public static final Permission CREATE = new BasePermission(1 << 2, 'C'); // 4
public static final Permission DELETE = new BasePermission(1 << 3, 'D'); // 8
public static final Permission ADMINISTRATION = new BasePermission(1 << 4, 'A'); // 16
protected BasePermission(int mask) {
super(mask);
}
protected BasePermission(int mask, char code) {
super(mask, code);
}
}
如果默认的五种权限不能满足需求,开发者也可以自定义类继承自 BasePermission,从而扩展权限的定义。
ACL 数据库分析
在 ACL 中,所有和权限相关的数据都保存在数据库中,在 spring-security-acl 依赖中,为数据库提供了相应的脚本,如图 14-2 所示。针对不同的数据库提供了不同的脚本,开发者根据自已的实际情况选择合适的数据库脚本执行即可。

脚本中主要涉及四张表,如图14-3所示

接下来逐一分析这四张表的作用。
acl_class
acl_class 表用来保存资源类的全路径,它有两个字段,其中 id 是自增长的主键,class 字段中保存资源类的全路径名,如图14-4所示。

acl_sid
acl_sid 表用来保存 Sid,principal 字段表示 Sid 的类型,该字段值为 1 表示 Sid 是 PrincipalSid,该字段值为 0 表示 Sid 是 GrantedAuthoritySid,如图 14-5 所示。

acl_object_identity
acl_object_identity 表用来保存需要进行访问控制的对象信息,如图14-6 所示

这个表涉及的字段比较多,含义如下:
-
object_id_class:关联 acl_class.id,即需要进行权限控制的类信息。
-
object_id_identity:需要控制的对象的 id,和 object_id_class 字段共同锁定一个具体对象。
-
parent_object: 父对象 ID,关联一条 acl_object_identity 记录。
-
owner_sid:这个 ACL 记录拥有者的 Sid,拥有者可以对该 ACL 记录执行更新、删除等操作。
-
entries_inheriting:是否需要继承父对象的权限。
简单来说,这个表中的 object_id_class 和 object_id_identity 字段锁定了要进行权限控制的对象,具体如何控制呢?则要看 acl_entry 表中的关联关系了。
acl_entry
acl_entry 表用来保存 Sid 和 Permission 之间的对应关系,如图 14-7 所示。

这个表涉及的字段比较多,含义如下:
-
acl_object_identity: 关联 acl_object_identity.id。
-
ace_order:权限顺序。acl_object_identity 和 ace_order 的组合要唯一。
-
sid:关联 acl_sid.id,该字段关联一个具体的用户/角色。
-
mask:权限掩码。
-
granting:表示当前记录是否生效。
-
audit_success/audit_failure:审计信息。
简单来说,acl_entry 中的一条记录,关联了一个要操作的对象(acl_object_identity 和 ace_order 字段),关联了Sid(sid 字段),也描述了权限(mask),将权限涉及的东西都在该字段中整合起来了。
至此,ACL 中涉及的相关表就介绍完了。
接下来我们通过一个案例,来学习 ACL 的具体用法。
实战
准备工作
首先创建一个 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-acl-5.3.4.RELEASE.jar)中可以找到数据库脚本文件,选择适合自已数据库的脚本文件复制到数据库中执行,执行后生成四张表,这四张表前面己经介绍过了,这单不再资述。
最后,再在项目的 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
至此,准备工作就算完成了。接下来我们来看配置。
Acl配置
创建 AclConfig 类,完成 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;
}
}
这里的配置实例比较多,我们来逐个解释:
-
@EnableGlobalMethodSecurity 注解表示开启项目中 @PreAuthorize、@PostAuthorize 以及 @Secured 注解的使用,我们将通过这些注解配置访问权限。
-
由于引入了数据库的一整套依赖,并且配置了数据库连接信息,所以这里可以直接注入 DataSource 实例以备后续使用。
-
AclAuthorizationStrategy 实例用来判断当前的认证主体是否有修改 ACL 的权限,准确来说是三种权限:修改 ACL 的 owner、修改 ACL 的审计信息、修改 ACE 本身。这个接口只有一个实现类就是 AclAuthorizationStrategyImpl,我们在创建实例时,可以传入三个参数,分别对应了这三种权限,也可以传入一个参数,表示这一个角色可以做三件事情。
-
PermissionGrantingStrategy 接口提供了一个 isGranted 方法,这个方法就是最终真正进行权限比对的方法,该接口只有一个实现类 DefaultPermissionGrantingStrategy,直接新建就行了。
-
在 ACL 中,由于权限比对总是要查询数据库,造成了性能问题,因此引入了 Ehcache 做缓存。AclCache 共有两个实现类:SpringCacheBasedAclCache 和 EhCacheBasedAclCache。我们前面已经引入了 Ehcache 实例,所以这里配置 EhCacheBasedAclCache 实例即可(如果使用 SpringCacheBasedAclCache,则不需要引入 Ehcache 依赖)。
-
LookupStrategy 可以通过 ObjectIdentity 解析出对应的 ACL。LookupStrategy 只有一个实现类就是 BasicLookupStrategy,直接新建即可。
-
AclService 已经在上文介绍过了,这里不再赘述。
-
PermissionEvaluator 是为表达式 hasPermission 提供支持的。由于本案例后面使用 @PreAuthorize("hasPermission(#noticeMessage,'WRITE')") 这样的注解进行权限控制,因此这里需要配置一个 PermissionEvaluator 实例,当进行权限校验时,就会调用到 AclPermissionEvaluator#hasPermission 方法。
以上就是 Acl 的配置类。
业务配置
假设我们现在有一个通知消息类 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;
}
}
然后根据该类创建一张数据表 system message,SQL 脚本如下:
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.acl.mapper.NoticeMessageMapper">
<select id="findAll" resultType="org.javaboy.acl.model.NoticeMessage">
select * from system_message;
</select>
<select id="findById" resultType="org.javaboy.acl.model.NoticeMessage">
select * from system_message where id=#{id};
</select>
<insert id="save" parameterType="org.javaboy.acl.model.NoticeMessage">
insert into system_message (id,content) values (#{id},#{content});
</insert>
<update id="update" parameterType="org.javaboy.acl.model.NoticeMessage">
update system_message set content = #{content} where id=#{id};
</update>
</mapper>
这是 MyBatis 的基本操作,这里不再赘述。
接下来创建 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);
}
}
通过在 Service 类上添加注解来实现权限控制,几个权限注解我们在本书第 13 章中都有介绍,不同的是这里注解中的表达式变成了 hasPermission:
-
@PostFilter("hasPermission(filterObject,READ)"):在方法执行完成后,过滤返回的集合或数组,筛选出当前用户/角色具有 READ 权限的数据。filterObject 表示方法的返回的集合/数组中的元素。
-
@PostAuthorize("hasPermission(returnObject,READ)"):在方法执行完成后,进行权限校验,如果表达式计算结果为 false,即当前用户/角色不具备返回对象的 READ 权限,将抛出异常。
-
@PreAuthorize("hasPermission(#noticeMessage,'CREATE)"):在方法调用之前,进行权限校验,判断当前用户/角色是否具备 noticeMessage 对象的 CREATE 权限。#noticeMessage 表示对方法参数 noticeMessage 的引用。
-
@PreAuthorize("hasPermission(#noticeMessage,'WRITE)"):在方法调用之前,进行权限校验,判断当前用户/角色是否具备 noticeMessage 对象的 WRITE 权限。
ACL 作为一种权限模型,底层实现原理还是本书第 13 章中所讲的权限实现原理,ACL 只是对这些原理的一个具体的应用,NoticeMessageService 中几个权限注解的原理这里就不再赘述了,hasPermission 表达式的具体实现则在 AclPermissionEvaluator#hasPermission 方法中。
配置完成,接下来我们进行测试。
测试
为了方使测试,我们首先准备几条测试数据,SQL 脚本如下:
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 中的三条数据。例如,执行如下代码获取到的集合为空:
@Autowired
NoticeMessageService noticeMessageService;
@Test
@WithMockUser(username = "manager")
public void test01() {
List<NoticeMessage> all = noticeMessageService.findAll();
assertEquals(0,all.size());
}
@WithMockUser(username="manager") 表示以 manager 用户身份进行访问,这是为了方便使用单元测试。读者可以自己给 Spring Security 配置用户,然后在 Controller 中添加测试接口进行测试。 |
现在我们对其进行权限配置。
首先我们想设置让 hr 这个用户可以读取 system_message 表中 id 为 1 的记录,方式如下:
@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,但是我们前面 acl_sid 表的预设数据中没有 javaboy,所以上面这段代码执行后,会自动向 acl_sid 表中添加一条记录,值为 javaboy。
在方法执行过程中,会分别向 acl_entry、acl_object_identity 以及 acl_sid 三张表中添加记录,因此需要添加事务。因为我们是在单元测试中执行,事务会自动回滚,为了确保能够看到数据库中数据的变化,所以需要添加 @Rollback(value=false) 注解,让事务不要自动回滚。
在方法内部,首先分别创建 ObjectIdentity 和 Permission 对象,然后创建一个 Acl 对象,接下来调用 acl.insertAce 方法,将 ACE 保存到一个集合中,最后调用 updateAcl 方法去更新 Acl 对象。acl_object_identity 和 acl_entry 两张表的更新操作也将在 updateAcl 这个方法中完成。
该方法执行完成后,数据库中就会有相应的记录了。
接下来,使用 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 test04() {
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);
}
和前面的 test02 方法相比,这里主要有两个变化:
-
定义的 Permission 对象是 WRITE。
-
调用 readAclById 方法来获取一个 Acl 对象。在 test02 方法中,由于当时创建的 objectIdentity 对象没有关联任何 Acl 对象,所以我们需要调用 createAcl 方法去创建一个全新的 Acl 对象,而经过前面的调用,现在我们创建的 objectIdentity 对象已经关联了相应的 Acl 对象了,所以这里调用 readAclById 方法去获取已经存在的 Acl 对象即可。
方法执行完毕后,我们再进行 hr 用户写权限的测试:
@Test
@WithMockUser(username = "hr")
public void test05() {
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());
}
假设现在想让 manager 这个用户去创建一个 id 为 99 的 NoticeMessage,默认情况下,manager 是没有这个权限的。我们现在可以为其赋权:
@Autowired
JdbcMutableAclService jdbcMutableAclService;
@Test
@WithMockUser(username = "javaboy")
@Transactional
@Rollback(value = false)
public void test06() {
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);
}
和 testo2 相比,这里构造的 objectIdentity 对象 id 是 99,权限是 CREATE。该方法执行完毕后,接下来就可以使用 manager 用户来添加一条 id 为 99 的数据了:
@Autowired
NoticeMessageService noticeMessageService;
@Test
@WithMockUser(username = "manager")
public void test07() {
NoticeMessage noticeMessage = new NoticeMessage();
noticeMessage.setId(99);
noticeMessage.setContent("999");
noticeMessageService.save(noticeMessage);
}
执行该方法,就可以将 id 为 99 的数据添加到数据库中。添加成功后,manager 这个用户没有 id 为 99 的数据读取权限,可以参考前面案例自行添加。