Java - SpringBoot整合Shiro之缓存功能
前言
在 上一篇文章 主要讲了Shiro
权限授权和认证跳过。本篇文章就主要讲解如何整合Shiro
和Redis
。这样就避免携带同一个Token
的时候,每次取查询数据库了。可以利用一个缓存,减轻DB
的压力。
一. SpringBoot 整合Redis
添加pom
依赖:
<!--redis整合springboot-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
1.1 配置 RedisTemplate
我们可以在application.yml
文件中添加Redis
相关的配置:
spring:
redis:
database: 0 # Redis数据库索引(默认为0)
host: xxx # Redis的服务地址
port: xxx # Redis的服务端口
password: xxx # Redis密码
jedis:
pool:
max-active: 8 # 连接池最大连接数(使用负值表示没有限制)
max-wait: -1 # 连接池最大阻塞等待时间(使用负值表示没有限制)
max-idle: 8 # 连接池中的最大空闲连接
min-idle: 0 # 连接池中的最小空闲链接
timeout: 30000 # 连接池的超时时间(毫秒)
添加Redis
的配置类RedisConfig
:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* @Date 2022/11/15 13:56
* @Created by jj.lin
*/
@Configuration
public class RedisConfig {
/**
* 实例化 RedisTemplate 对象
*
* @return
*/
@Bean
public RedisTemplate<String, Object> functionDomainRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
initDomainRedisTemplate(redisTemplate, redisConnectionFactory);
return redisTemplate;
}
/**
* 设置数据存入 redis 的序列化方式,并开启事务
*
* @param redisTemplate
* @param factory
*/
private void initDomainRedisTemplate(RedisTemplate<String, Object> redisTemplate, RedisConnectionFactory factory) {
// 配置序列化器。如果不配置Serializer,那么存储的时候缺省使用String,如果用User类型存储,那么会提示错误User can't cast to String!
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
// 开启事务
redisTemplate.setEnableTransactionSupport(true);
redisTemplate.setConnectionFactory(factory);
}
}
1.2 Shiro整合Redis缓存配置
1.我们自定义一个缓存管理器RedisCacheManager
,需要继承CacheManager
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.apache.shiro.cache.CacheManager;
/**
* @Date 2022/11/15 13:52
* @Created by jj.lin
*/
public class RedisCacheManager implements CacheManager {
@Override
public <K, V> Cache<K, V> getCache(String s) throws CacheException {
return new RedisCache<K,V>(s);
}
}
2.上面的RedisCache
是我们自定义的实现,我们需要重写相关的put/get
函数。这样Shiro
在认证的时候,就会通过我们自定义的put/get
函数去存储/获得缓存了。
import com.pro.config.SpringBeanUtil;
import com.pro.constant.JwtConstant;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException;
import org.springframework.data.redis.core.RedisTemplate;
import java.util.Collection;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* @Date 2022/11/15 13:52
* @Created by jj.lin
*/
@Slf4j
public class RedisCache<K, V> implements Cache<K, V> {
private String cacheName;
public RedisCache(String cacheName) {
this.cacheName = cacheName;
}
private RedisTemplate getRedisTemplate() {
RedisTemplate redisTemplate = SpringBeanUtil.getBean(RedisTemplate.class);
return redisTemplate;
}
@Override
public V get(K k) throws CacheException {
RedisTemplate redisTemplate = getRedisTemplate();
V v = (V) redisTemplate.opsForHash().get(cacheName, k.toString());
Long expire = redisTemplate.getExpire(cacheName);
log.info("从 {} 中获取Key:{}, Value:{},剩余缓存时长:{}", cacheName, k, v, expire);
return v;
}
@Override
public V put(K k, V v) throws CacheException {
log.info("从 {} 中插入Key:{}, Value:{}", cacheName, k, v);
RedisTemplate redisTemplate = getRedisTemplate();
redisTemplate.opsForHash().put(cacheName, k.toString(), v);
// 可以设置一个缓存的超时时间
redisTemplate.expire(cacheName, JwtConstant.TIMEOUT, TimeUnit.MINUTES);
return null;
}
@Override
public V remove(K k) throws CacheException {
return (V) getRedisTemplate().opsForHash().delete(cacheName, k.toString());
}
@Override
public void clear() throws CacheException {
getRedisTemplate().delete(cacheName);
}
@Override
public int size() {
return getRedisTemplate().opsForHash().size(cacheName).intValue();
}
@Override
public Set<K> keys() {
return getRedisTemplate().opsForHash().keys(cacheName);
}
@Override
public Collection<V> values() {
return getRedisTemplate().opsForHash().values(cacheName);
}
}
3.常量JwtConstant
:
public class JwtConstant {
public static final String USER_ID = "userId";
public static final Integer TIMEOUT = 10;
}
4.然后Shiro
开启缓存功能的配置ShiroConfig
:
原代码:
@Bean
public Realm realm() {
JwtRealm jwtRealm = new JwtRealm();
return jwtRealm;
}
新代码:
@Bean
public Realm realm() {
JwtRealm jwtRealm = new JwtRealm();
// 开启缓存,设置缓存管理器
jwtRealm.setCachingEnabled(true);
jwtRealm.setAuthenticationCachingEnabled(true);
jwtRealm.setAuthorizationCachingEnabled(true);
jwtRealm.setCacheManager(new RedisCacheManager());
return jwtRealm;
}
1.3 测试
1.我们先登录一下:拿到一个最新的Token
:
2.第一次访问getUser
接口(需要经过JWT
认证):
为什么这里有两次插入?因为我getUser
接口还有身份校验的过程:
@RequiresRoles("user")
@PostMapping("/getUser")
public String getUser() {
Long userId = JwtUtil.getUserId();
Subject currentUser = SecurityUtils.getSubject();
if (userId != null) {
return "成功拿到用户信息: " + currentUser.getPrincipal();
}
return "用户信息为空";
}
相当于:
doGetAuthenticationInfo
:第一次认证的时候,插入了一条缓存。doGetAuthorizationInfo
:第二次身份校验的时候,又更新了一次缓存。
3.第二次访问:可见没有插入的日志了。同时Degbug
打个断点也能发现,上面两个函数,只有第一次执行的时候会访问。后续就不会再进入了。因为缓存的原因。
我们存入Redis
中的hashKey
就是我们的Token
。所以在有效时间内,带着相同Token
的请求,都不需要经过认证了。