🚀 核心提示:授权是安全框架的灵魂。本文将带你深入Shiro强大的授权体系,从基于角色的访问控制(RBAC)到粒度更细、更灵活的基于权限的访问控制(PBAC)。我们将通过实例代码、数据库设计和最佳实践,助你构建坚不可摧且易于维护的权限系统。
一、授权的核心概念:角色 vs 权限
在讨论Shiro授权之前,我们必须清晰地辨别两个核心概念:角色(Role) 和 权限(Permission)。
- 角色(Role):代表一组权限的集合。它通常与组织中的职位或职责相对应,例如"管理员"、“编辑”、“普通用户”。授权是面向角色的。
- 权限(Permission):代表一个具体的操作许可。它描述了"能对什么资源做什么操作",例如"对
user
资源有create
操作"、“对ID为123
的article
有delete
操作”。授权是面向资源的。
对比分析:
特性 | 基于角色的访问控制 (RBAC) | 基于权限的访问控制 (PBAC) |
---|---|---|
优点 | 简单直观,易于理解和管理。 | 极其灵活,控制粒度非常细。 |
缺点 | 灵活性差,当角色权限变更时,需要修改代码。 | 设计相对复杂,需要精心规划权限字符串。 |
适用场景 | 权限需求简单、固定的系统。 | 权限需求复杂、多变的企业级应用。 |
最佳实践:始终基于权限进行授权,然后将权限赋予角色,最后将角色分配给用户。 (用户 -> 角色 -> 权限) 这种模式结合了RBAC的简洁性和PBAC的灵活性。
二、Shiro的王牌:Wildcard Permissions
Shiro的权限字符串设计是其一大亮点,特别是通配符权限(Wildcard Permissions)。它提供了一种强大、直观且灵活的方式来定义权限。
2.1 语法规则
Wildcard Permission的字符串遵循一个简单的规则:资源:操作:实例
(resource:action:instance
)。
- 多级资源:可以用冒号分隔,表示层级关系。例如
system:user
。 - 通配符:
*
代表"所有"。 - 多值分隔符:
,
代表"或"的关系。
2.2 示例解析
权限字符串 | 含义 |
---|---|
user:create | 允许对user 资源执行create 操作。 |
user:view,update | 允许对user 资源执行view 或update 操作。 |
user:* | 允许对user 资源执行所有操作。 |
*:view | 允许对所有资源执行view 操作。 |
user:view:1 | 允许对ID为1 的user 资源执行view 操作。 |
user:*:1 | 允许对ID为1 的user 资源执行所有操作。 |
user:view:* | 允许对所有user 实例执行view 操作。 |
这种设计使得权限表达能力极强,例如,我们可以轻松定义"某个主管只能管理自己部门员工的信息"这类复杂的权限。
三、实现授权:改造AuthorizingRealm
授权的核心逻辑在AuthorizingRealm
的doGetAuthorizationInfo
方法中实现。当Shiro需要进行权限检查,并且缓存中没有找到授权信息时,会调用此方法。
3.1 doGetAuthorizationInfo
方法
这个方法的职责是:根据当前登录用户的身份(PrincipalCollection
),从数据库或其他数据源中查找该用户拥有的所有角色和权限,然后封装成AuthorizationInfo
对象返回。
public class MyRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
@Autowired
private RoleService roleService;
@Autowired
private PermissionService permissionService;
// ... 认证方法 doGetAuthenticationInfo ...
/**
* 授权
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
System.out.println("执行授权逻辑...");
// 1. 获取当前登录用户
User user = (User) principals.getPrimaryPrincipal();
// 2. 从数据库查询该用户的角色
Set<String> roles = roleService.findRolesByUserId(user.getId());
// 3. 从数据库查询该用户的所有权限(通过角色间接获得)
Set<String> permissions = permissionService.findPermissionsByUserId(user.getId());
// 4. 创建AuthorizationInfo对象
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
// 5. 设置角色和权限
info.setRoles(roles);
info.setStringPermissions(permissions);
return info;
}
}
3.2 启用授权缓存
授权信息的查询可能涉及多次数据库操作,性能开销较大。因此,强烈建议开启授权缓存。
@Bean
public Realm realm() {
MyRealm realm = new MyRealm();
// ... 其他配置
// 开启缓存
realm.setCachingEnabled(true);
realm.setAuthorizationCachingEnabled(true); // 开启授权缓存
realm.setAuthorizationCacheName("authorizationCache");
return realm;
}
当用户的权限发生变更时(例如,被赋予新角色),需要清空该用户的授权缓存,以确保Shiro下次能重新加载最新的权限信息。
public void updateUserRoles(Long userId, Set<Long> roleIds) {
// ... 更新数据库 ...
// 清空缓存
Subject subject = SecurityUtils.getSubject();
PrincipalCollection principals = subject.getPrincipals();
// 获取Realm
MyRealm realm = ... // 获取到你的Realm实例
realm.getAuthorizationCache().remove(principals);
}
四、使用授权:注解与编程方式
Shiro提供了两种方式来进行权限检查:注解驱动和编程式。
4.1 注解驱动(推荐)
使用注解是最优雅、最常用的方式。它能以声明式的方式保护方法,使代码更具可读性。
@RequiresAuthentication
: 要求用户必须已认证(isAuthenticated()
为true
)。@RequiresUser
: 要求用户必须已被识别(isAuthenticated()
或isRemembered()
为true
)。@RequiresGuest
: 要求用户必须是游客(未被识别)。@RequiresRoles(value={"admin", "editor"}, logical=Logical.AND)
: 要求用户必须同时拥有admin
和editor
角色。logical
可以设置为AND
或OR
。@RequiresPermissions("user:create")
: 要求用户必须拥有user:create
权限。
要使注解生效,必须配置Shiro与Spring的AOP集成:
@Configuration
public class ShiroConfig {
// ... 其他配置
// 开启Shiro的AOP注解支持
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager);
return advisor;
}
// AOP需要这个Bean
@Bean
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator proxyCreator = new DefaultAdvisorAutoProxyCreator();
proxyCreator.setProxyTargetClass(true); // 解决@RequiresPermissions注解不生效问题
return proxyCreator;
}
}
4.2 编程式
在某些场景下(例如在视图层根据权限显示或隐藏按钮),需要使用编程式授权。
Subject subject = SecurityUtils.getSubject();
// 检查角色
if (subject.hasRole("admin")) {
// 有admin角色
} else {
// 没有admin角色
}
// 检查权限
if (subject.isPermitted("user:create")) {
// 有创建用户的权限
} else {
// 没有
}
// 检查多个权限
if (subject.isPermittedAll("user:create", "user:delete")) {
// 同时拥有创建和删除用户的权限
}
五、数据库设计最佳实践
一个好的数据库设计是权限系统的基石。以下是经典的用户-角色-权限模型设计。
1. 用户表 (t_user
)
字段 | 类型 | 描述 |
---|---|---|
id | bigint | 用户ID (PK) |
username | varchar | 用户名 |
password | varchar | 密码 |
… | … | … |
2. 角色表 (t_role
)
字段 | 类型 | 描述 |
---|---|---|
id | bigint | 角色ID (PK) |
role_name | varchar | 角色名 (e.g., admin) |
description | varchar | 描述 |
3. 权限表 (t_permission
)
字段 | 类型 | 描述 |
---|---|---|
id | bigint | 权限ID (PK) |
permission_string | varchar | 权限字符串 (e.g., user:create) |
description | varchar | 描述 |
4. 用户-角色关联表 (t_user_role
)
字段 | 类型 | 描述 |
---|---|---|
id | bigint | (PK) |
user_id | bigint | 用户ID (FK) |
role_id | bigint | 角色ID (FK) |
5. 角色-权限关联表 (t_role_permission
)
字段 | 类型 | 描述 |
---|---|---|
id | bigint | (PK) |
role_id | bigint | 角色ID (FK) |
permission_id | bigint | 权限ID (FK) |
通过这五张表,就可以构建起一个非常灵活和强大的权限体系。
六、总结
- 权限优先:始终围绕"权限"来设计你的安全模型,角色只是权限的便捷容器。
- 拥抱通配符:充分利用Shiro的Wildcard Permissions,它能极大地简化你的权限定义和检查逻辑。
- 缓存是关键:对于任何生产系统,必须开启授权缓存,并在权限变更时手动清除缓存,以获得最佳性能。
- 注解为主,编程为辅:优先使用声明式的注解来保护你的业务逻辑,只在必要时使用编程式检查。
- 设计先行:在编码之前,花时间设计好你的权限表结构和权限字符串规则,这将事半功倍。
掌握了Shiro的授权机制,你就拥有了保护任何复杂应用的核心能力。
📚 系列导航
🔍 查看完整系列目录
如果本文对你有帮助,欢迎点赞、收藏和评论!你的支持是我持续创作的动力!🌟