1. 简介

本文将介绍如何使用 Apache Shiro 这个 Java 安全框架,实现细粒度的基于权限的访问控制(Permissions-Based Access Control)

相比简单的角色判断,权限模型能让你的系统更灵活、更易维护。尤其是在业务频繁变更的项目中,避免“改个权限就得改代码、重新打包上线”这种踩坑操作。

2. 环境准备

我们沿用 Shiro 的基础配置,仅引入 shiro-core 模块即可:

<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-core</artifactId>
    <version>1.4.1</version>
</dependency>

为了简化测试,我们使用一个 shiro.ini 文件作为 Realm 数据源,放在类路径根目录下:

[users]
jane.admin = password, admin
john.editor = password2, editor
zoe.author = password3, author
 
[roles]
admin = *
editor = articles:*
author = articles:create, articles:edit

接着初始化 Shiro:

IniRealm iniRealm = new IniRealm("classpath:shiro.ini");
SecurityManager securityManager = new DefaultSecurityManager(iniRealm);
SecurityUtils.setSecurityManager(securityManager);

这样就完成了最简安全环境搭建 ✅

3. 角色与权限

权限控制的核心是两个概念:用户(Subject)角色(Role)权限(Permission)

角色是用户的一个分类标签,比如 admineditor,代表一类用户的访问能力。传统做法是通过角色来判断访问权限:

Subject subject = SecurityUtils.getSubject();
if (subject.hasRole("admin")) {       
    logger.info("Welcome Admin");              
}

⚠️ 但这种方式有个致命问题:把权限逻辑硬编码在代码里了

比如,某天产品经理说:“所有编辑角色也能删除文章”。你得改代码里的 hasRole("admin") 判断,然后重新构建部署 —— 这显然不现实。

3.1. 权限(Permission)才是王道

Shiro 的设计哲学是:权限描述“能做什么”,而不是“谁可以做”

权限是系统操作的抽象,比如:

  • articles:create:创建文章
  • profile:edit:编辑个人资料
  • document:approve:审批文档

使用权限判断更灵活:

Subject subject = SecurityUtils.getSubject();
if (subject.isPermitted("articles:create")) {
    // 创建新文章
}

✅ 优势很明显:权限变更只需改配置,无需动代码。

❌ 注意:权限在 Shiro 中是可选项,但一旦用上,就别回头了。

3.2. 权限如何分配给用户

Shiro 的权限模型很灵活,支持:

  • 权限 → 角色 → 用户
  • 权限 → 用户(直接分配)

但大多数 Realm(包括我们用的 IniRealm)只支持“权限绑定到角色”。

结构如下:

User (zoe.author)
  └─ Role: author
       └─ Permissions: articles:create, articles:edit

对应 shiro.ini 配置:

[users]
zoe.author = password3, author

[roles]
author = articles:create, articles:edit

其他 Realm(如 JdbcRealm)也可以配置权限与角色的映射,原理一致。

4. 通配符权限(Wildcard Permissions)

Shiro 默认的权限实现是 Wildcard Permissions,它用字符串表达权限,并支持通配和层级结构,非常实用。

基本结构

权限字符串由多个部分组成,用冒号 : 分隔:

资源:操作:实例ID

例如:

articles:edit:123

可理解为:

  1. articles:资源类型
  2. edit:操作类型
  3. 123:具体资源 ID

当然,组件数量不限于三个,但三段式最常见。

使用示例:

Subject subject = SecurityUtils.getSubject();
if (subject.isPermitted("articles:edit:123")) {
    // 编辑 ID 为 123 的文章
}

4.1. 权限推断与实例级控制

这是 Wildcard Permissions 的精髓:权限判断是“推断(implies)”而非“相等(equals)”

  • ❌ 角色判断:hasRole("editor") → 必须精确匹配
  • ✅ 权限判断:isPermitted("articles:edit:1") → 只要用户权限能“推断出”该权限即可

举个例子,如果 author 角色拥有:

[roles]
author = articles:*

