shiro学习五:使用springboot整合shiro。在前面学习四的基础上,增加shiro的缓存机制,源码讲解:认证缓存、授权缓存。
文章目录
- 前言
- 1. 直接上代码最后在讲解
- 1.1 新增的pom依赖
- 1.2 RedisCache.java
- 1.3 RedisCacheManager.java
- 1.4 jwt的三个类
- 1.5 ShiroConfig.java新增Bean
- 2. 源码讲解。
- 2.1 shiro 缓存的代码流程。
- 2.2 缓存流程
- 2.2.1 认证和授权简述
- 2.2.2 AuthenticatingRealm.getAuthenticationInfo() 认证缓存。
- 2.2.3 AuthorizingRealm.getAuthorizationInfo() 授权缓存。
- 2.3 redis缓存的设置入口(个人认为是重点)
- 3. 问题解决
- 3.1 授权的流程是怎么做的
- 3.2 @RequiresPermissions("sys:user:list")是如何工作的
- **1. 基本原理**
- **2. 具体流程**
- **1. 用户登录阶段**
- **2. 调用 `listUsers` 方法时**
- **3. 匹配机制**
前言
-
shiro学习一:了解shiro,学习执行shiro的流程。使用springboot的测试模块学习shiro单应用(demo 6个)
-
shiro学习二:shiro的加密认证详解,加盐与不加盐两个版本。
-
shiro学习三:shiro的源码分析
-
密码专辑:对密码加盐加密,对密码进行md5加密,封装成密码工具类
-
shiro学习四:使用springboot整合shiro,正常的企业级后端开发shiro认证鉴权流程。使用redis做token的过滤。md5做密码的加密。
-
代码所在地:https://github.com/fengfanli/springboot-shiro 记得给个星哦。
-
本文详细介绍了在Java Spring Boot项目中使用Apache Shiro进行权限管理的实现方式。通过整合Redis作为缓存管理器,实现了用户权限的缓存,提高了权限验证的效率。文章还解析了@RequiresPermissions注解的工作原理,说明了如何通过AOP机制拦截方法,并进行权限字符串的匹配校验,确保只有具备相应权限的用户能够访问受保护的方法。整体上,文章为Shiro在Spring Boot项目中的应用提供了全面的技术指导。
1. 直接上代码最后在讲解
1.1 新增的pom依赖
<!--JWT-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
1.2 RedisCache.java
package com.feng.shiro;
import com.alibaba.fastjson.JSON;
import com.feng.constant.Constant;
import com.feng.jwt.JwtTokenUtil;
import com.feng.utils.RedisUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.apache.shiro.util.CollectionUtils;
import java.util.*;
import java.util.concurrent.TimeUnit;
/**
* @ClassName: RedisCache
* @Description: 缓存工具类
* @createTime: 2020/2/9 20:42
* @Author: 冯凡利
* @UpdateUser: 冯凡利
* @Version: 0.0.1
*/
/**
* 这里的 RedisUtils 不能注入
*
* @param <K>
* @param <V>
*/
@Slf4j
public class RedisCache<K, V> implements Cache<K, V> {
private final static String PREFIX = "shiro-cache:";
private String cacheKey;
private long expire = 24; // 24 小时
private RedisUtil redisUtil;
public RedisCache(String name, RedisUtil redisUtil) {
// this.cacheKey=PREFIX+name+":";
this.cacheKey = Constant.IDENTIFY_CACHE_KEY;
this.redisUtil = redisUtil;
}
/**
* 根据 key 值 获取 权限信息
*
* @param key jwt
* @return
* @throws CacheException
*/
@Override
public V get(K key) throws CacheException {
log.info("Shiro 从缓存中获取数据 KEY 值[{}]", key);
if (key == null) {
return null;
}
try {
String redisCacheKey = getRedisCacheKey(key);
Object rawValue = redisUtil.get(redisCacheKey); // 根据key 获取 数据
if (rawValue == null) {
return null;
}
SimpleAuthorizationInfo info = JSON.parseObject(rawValue.toString(), SimpleAuthorizationInfo.class);
V value = (V) info;
return value;
} catch (Exception e) {
throw new CacheException(e);
}
}
/**
* 存值
*
* @param key jwt
* @param value
* @return
* @throws CacheException
*/
@Override
public V put(K key, V value) throws CacheException {
log.info("put key [{}]", key);
if (key == null) {
log.warn("Saving a null key is meaningless, return value directly without call Redis.");
return value;
}
try {
String redisCacheKey = getRedisCacheKey(key); // cacheKey + userId
redisUtil.set(redisCacheKey, value != null ? value : null, expire, TimeUnit.HOURS);
return value;
} catch (Exception e) {
throw new CacheException(e);
}
}
/**
* 根据 key 值 删除缓存的值
*
* @param key
* @return
* @throws CacheException
*/
@Override
public V remove(K key) throws CacheException {
log.info("remove key [{}]", key);
if (key == null) {
return null;
}
try {
String redisCacheKey = getRedisCacheKey(key);
Object rawValue = redisUtil.get(redisCacheKey);
V previous = (V) rawValue;
redisUtil.delete(redisCacheKey);
return previous;
} catch (Exception e) {
throw new CacheException(e);
}
}
/**
* 清除 所有的值
*
* @throws CacheException
*/
@Override
public void clear() throws CacheException {
log.debug("clear cache");
Set<String> keys = null;
try {
keys = redisUtil.keys(this.cacheKey + "*");
} catch (Exception e) {
log.error("get keys error", e);
}
if (keys == null || keys.size() == 0) {
return;
}
for (String key : keys) {
redisUtil.delete(key);
}
}
/**
* 获取 redis 所存的 缓存数的大小
*
* @return
*/
@Override
public int size() {
int result = 0;
try {
result = redisUtil.keys(this.cacheKey + "*").size();
} catch (Exception e) {
log.error("get keys error", e);
}
return result;
}
/**
* 获取key值
*
* @return
*/
@SuppressWarnings("unchecked")
@Override
public Set<K> keys() {
Set<String> keys = null;
try {
keys = redisUtil.keys(this.cacheKey + "*");
} catch (Exception e) {
log.error("get keys error", e);
return Collections.emptySet();
}
if (CollectionUtils.isEmpty(keys)) {
return Collections.emptySet();
}
Set<K> convertedKeys = new HashSet<>();
for (String key : keys) {
try {
convertedKeys.add((K) key);
} catch (Exception e) {
log.error("deserialize keys error", e);
}
}
return convertedKeys;
}
/**
* 获取 value值
*
* @return
*/
@Override
public Collection<V> values() {
Set<String> keys = null;
try {
keys = redisUtil.keys(this.cacheKey + "*");
} catch (Exception e) {
log.error("get values error", e);
return Collections.emptySet();
}
if (CollectionUtils.isEmpty(keys)) {
return Collections.emptySet();
}
List<V> values = new ArrayList<V>(keys.size());
for (String key : keys) {
V value = null;
try {
value = (V) redisUtil.get(key);
} catch (Exception e) {
log.error("deserialize values= error", e);
}
if (value != null) {
values.add(value);
}
}
return Collections.unmodifiableList(values);
}
/**
* 获取 redis 中的 缓存 key ,很重要,
*
* @param key
* @return
*/
private String getRedisCacheKey(K key) {
if (null == key) {
return null;
} else {
return this.cacheKey + JwtTokenUtil.getUserId(key.toString());
}
}
}
1.3 RedisCacheManager.java
package com.feng.shiro;
import com.feng.utils.RedisUtil;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.apache.shiro.cache.CacheManager;
import org.springframework.beans.factory.annotation.Autowired;
/**
* @ClassName: RedisCacheManager
* @Description: 描述
* @createTime: 2020/2/9 21:14
* @Author: 冯凡利
* @UpdateUser: 冯凡利
* @Version: 0.0.1
*/
public class RedisCacheManager implements CacheManager {
@Autowired
private RedisUtil redisUtil;
@Override
public <K, V> Cache<K, V> getCache(String s) throws CacheException {
return new RedisCache<>(s, redisUtil);
}
}
1.4 jwt的三个类
我不贴了,不是核心代码,可直接看GitHub。
四个部分:
- JwtTokenUtil.java:jwt的工具类
- JwtPropertiesConfig.java:application.properties 中的jwt的属性读取类
- InitializerJwtPropertiesConfig.java:将 JwtPropertiesConfig.java进行注入。
- application.properties 中的jwt的自定义属性配置。
1.5 ShiroConfig.java新增Bean
新增了redisCacheManager bean,然后再 getShiroRealm() 函数中 ,通过 Realm(ShiroRealm) 进行设置 缓存。
/**
* shiro 的 缓存管理器
* 需要在 ShiroRealm bean 中进行设置
* @return
*/
@Bean(name = "redisCacheManager")
public RedisCacheManager redisCacheManager() {
return new RedisCacheManager();
}
/**
* 登录的 认证域
*
* @param hashedCredentialsMatcher
* @return
*/
@Bean(name = "shiroRealm")
public ShiroRealm getShiroRealm(@Qualifier("shiroHashedCredentialsMatcher") HashedCredentialsMatcher hashedCredentialsMatcher,
@Qualifier("redisCacheManager") RedisCacheManager redisCacheManager) {
ShiroRealm shiroRealm = new ShiroRealm();
// 自定义 处理 token 过滤
shiroRealm.setCredentialsMatcher(hashedCredentialsMatcher);
// 自定义 缓存管理器
shiroRealm.setCacheManager(redisCacheManager);
return shiroRealm;
}
shiro 新增的 shiro 缓存 代码到此结束。
其他代码没有展示,是因为 shiro 缓存的代码只有这些。
2. 源码讲解。
2.1 shiro 缓存的代码流程。
- 首先编写 RedisCache.java:实现 Cache 接口,再这里重写redis的一些操作函数
- 编写RedisCacheManager.java 类:实现 CacheManager 接口,将RedisCache.java 类加入到 缓存管理中。
- 编写ShiroConfig.java 类:将RedisCacheManager注入生Bean,然后再Realm(本案例是ShiroRealm)中设置
缓存管理
,同时也是设置资格匹配
的地方。
2.2 缓存流程
shiro的核心是再 数据域中,也就是Realm,本案例的是ShiroRealm,继承 AuthorizingRealm
授权域, 授权域 继承了 AuthenticatingRealm
认证域,认证域 又继承了 CachingRealm
缓存域。这三个类是shiro 认证、授权、缓存 三个最重要的类。
2.2.1 认证和授权简述
继承了 AuthorizingRealm
授权域,就要重写两个最重要的方法
-
认证函数 doGetAuthenticationInfo(),既然认证,重写的就是
AuthenticatingRealm
认证域 中的函数。在函数getAuthenticationInfo()
中。源码截图如下:
-
授权函数 doGetAuthorizationInfo(),既然授权,重写的就是
AuthorizingRealm
授权域 中的函数。在函数getAuthorizationInfo()
中,源码截图如下:
在 Shiro 中,认证缓存(authentication cache) 和 授权缓存(authorization cache) 是两个不同的缓存,它们用于不同的目的:
- 认证缓存 (authenticationCache):用于缓存用户的认证信息(如,用户是否存在,凭证是否正确等)。这通常是一个比较频繁更新的缓存,因为用户登录的凭证(如密码)是动态变化的,可能会变得无效。
- 授权缓存 (authorizationCache):用于缓存用户的授权信息(如,用户的角色、权限等)。授权信息通常不太会频繁变动,因此可以缓存很长时间。它一般是用于存储用户的角色和权限信息,以减少每次请求时对数据库或外部系统的查询。
2.2.2 AuthenticatingRealm.getAuthenticationInfo() 认证缓存。
该函数的第一句就是:AuthenticationInfo info = this.getCachedAuthenticationInfo(token);
该函数就是从缓存中获取认证信息。
先说答案:shiro不推荐、默认不支持 认证从缓存中获取。
代码中可以看到 在AuthenticatingRealm类属性 this.authenticationCachingEnabled = false;
在构造函数中 默认为 false,不开启缓存。
究其原因:用于缓存用户的认证信息(如,用户是否存在,凭证是否正确等)。这通常是一个比较频繁更新的缓存,因为用户登录的凭证(如密码)是动态变化的,可能会变得无效。
所以不推荐缓存,通过debug,也可以看到。
2.2.3 AuthorizingRealm.getAuthorizationInfo() 授权缓存。
授权缓存 在shiro 中是支持的。因为 AuthorizingRealm 授权域类中的 类属性 this.authorizationCachingEnabled = true;
在构造函数中默认为true的。
getAuthorizationInfo() 函数就是从缓存中进行获取的,没有获取到在 进入 到 doGetAuthorizationInfo() 函数中进行获取、授权。
可以通过debug的方式一点点去查看。
如果没有设置redis作为缓存管理,默认可以配置缓存,但是shiro并没有帮我指定缓存,估计是怕默认指定之后怕内存使用过高,需要自行设定,接下来讲解。
2.3 redis缓存的设置入口(个人认为是重点)
前边讲的都是逻辑流程,如何从缓存中获取。我在读源码的时候我就很好奇,那设置redis作为缓存的地方在哪里呢?默认是用内存作为缓存的设置又在哪里呢?抱着这些疑问继续走。
答案:入口就在ShiroConfig中 Realm 的配置中。
ShiroRealm shiroRealm = new ShiroRealm();
// 自定义 处理 token 过滤
shiroRealm.setCredentialsMatcher(hashedCredentialsMatcher);
// 自定义 缓存管理器
shiroRealm.setCacheManager(redisCacheManager);
new ShiroRealm()时 ;会执行 AuthorizingRealm、AuthenticatingRealm的构造函数。构造函数中会设置 缓存启动情况、缓存的配置情况。
-
其中
AuthenticatingRealm
认证域的构造函数中,会默认指定SimpleCredentialsMatcher
作为 凭证匹配器 的 类,shiro还提供了HashedCredentialsMatcher
作为 凭证匹配器 类(具体有几个,看CredentialsMatcher
类的实现类有几个即可,下图所示)。后面我们自定义了 凭证匹配器 类CustomHashedCredentialsMatcher
,其中的doCredentialsMatch()
函数就是默认 认证的最核心的函数。
-
AuthorizingRealm 授权域的构造函数中,会默认指定 缓存管理(在CachingRealm 类中)、凭证匹配器(认证域类中) 的初始化,默认都是null。
-
shiroRealm.setCredentialsMatcher(hashedCredentialsMatcher);
:调用 认证域类 中的方法设置凭证匹配器
。 -
shiroRealm.setCacheManager(redisCacheManager);
:调用缓存域类
中的方法设置缓存管理器 以及 实现缓存的方式(第二行语句),该方法是个接口,有两个实现方法:一个是 认证域,一个是授权域。往下走。 -
这里就是 授权域 中的
afterCacheManagerSet()
函数,- 第一个函数调用父类也就是认证域上的函数,会继续调用
this.getAvailableAuthenticationCache()
。认证域前边说了,认证缓存shiro默认是关闭的,debug走一边流程就明白了;重点就是授权域了。 - 在第二个函数上,先判断是否有缓存,为null,然后再看 授权域 是否支持 授权缓存(默认支持,前边说了);然后进入懒加载缓存(下面第二张图);到获取缓存的地方,缓存管理不为null,是redis的,接着从158行,获取缓存,158行往下debug,就是上面的1.3小节的
RedisCacheManager.java
,将redis返回。
- 第一个函数调用父类也就是认证域上的函数,会继续调用
-
我又来问题了,如果没有redis,走的是哪个呢?首先我也不知道,得先把redis缓存管理给去掉。看看使用的哪个。CacheManager.java 是接口,其实现类截图如下,第一个是我实现的,其余三个都是shiro自带的(第二个应该不是,看源码)。
-
通过源码查看,shiro并没有默认使用缓存配置,仅仅是打开了缓存配置,可以供我们使用,如果想用使用缓存需要自行配置缓存,想上面配置 redis一样,将下面的第二个或者第四个配置到缓存管理中去。
到这里为止源码讲解完啦。
3. 问题解决
3.1 授权的流程是怎么做的
这个需要有背景的。
我基于这个项目来说一下
前端 LayUI+thymeleaf
后端使用springboot、shiro作为权限管理。
根据请求 /api/users,进行举例。
前端发请求后,会先认证,此处略。
然后会先从前端标签获取 权限字符串 sys:user:list,这里使用到了 thymeleaf,然后 通过 doGetAuthorizationInfo()获取所有权限字符串和角色,然后与从前端拿到的 权限字符串 sys:user:list进行对比,若包含,这通过。否则失败
3.2 @RequiresPermissions(“sys:user:list”)是如何工作的
1. 基本原理
@RequiresPermissions("sys:user:list")
是 Apache Shiro 提供的权限控制注解,用于声明访问某个方法或类需要的特定权限。
它的工作机制如下:
-
拦截注解:
- Shiro 提供了
AuthorizationAttributeSourceAdvisor
和 AOP(切面编程)机制来拦截被注解标记的类或方法。 - 当调用被标记的方法时,Shiro 拦截并执行权限校验。
- Shiro 提供了
-
获取当前用户的权限:
- Shiro 从当前登录用户的
Subject
(主体)中获取用户的权限信息(通常是一组权限字符串)。
- Shiro 从当前登录用户的
-
权限字符串匹配:
- Shiro 将注解中的权限字符串(如
"sys:user:list"
)与当前用户的权限列表进行比对。 - 如果用户的权限列表中包含注解指定的权限字符串,则校验通过;否则抛出
AuthorizationException
,拒绝访问。
- Shiro 将注解中的权限字符串(如
2. 具体流程
假设你有以下代码:
@RequiresPermissions("sys:user:list")
public void listUsers() {
// 获取用户列表逻辑
}
1. 用户登录阶段
- 用户通过登录接口认证成功后,Shiro 会根据用户身份(如用户名)从数据库或缓存中加载用户的所有权限,并存储在
Subject
中。 - 示例权限列表:
["sys:user:list", "sys:user:edit", "sys:user:delete"]
2. 调用 listUsers
方法时
- Shiro 拦截
@RequiresPermissions
注解,通过当前用户的Subject
获取权限列表。 - 调用
Subject.isPermitted("sys:user:list")
方法,具体逻辑如下:- Shiro 将注解中的权限字符串
"sys:user:list"
传递给AuthorizingRealm
的doGetAuthorizationInfo
方法。 doGetAuthorizationInfo
返回用户的权限列表。- Shiro 比较
"sys:user:list"
是否存在于用户的权限列表中:- 存在:校验通过,继续执行方法。
- 不存在:抛出异常
AuthorizationException
。
- Shiro 将注解中的权限字符串
3. 匹配机制
- 权限字符串可以使用通配符(Wildcard)进行匹配:
- 完全匹配:
"sys:user:list"
对比"sys:user:list"
,通过。 - 通配符匹配:
"sys:user:*"
对比"sys:user:list"
,通过。 - 多级通配符:
"sys:*:*"
对比"sys:user:list"
,通过。
- 完全匹配: