Springboot中的Shiro框架
首先了解下原理,了解下shrio的认证的逻辑,再讲解下springboot中,如何通过代码进行认证,授权操作。
在讲解认证授权之前,先介绍下RBAC模型,Shiro框架后续用上的最后本质上,还是通过查询这个库。
定义:RBAC(Role-Based Access Control)即基于角色的访问控制模型,核心是通过 “用户 - 角色 - 权限” 的层级关系实现访问控制,简化权限管理流程。
核心要素
:
如何做到不同的角色有不同的权限。
sys_menu
(权限 / 菜单表)存储系统中所有可操作的权限(目录、菜单、按钮),对应 “创建文档”“查看文档” 等具体权限。
id | parent_id | title | path | perms | component | type | created | sort_order | icon | status | updated |
---|---|---|---|---|---|---|---|---|---|---|---|
1 | 0 | 文档管理 | /document | NULL | Layout | 0 | 2024-01-01 00:00:00 | 1 | folder | 0 | 2024-01-01 00:00:00 |
2 | 1 | 文档列表 | /document/list | document:list | DocumentList | 1 | 2024-01-01 00:00:00 | 2 | list | 0 | 2024-01-01 00:00:00 |
3 | 2 | 创建文档 | NULL | document:create | NULL | 2 | 2024-01-01 00:00:00 | 3 | plus | 0 | 2024-01-01 00:00:00 |
4 | 2 | 查看文档 | NULL | document:view | NULL | 2 | 2024-01-01 00:00:00 | 4 | eye | 0 | 2024-01-01 00:00:00 |
5 | 2 | 编辑文档 | NULL | document:edit | NULL | 2 | 2024-01-01 00:00:00 | 5 | edit | 0 | 2024-01-01 00:00:00 |
6 | 2 | 删除文档 | NULL | document:delete | NULL | 2 | 2024-01-01 00:00:00 | 6 | trash | 0 | 2024-01-01 00:00:00 |
7 | 0 | 系统管理 | /system | NULL | Layout | 0 | 2024-01-01 00:00:00 | 7 | setting | 0 | 2024-01-01 00:00:00 |
8 | 7 | 用户管理 | /system/user | user:manage | UserManage | 1 | 2024-01-01 00:00:00 | 8 | user | 0 | 2024-01-01 00:00:00 |
解释:
type
字段区分权限类型:0 = 目录(如 “文档管理”“系统管理”)、1 = 菜单(如 “文档列表”“用户管理”)、2 = 按钮(如 “创建文档”“删除文档”)。
perms
字段对应具体操作权限,与前文的 P1-P5 对应关系:
document:create
document:view
document:edit
document:delete
user:manage
sys_role
(角色表)存储系统中的角色,对应 “Admin、Manager、Employee、Guest”。
id | name | code | remark | created | updated |
---|---|---|---|---|---|
1 | 超级管理员 | ROLE_ADMIN | 拥有系统所有权限 | 2024-01-01 00:00:00 | 2024-01-01 00:00:00 |
2 | 部门经理 | ROLE_MANAGER | 拥有文档全操作权限 | 2024-01-01 00:00:00 | 2024-01-01 00:00:00 |
3 | 普通员工 | ROLE_EMPLOYEE | 拥有文档创建 / 查看 / 编辑权限 | 2024-01-01 00:00:00 | 2024-01-01 00:00:00 |
4 | 访客 | ROLE_GUEST | 仅拥有文档查看权限 | 2024-01-01 00:00:00 | 2024-01-01 00:00:00 |
解释:
code
字段为角色标识,用于权限校验(如ROLE_ADMIN
对应管理员)。sys_role_menu
(角色 - 权限关联表)关联角色与权限,定义每个角色可操作的具体权限。
id | role_id | menu_id | |
---|---|---|---|
1 | 1 | 3 | (Admin 拥有 “创建文档” 权限) |
2 | 1 | 4 | (Admin 拥有 “查看文档” 权限) |
3 | 1 | 5 | (Admin 拥有 “编辑文档” 权限) |
4 | 1 | 6 | (Admin 拥有 “删除文档” 权限) |
5 | 1 | 8 | (Admin 拥有 “用户管理” 权限) |
6 | 2 | 3 | (Manager 拥有 “创建文档” 权限) |
7 | 2 | 4 | (Manager 拥有 “查看文档” 权限) |
8 | 2 | 5 | (Manager 拥有 “编辑文档” 权限) |
9 | 2 | 6 | (Manager 拥有 “删除文档” 权限) |
10 | 3 | 3 | (Employee 拥有 “创建文档” 权限) |
11 | 3 | 4 | (Employee 拥有 “查看文档” 权限) |
12 | 3 | 5 | (Employee 拥有 “编辑文档” 权限) |
13 | 4 | 4 | (Guest 仅拥有 “查看文档” 权限) |
解释:
sys_user
(用户表)存储系统用户信息,对应 “张三、李四、王五、访客 001”。
id | username | password | avatar | phone | created | updated | last_login | status | is_delete | |
---|---|---|---|---|---|---|---|---|---|---|
1 | zhangsan | $2a101010xxxxxx(加密后) | /avatar/zhangsan.jpg | [email protected] | 13800138000 | 2024-01-01 00:00:00 | NULL | 2024-07-16 09:00:00 | 0 | 0 |
2 | lisi | $2a101010xxxxxx(加密后) | /avatar/lisi.jpg | [email protected] | 13900139000 | 2024-01-02 00:00:00 | NULL | 2024-07-16 09:30:00 | 0 | 0 |
3 | wangwu | $2a101010xxxxxx(加密后) | /avatar/wangwu.jpg | [email protected] | 13700137000 | 2024-01-03 00:00:00 | NULL | 2024-07-16 10:00:00 | 0 | 0 |
4 | guest001 | $2a101010xxxxxx(加密后) | /avatar/guest.jpg | [email protected] | NULL | 2024-07-16 08:00:00 | NULL | 2024-07-16 08:30:00 | 0 | 0 |
解释:
password
字段存储加密后的密码(如 BCrypt 加密),避免明文泄露。status=0
表示用户正常,is_delete=0
表示未删除(逻辑删除标记)。last_login
记录最近登录时间,用于追踪用户活动。sys_user_role
(用户 - 角色关联表)关联用户与角色,定义每个用户所属的角色。
id | user_id | role_id | |
---|---|---|---|
1 | 1 | 1 | (张三→超级管理员) |
2 | 2 | 2 | (李四→部门经理) |
3 | 3 | 3 | (王五→普通员工) |
4 | 4 | 4 | (访客 001→访客) |
解释:
user_id
和role_id
关联,实现 “用户→角色→权限” 的间接映射。Shiro 的功能可概括为四大基石及相关支持特性:
Shiro 架构主要包含三个核心概念:
Subject:当前用户(可指人、第三方服务等任何与软件交互的实体)
SecurityManager:管理所有 Subject,是 Shiro 架构的核心
Realm 是 Shiro 的核心安全数据访问对象(Security DAO),它:
类比理解:就像JDBC连接数据库,Realm是Shiro连接各种安全数据源的标准接口。这里不好解释,就是类似存放了相关用户数据。通过一些列操作之后得到相关的用户的数据。
下面有关realm不了解可以先跳过,博主也暂时能力有限,没有找到更好的表述方式,可以暂时先跳过。跟着流程表述就行,这里还是比较晦涩难懂。写到这里博主觉得,可以先跟着后面的springboot是如何是如何使用shrio框架的进行过一遍流程。有不懂得地方再进行查阅,先弄懂相关的认证、授权的逻辑。先过一遍,后面再慢慢的一点的弄懂。先用着,再慢慢的了解其特性。
Realm(域)是 Shiro 框架中连接应用与安全数据源的 “桥梁” 或 “连接器”。当进行认证(登录)和授权(访问控制)验证时,Shiro 会通过 Realm 获取用户及其权限信息。
其核心特点包括:
getAuthenticationInfo
方法public class UsernamePasswordToken implements HostAuthenticationToken, RememberMeAuthenticationToken {
private String username; // 用户名
private char[] password; // 密码
private boolean rememberMe; // 记住我
private String host; // 主机地址
}
getAuthorizationInfo
方法supports
方法getAuthenticationInfo
subject.login(token)
时执行Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
subject.login(token); // 此时触发身份验证
getAuthorizationInfo
subject.hasRole("角色名")
或subject.isPermitted("权限名")
时@RequiresRoles("角色名")
等注解时[@shiro.hasPermission name="权限名"][/@shiro.hasPermission]
)时// 开发者可见的调用入口
Subject currentUser = SecurityUtils.getSubject();
currentUser.login(new UsernamePasswordToken("admin", "password123"));
SecurityUtils.setSecurityManager()
初始化安全管理器SecurityManager作为中央调度器:
Authenticator
组件执行具体验证Authenticator(默认实现ModularRealmAuthenticator
):
@startuml
Authenticator -> AuthenticationStrategy : 应用验证策略
AuthenticationStrategy -> Realm1 : 查询凭证
AuthenticationStrategy -> Realm2 : 查询凭证
@enduml
setAuthenticator()
注入自定义实现AuthenticationStrategy控制多Realm协作方式:
策略类型 | 行为特征 | 适用场景 |
---|---|---|
AtLeastOneSuccess | 任意Realm验证成功即通过 | 多认证源并联 |
FirstSuccessful | 采用首个成功的验证结果 | 认证源优先级排序 |
AllSuccessful | 要求全部Realm验证成功 | 多因素认证 |
Realm实际执行凭证校验:
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) {
String username = (String) token.getPrincipal();
User user = userService.findByUsername(username);
if(user == null) throw new UnknownAccountException();
return new SimpleAuthenticationInfo(user, user.getPassword(), getName());
}
IncorrectCredentialsException
:密码错误LockedAccountException
:账户锁定图片参考:https://blog.csdn.net/qq_45299673/article/details/122091352
流程如下:
// 开发者调用方式示例
Subject subject = SecurityUtils.getSubject();
boolean hasAccess = subject.isPermitted("user:delete");
boolean hasRole = subject.hasRole("admin");
组件 | 职责说明 | 默认实现类 |
---|---|---|
SecurityManager | 授权请求的中转调度 | DefaultSecurityManager |
Authorizer | 授权逻辑的抽象接口 | ModularRealmAuthorizer |
PermissionResolver | 权限字符串转换器 | WildcardPermissionResolver |
ModularRealmAuthorizer 的工作逻辑:
遍历所有配置的Realm
检查Realm是否实现Authorizer
接口
对符合条件的Realm调用
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-web-starter</artifactId>
<version>1.10.0</version>
</dependency>
public class AccountRealm extends AuthorizingRealm {
/*
* doGetAuthorizationInfo:权限校验
* 获取用户权限信息
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principal) {
AccountProfile profile =(AccountProfile) principal.getPrimaryPrincipal();
return info;
}
/*
* doGetAuthenticationInfo:认证校验
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
return new SimpleAuthenticationInfo(profile, jwtToken.getCredentials(), getName());
}
}
认证和授权的过程通常需要把这两个方法实现就可以
通常有两种写法,可以对着流程图
方法一:直接用Jwt种的token进行认证,无需要密码验证
*
* doGetAuthenticationInfo:认证校验
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
JwtToken jwtToken = (JwtToken) authenticationToken;
// 校验jwt
Claims claim = jwtUtils.getClaimByToken((String) jwtToken.getPrincipal());
if(claim == null || jwtUtils.isTokenExpired(claim.getExpiration())) {
throw new UnauthenticatedException("请重新登录");
}
//获取到用户的信息,这里放到claim中,在设计的产生token的过程中
//以下是业务逻辑
String userId = claim.getSubject();
SysUser sysUser = userService.getById(Long.valueOf(userId));
if (sysUser == null) {
throw new UnknownAccountException("账户不存在");
}
if (sysUser.getStatus() == -1) {
throw new LockedAccountException("账户已被锁定");
}
AccountProfile profile = new AccountProfile();
BeanUtil.copyProperties(sysUser, profile);
/
return new SimpleAuthenticationInfo(profile, jwtToken.getCredentials(), getName());
}
方法一最重要的是通过return new SimpleAuthenticationInfo(profile, jwtToken.getCredentials(), getName());进行返回。
参数一为用户,参数二为token,参数三为realm
方法二:需要进行密码,以及加盐的过程
参考:https://blog.csdn.net/hubeilihao/article/details/106414363
public class ShiroRealm extends AuthorizingRealm {
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
// 第一步从token中取出用户名
String userName = (String) token.getPrincipal();
// 第二步:根据用户输入的userName从数据库查询
User user = userService.findByUsername("userName");
if(user==null){
return null;//用户不存在
}
//第三步 从数据库取该用户的passw
String password = user.getPassword();
// 第四步 加盐
String salt = userCode;
.......其他判断逻辑......
// 第五步 创建SimpleAuthenticationInfo
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(user,password,ByteSource.Util.bytes(salt), this.getName());
//第六步 返回
return simpleAuthenticationInfo;// return的过程完成 password的验证
}
}
/*
* doGetAuthorizationInfo:权限校验
* 获取用户权限信息
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principal) {
AccountProfile profile =(AccountProfile) principal.getPrimaryPrincipal();
Long userId = profile.getId();
//获取角色
List<SysRole> roles = sysRoleService.listRolesByUserId(userId);
//获取菜单
List<SysMenu> menus = sysMenuService.listMenusByUserId(userId);
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.setRoles(roles.stream().map(SysRole::getCode).collect(Collectors.toSet()));
info.setStringPermissions(menus.stream().map(SysMenu::getPerms).collect(Collectors.toSet()));
return info;
}
PrincipalCollection
是 Shiro 中表示用户身份信息的集合接口,它:
AuthenticationInfo
// 获取主要主体(通常是认证时设置的第一个主体)
Object getPrimaryPrincipal()
// 获取所有主体(当使用多Realm认证时可能有多个)
Collection> getPrincipals()
PrincipalCollection
获取主要主体AccountProfile
类型(这是在认证阶段存入的用户概要信息)最重要的一点是这个,这里都用set集合
//创建 SimpleAuthorizationInfo 对象
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
//设置角色集合(提取角色编码)
info.setRoles(roles.stream().map(SysRole::getCode).collect(Collectors.toSet()));
//设置权限字符串集合(提取权限标识)
info.setStringPermissions(menus.stream().map(SysMenu::getPerms).collect(Collectors.toSet()));
@RequiresRoles
或 @RequiresPermissions
注解时subject.hasRole()
或 subject.isPermitted()
时@Configuration
public class ShiroConfig {
//自定义Realm永远完成具体的认证和授权操作
// Realm的父类抽象类
// AuthenticatingRealm 只负责认证(登录)的Realm父类
// AuthorizingRealm 负责认证(登录)和授权 的Realm父类
@Bean
public Realm realm() {
return new AccountRealm();
}
/**
* 配置ShiroFilterChainDefinition
* @return
* 定义 URL 路径与 Shiro 过滤器的映射关系,即哪些路径需要什么样的安全控制。
* 默认情况下,Shiro 框架会为所有路径添加一个 "authc" 过滤器,即要求用户进行身份验证。
*/
@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition() {
DefaultShiroFilterChainDefinition chain = new DefaultShiroFilterChainDefinition();
chain.addPathDefinition("/app/**", "anon");
chain.addPathDefinition("/sys/login", "anon");
chain.addPathDefinition("/**", "jwt");
return chain;
}
/**
* 配置ShiroFilterFactoryBean
* @param securityManager
* @param shiroFilterChainDefinition
* @return
* SecurityManager:Shiro 的核心安全管理器(由 Spring 自动注入)
* ShiroFilterChainDefinition:前面定义的 URL 过滤规则(由 Spring 自动注入)
*置一个Shiro的过滤器bean,这个bean将配置Shiro相关的一个规则的拦截
* //例如什么样的请求可以访问,什么样的请求不可以访问等等
*/
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager,
ShiroFilterChainDefinition shiroFilterChainDefinition) {
//这是 Shiro 提供的工厂类,用于创建过滤器链
// 创建Shiro的拦截的拦截器 ,用于拦截我们的用户请求
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
//设置Shiro的安全管理,设置管理的同时也会指定某个Realm 用来完成我们权限分配
shiroFilter.setSecurityManager(securityManager);
/**
* shiroFilter.setFilters(MapUtil.of("jwt", new JwtFilter()));
* 使用 Hutool 的 MapUtil.of() 创建了一个单元素 Map
* Key "jwt":过滤器名称(在路径规则中引用)
* Value new JwtFilter():自定义的 JWT 过滤器实例
* 这样就将自定义的 JwtFilter 注册到了 Shiro 过滤器系统中
*/
shiroFilter.setFilters(MapUtil.of("jwt",new JwtFilter()));
//获取并设置过滤器链映射
//从 shiroFilterChainDefinition 获取之前定义的路径-过滤器映射
//(即 /app/**=anon, /sys/login=anon, /**=jwt)
//将这些映射设置到 shiroFilter 中
//定义一个Map集合,这个Map集合中存放的数据全部都是规则,用于设置通知Shiro什么样的请求可以访问什么样的请求不可以访问
Map<String, String> filterMap = shiroFilterChainDefinition.getFilterChainMap();
shiroFilter.setFilterChainDefinitionMap(filterMap);
return shiroFilter;
}
}
() 创建了一个单元素 Map
* Key "jwt":过滤器名称(在路径规则中引用)
* Value new JwtFilter():自定义的 JWT 过滤器实例
* 这样就将自定义的 JwtFilter 注册到了 Shiro 过滤器系统中
*/
shiroFilter.setFilters(MapUtil.of("jwt",new JwtFilter()));
//获取并设置过滤器链映射
//从 shiroFilterChainDefinition 获取之前定义的路径-过滤器映射
//(即 /app/**=anon, /sys/login=anon, /**=jwt)
//将这些映射设置到 shiroFilter 中
//定义一个Map集合,这个Map集合中存放的数据全部都是规则,用于设置通知Shiro什么样的请求可以访问什么样的请求不可以访问
Map<String, String> filterMap = shiroFilterChainDefinition.getFilterChainMap();
shiroFilter.setFilterChainDefinitionMap(filterMap);
return shiroFilter;
}
}