那么以下所有判断都为 true

subject.isPermitted("articles:create")     → ✅
subject.isPermitted("articles:edit")       → ✅
subject.isPermitted("articles:delete")     → ✅

因为 articles:* 中的 * 是通配符,匹配任意第二级操作。

更进一步,还可以支持多级通配:

articles:*:*     → 匹配所有文章的所有操作
*:edit:*         → 匹配所有资源的编辑操作

性能建议

由于权限推断是模式匹配,比字符串相等更耗性能,所以:

✅ 推荐:使用最具体的权限判断
❌ 避免:过度依赖通配符做判断

// 推荐:具体判断
if (subject.isPermitted("articles:edit:1")) { ... }

// 不推荐:太宽泛,影响性能
if (subject.isPermitted("articles:*")) { ... }

5. 自定义权限实现

虽然 Wildcard Permissions 能覆盖大多数场景,但某些特殊业务可能需要自定义权限模型。

比如:路径权限,即 /articles 的权限应自动包含 /articles/drafts 下的所有子路径。

5.1. 实现 Permission 接口

只需实现 Permission 接口的 implies 方法:

public class PathPermission implements Permission {

    private final Path path;

    public PathPermission(Path path) {
        this.path = path;
    }

    @Override
    public boolean implies(Permission p) {
        if (p instanceof PathPermission) {
            return ((PathPermission) p).path.startsWith(path);
        }
        return false;
    }
}

逻辑很简单:当前路径是否为被判断路径的前缀(即父路径)。

5.2. 注册自定义 PermissionResolver

Shiro 需要知道如何把字符串转成 PathPermission 对象,这由 PermissionResolver 负责:

public class PathPermissionResolver implements PermissionResolver {
    @Override
    public Permission resolvePermission(String permissionString) {
        return new PathPermission(Paths.get(permissionString));
    }
}

然后注入到 Realm 中:

IniRealm realm = new IniRealm();
Ini ini = Ini.fromResourcePath(Main.class.getResource("/shiro.ini").getPath());
realm.setIni(ini);
realm.setPermissionResolver(new PathPermissionResolver());
realm.init();

SecurityManager securityManager = new DefaultSecurityManager(realm);
SecurityUtils.setSecurityManager(securityManager);

更新 shiro.ini 配置为路径风格:

[roles]
admin = /
editor = /articles
author = /articles/drafts

使用方式:

Subject currentUser = SecurityUtils.getSubject();
if (currentUser.isPermitted("/articles/drafts/new-article")) {
    log.info("You can access articles");
}

结果:

  • admin → 可访问所有路径
  • editor → 可访问 /articles 及其子路径
  • author → 仅可访问 /articles/drafts 下内容

生产环境配置示例

实际项目中,通常用 shiro.ini 或 Spring 配置。例如:

[main]
permissionResolver = com.example.shiro.PathPermissionResolver
dataSource = org.apache.shiro.jndi.JndiObjectFactory
dataSource.resourceName = java://app/jdbc/myDataSource

jdbcRealm = org.apache.shiro.realm.jdbc.JdbcRealm
jdbcRealm.dataSource = $dataSource 
jdbcRealm.permissionResolver = $permissionResolver

这样就能在 JDBC Realm 中使用自定义权限模型了。

6. 总结

本文系统介绍了 Apache Shiro 的权限控制机制,重点包括:

  • ✅ 使用 isPermitted() 替代 hasRole(),避免硬编码
  • ✅ Wildcard Permissions 支持层级和通配,适合大多数场景
  • ✅ 权限判断是“推断”而非“相等”,更灵活
  • ✅ 可通过自定义 PermissionPermissionResolver 实现复杂模型(如路径权限)

权限模型是安全系统的基石,用好 Shiro 的权限机制,能让你的系统在面对复杂授权需求时依然游刃有余。

示例代码已上传至 GitHub:https://github.com/baeldung/tutorials/tree/master/security-modules/apache-shiro


原始标题:Permissions-Based Access Control with Apache Shiro | Baeldung