简单的shiro-jwt-demo开发实例
1.需求
之前公司使用的spring security框架作为登录框架,偶然间看到了公司其他老项目使用的shiro作为登录框架,所以简单了解了下此框架,参考网上部分代码,简单整理加工了下,得到了一个简单的shiro jwt redis结合的加工例子,方便以后学习使用。
2.主要实现过程
2.1 引入pom文件
主要引入shiro-starter对应的包,spring-boot-starter-parent包,以及一些实例工具包等
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<!-- springboot dependency -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.8.RELEASE</version>
<relativePath/>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>shiro-jwt-demo</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-web-starter</artifactId>
<version>1.9.0</version>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.10.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.3.8.RELEASE</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.6</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.5.7</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>2.0.32</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<!-- compiler -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.6</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
<!-- package -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.3.8.RELEASE</version>
</plugin>
</plugins>
</build>
</project>
2.2 redis部分实现
项目引入redis,作为用户基础信息存储的媒介,模拟查询的客户信息将以hash形式存入redis中,key为jwt工具生成的token信息,并且前端每次请求都需要带上token信息,进行登录token验证。redis实现主要是一下两部分代码
- config模板配置类(配置类以及序列化类)
- redisUtil工具类
2.2.1 配置redisConfig配置类
配置redis类的序列化与反序列化方式
package cn.git.shiro.config;
import org.springframework.beans.factory.annotation.Autowired;
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.StringRedisSerializer;
@Configuration
public class RedisConfig {
/**
* 自定义Redis序列化器,用于序列化Redis中的数据。
*/
private static final FastJson2JsonRedisSerializer REDIS_SERIALIZER = new FastJson2JsonRedisSerializer();
@Autowired
private RedisConnectionFactory redisConnectionFactory;
/**
* 配置并返回一个RedisTemplate bean,用于操作Redis数据库。
* 这个模板配置了各种序列化器,以确保键和值在与Redis进行交互时能够正确序列化和反序列化。
*
* @return RedisTemplate<String, Object> 一个配置好的Redis模板,适用于字符串键和任意类型值的操作。
*/
@Bean
public RedisTemplate<String, Object> redisTemplate() {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
// 设置Redis连接工厂
redisTemplate.setConnectionFactory(redisConnectionFactory);
// 禁用事务支持
redisTemplate.setEnableTransactionSupport(false);
// 序列化配置
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(REDIS_SERIALIZER);
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(REDIS_SERIALIZER);
redisTemplate.setDefaultSerializer(new StringRedisSerializer());
// 初始化模板
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
序列化反序列化类代码如下
package cn.git.shiro.config;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;
import com.alibaba.fastjson.serializer.SerializerFeature;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;
import org.springframework.stereotype.Component;
import java.nio.charset.Charset;
/**
* @description: redis的序列化
* @program: bank-credit-sy
* @author: lixuchun
* @create: 2024-03-27
*/
@Component
public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T> {
private final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
Class<T> clazz;
static {
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
}
/**
* 序列化
* @param t
* @return
* @throws SerializationException
*/
@Override
public byte[] serialize(T t) throws SerializationException {
if (null == t) {
return new byte[0];
}
return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);
}
/**
* 反序列化
* @param bytes
* @return
* @throws SerializationException
*/
@Override
public T deserialize(byte[] bytes) throws SerializationException {
if (null == bytes || 0 >= bytes.length) {
return null;
}
String str = new String(bytes, DEFAULT_CHARSET);
return JSON.parseObject(str, clazz);
}
}
2.2.2 实现redisUtil工具类
具体实现redis增删改查的逻辑
package cn.git.shiro.util;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.data.geo.*;
import org.springframework.data.redis.connection.RedisConnection;
import org.springframework.data.redis.connection.RedisGeoCommands;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.script.RedisScript;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* @description: Redis工具类
* @program: bank-credit-sy
* @author: lixuchun
* @create: 2024-03-27
*/
@Component
public class RedisUtil {
/**
* 模糊查询匹配
*/
private static final String FUZZY_ENQUIRY_KEY = "*";
@Autowired
private RedisTemplate<String, Object> redisTemplate;
/**
* 指定缓存失效时间
*
* @param key 键
* @param time 时间(秒)
* @return
*/
public boolean expire(String key, long time) {
try {
if (time > 0) {
redisTemplate.expire(key, time, TimeUnit.SECONDS);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据key 获取过期时间
*
* @param key 键 不能为null
* @return 时间(秒) 返回0代表为永久有效
*/
public long getExpire(String key) {
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}
/**
* 判断key是否存在
*
* @param key 键
* @return true 存在 false不存在
*/
public boolean hasKey(String key) {
try {
return redisTemplate.hasKey(key);
} catch (Exception e) {
return false;
}
}
/**
* 删除缓存
*
* @param key 可以传一个值 或多个
*/
@SuppressWarnings("unchecked")
public void del(String... key) {
if (key != null && key.length > 0) {
if (key.length == 1) {
redisTemplate.delete(key[0]);
} else {
redisTemplate.delete(CollectionUtils.arrayToList(key));
}
}
}
/**
* 普通缓存获取
*
* @param key 键
* @return 值
*/
public Object get(String key) {
return key == null ? null : redisTemplate.opsForValue().get(key);
}
/**
* 普通缓存放入
*
* @param key 键
* @param value 值
* @return true成功 false失败
*/
public boolean set(String key, Object value) {
try {
redisTemplate.opsForValue().set(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 普通缓存放入并设置时间
*
* @param key 键
* @param value 值
* @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
* @return true成功 false 失败
*/
public boolean set(String key, Object value, long time) {
try {
if (time > 0) {
redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
} else {
set(key, value);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 如果不存在,则设置对应key,value 键值对,并且设置过期时间
* @param key 锁key
* @param value 锁值
* @param time 时间单位second
* @return 设定结果
*/
public Boolean setNxEx(String key, String value, long time) {
return redisTemplate.opsForValue().setIfAbsent(key, value, time, TimeUnit.SECONDS);
}
/**
* 递增
*
* @param key 键
* @return
*/
public long incr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递增因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, delta);
}
/**
* 递减
*
* @param key 键
* @return
*/
public long decr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递减因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, -delta);
}
/**
* HashGet
*
* @param key 键 不能为null
* @param item 项 不能为null
* @return 值
*/
public Object hget(String key, String item) {
return redisTemplate.opsForHash().get(key, item);
}
/**
* 获取hashKey对应的所有键值
*
* @param key 键
* @return 对应的多个键值
*/
public Map<Object, Object> hmget(String key) {
return redisTemplate.opsForHash().entries(key);
}
/**
* 获取hashKey对应的所有键值
*
* @param key 键
* @return 对应的多个键值
*/
public List<Object> hmget(String key, List<Object> itemList) {
return redisTemplate.opsForHash().multiGet(key, itemList);
}
/**
* 获取key对应的hashKey值
*
* @param key 键
* @param hashKey 键
* @return 对应的键值
*/
public Object hmget(String key, String hashKey) {
return redisTemplate.opsForHash().get(key, hashKey);
}
/**
* HashSet
*
* @param key 键
* @param map 对应多个键值
* @return true 成功 false 失败
*/
public boolean hmset(String key, Map<String, Object> map) {
try {
redisTemplate.opsForHash().putAll(key, map);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* HashSet 并设置时间
*
* @param key 键
* @param map 对应多个键值
* @param time 时间(秒)
* @return true成功 false失败
*/
public boolean hmset(String key, Map<Object, Object> map, long time) {
try {
redisTemplate.opsForHash().putAll(key, map);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
*
* @param key 键
* @param item 项
* @param value 值
* @return true 成功 false失败
*/
public boolean hset(String key, String item, Object value) {
try {
redisTemplate.opsForHash().put(key, item, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
*
* @param key 键
* @param item 项
* @param value 值
* @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
* @return true 成功 false失败
*/
public boolean hset(String key, String item, Object value, long time) {
try {
redisTemplate.opsForHash().put(key, item, value);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除hash表中的值
*
* @param key 键 不能为null
* @param item 项 可以使多个 不能为null
*/
public void hdel(String key, Object... item) {
redisTemplate.opsForHash().delete(key, item);
}
/**
* 判断hash表中是否有该项的值
*
* @param key 键 不能为null
* @param item 项 不能为null
* @return true 存在 false不存在
*/
public boolean hHasKey(String key, String item) {
return redisTemplate.opsForHash().hasKey(key, item);
}
/**
* hash递增 如果不存在,就会创建一个 并把新增后的值返回
*
* @param key 键
* @param item 项
* @param by 要增加几(大于0)
* @return
*/
public double hincr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, by);
}
/**
* hash递减
*
* @param key 键
* @param item 项
* @param by 要减少记(小于0)
* @return
*/
public double hdecr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, -by);
}
/**
* 根据key获取Set中的所有值
*
* @param key 键
* @return
*/
public Set<Object> sGet(String key) {
try {
return redisTemplate.opsForSet().members(key);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 根据value从一个set中查询,是否存在
*
* @param key 键
* @param value 值
* @return true 存在 false不存在
*/
public boolean sHasKey(String key, Object value) {
try {
return redisTemplate.opsForSet().isMember(key, value);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将数据放入set缓存
*
* @param key 键
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSet(String key, Object... values) {
try {
return redisTemplate.opsForSet().add(key, values);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 将set数据放入缓存
*
* @param key 键
* @param time 时间(秒)
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSetAndTime(String key, long time, Object... values) {
try {
Long count = redisTemplate.opsForSet().add(key, values);
if (time > 0) {
expire(key, time);
}
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 获取set缓存的长度
*
* @param key 键
* @return
*/
public long sGetSetSize(String key) {
try {
return redisTemplate.opsForSet().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 移除值为value的
*
* @param key 键
* @param values 值 可以是多个
* @return 移除的个数
*/
public long setRemove(String key, Object... values) {
try {
Long count = redisTemplate.opsForSet().remove(key, values);
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 获取list缓存的内容
*
* @param key 键
* @param start 开始
* @param end 结束 0 到 -1代表所有值
* @return
*/
public List<Object> lGet(String key, long start, long end) {
try {
return redisTemplate.opsForList().range(key, start, end);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 获取list缓存的长度
*
* @param key 键
* @return
*/
public long lGetListSize(String key) {
try {
return redisTemplate.opsForList().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 通过索引 获取list中的值
*
* @param key 键
* @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
* @return
*/
public Object lGetIndex(String key, long index) {
try {
return redisTemplate.opsForList().index(key, index);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @return
*/
public boolean lSet(String key, Object value) {
try {
redisTemplate.opsForList().rightPush(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @param time 时间(秒)
* @return
*/
public boolean lSet(String key, Object value, long time) {
try {
redisTemplate.opsForList().rightPush(key, value);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @return
*/
public boolean lSet(String key, List<Object> value) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @param time 时间(秒)
* @return
*/
public boolean lSet(String key, List<Object> value, long time) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据索引修改list中的某条数据
*
* @param key 键
* @param index 索引
* @param value 值
* @return
*/
public boolean lUpdateIndex(String key, long index, Object value) {
try {
redisTemplate.opsForList().set(key, index, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 移除N个值为value
*
* @param key 键
* @param count 移除多少个
* @param value 值
* @return 移除的个数
*/
public long lRemove(String key, long count, Object value) {
try {
Long remove = redisTemplate.opsForList().remove(key, count, value);
return remove;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
public void testAdd(Double X, Double Y, String accountId) {
Long addedNum = redisTemplate.opsForGeo()
.add("cityGeoKey", new Point(X, Y), accountId);
System.out.println(addedNum);
}
public Long addGeoPoin() {
Point point = new Point(123.05778991994906, 41.188314667658965);
Long addedNum = redisTemplate.opsForGeo().geoAdd("cityGeoKey", point, 3);
return addedNum;
}
public void testNearByPlace() {
Distance distance = new Distance(100, Metrics.KILOMETERS);
RedisGeoCommands.GeoRadiusCommandArgs args = RedisGeoCommands
.GeoRadiusCommandArgs
.newGeoRadiusArgs()
.includeDistance()
.includeCoordinates()
.sortAscending()
.limit(5);
GeoResults<RedisGeoCommands.GeoLocation<Object>> results = redisTemplate.opsForGeo()
.radius("cityGeoKey", "北京", distance, args);
System.out.println(results);
}
public GeoResults<RedisGeoCommands.GeoLocation<Object>> testGeoNearByXY(Double X, Double Y) {
Distance distance = new Distance(100, Metrics.KILOMETERS);
Circle circle = new Circle(X, Y, Metrics.KILOMETERS.getMultiplier());
RedisGeoCommands.GeoRadiusCommandArgs args = RedisGeoCommands
.GeoRadiusCommandArgs
.newGeoRadiusArgs()
.includeDistance()
.includeCoordinates()
.sortAscending();
GeoResults<RedisGeoCommands.GeoLocation<Object>> results = redisTemplate.opsForGeo()
.radius("cityGeoKey", circle, distance, args);
System.err.println(results);
return results;
}
/**
* @Description: 执行lua脚本,只对key进行操作
* @Param: [redisScript, keys]
* @return: java.lang.Long
* @Date: 2021/2/21 15:00
*/
public Long executeLua(RedisScript<Long> redisScript, List keys) {
return redisTemplate.execute(redisScript, keys);
}
/**
* @Description: 执行lua脚本,只对key进行操作
* @Param: [redisScript, keys, value]
* @return: java.lang.Long
* @Date: 2021/2/21 15:00
*/
public Long executeLuaCustom(RedisScript<Long> redisScript, List keys, Object value) {
return redisTemplate.execute(redisScript, keys, value);
}
/**
* 时间窗口限流
* @param key key
* @param timeWindow 时间窗口
* @return
*/
public Integer rangeByScore(String key, Integer timeWindow) {
// 获取当前时间戳
Long currentTime = System.currentTimeMillis();
Set<Object> rangeSet = redisTemplate.opsForZSet().rangeByScore(key, currentTime - timeWindow, currentTime);
if (ObjectUtil.isNotNull(rangeSet)) {
return rangeSet.size();
} else {
return 0;
}
}
/**
* 新增Zset
* @param key
*/
public String addZset(String key) {
String value = IdUtil.simpleUUID();
Long currentTime = System.currentTimeMillis();
redisTemplate.opsForZSet().add(key, value, currentTime);
return value;
}
/**
* 删除Zset
* @param key
*/
public void removeZset(String key, String value) {
// 参数存在校验
if (ObjectUtil.isNotNull(redisTemplate.opsForZSet().score(key, value))) {
redisTemplate.opsForZSet().remove(key, value);
}
}
/**
* 通过前缀key值获取所有key内容(hash)
* @param keyPrefix 前缀key
* @param fieldArray 查询对象列信息
*/
public List<Object> getPrefixKeys(String keyPrefix, byte[][] fieldArray) {
if (StrUtil.isBlank(keyPrefix)) {
return null;
}
keyPrefix = keyPrefix.concat(FUZZY_ENQUIRY_KEY);
// 所有完整key值
Set<String> keySet = redisTemplate.keys(keyPrefix);
List<Object> objectList = redisTemplate.executePipelined(new RedisCallback<Object>() {
/**
* Gets called by {@link RedisTemplate} with an active Redis connection. Does not need to care about activating or
* closing the connection or handling exceptions.
*
* @param connection active Redis connection
* @return a result object or {@code null} if none
* @throws DataAccessException
*/
@Override
public Object doInRedis(RedisConnection connection) throws DataAccessException {
for (String key : keySet) {
connection.hMGet(key.getBytes(StandardCharsets.UTF_8), fieldArray);
}
return null;
}
});
return objectList;
}
}
2.2.3 application.yml中基本配置信息
服务端口,redis基本配置信息,自定义jwt验证信息
server:
port: 8099
servlet:
context-path: /shiro-server
spring:
redis:
host: 127.0.0.1
port: 6379
database: 0
#jwt部分配置信息
custom:
jwt:
secret: lOan#2o22@!
expire: 3600
2.3 jwt工具类实现
jwt是一个登录验证方式,提供header(加密方式)+载荷(用户定义信息)+sign(通过header中加密方式,通过自定义秘钥,加密载荷的信息),此信息生成,传递到服务端,服务端可以使用header+荷载信息与自定义秘钥生成sign信息,与传入sign进行比对,实现登录状态查验,具体工具如下
package cn.git.shiro.util;
import cn.hutool.core.util.ObjectUtil;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.Map;
/**
* @description: jwt工具类
* @program: bank-credit-sy
* @author: lixuchun
* @create: 2024-03-27
*/
@Component
public class JWTUtil {
/**
* 自定义jwt密钥
*/
@Value("${custom.jwt.secret}")
private String secret;
/**
* jwt过期时间
* 单位秒
*/
@Value("${custom.jwt.expire}")
private long expire;
/**
* 创建jwt
* @param username 用户名
* @param customClaimsMap 自定义jwt键值对
* @return
*/
public String createJWTToken(String username, Map<String, String> customClaimsMap) {
// 创建jwt构造器
JWTCreator.Builder builder = JWT.create();
if (ObjectUtil.isNotEmpty(customClaimsMap)) {
// 设置自定义值信息
customClaimsMap.forEach(builder::withClaim);
}
// 使用HS256算法
Algorithm algorithm = Algorithm.HMAC256(secret);
// 设置过期时间,单位秒
Date expireDate = new Date(System.currentTimeMillis() + expire * 1000);
return builder
.withExpiresAt(expireDate)
.sign(algorithm);
}
/**
* 判断jwt是否过期
* @param token
* @return
*/
public boolean ifTokenExpire(String token) {
try {
DecodedJWT decode = JWT.decode(token);
return decode.getExpiresAt().before(new Date());
} catch (Exception e) {
throw new RuntimeException("传入token解析失败,请确认!");
}
}
/**
* 验证jwt
* @param token
* @return
*/
public boolean verifyJWTToken(String token) {
try {
JWT.require(Algorithm.HMAC256(secret)).build().verify(token);
return true;
} catch (Exception e) {
return false;
}
}
/**
* 获取jwt自定义值
* @param token
* @param key
* @return
*/
public String getCustomClaim(String token, String key) {
try {
DecodedJWT decode = JWT.decode(token);
return decode.getClaim(key).asString();
} catch (Exception e) {
return null;
}
}
}
2.4 shiro部分实现
shiro部分实现,具体主要包含以下部分实现
- shiro对应的自定义customRealm
- shiro对应的自定义登录校验过滤器CsutomJwtFilter
- shiro对应配置类CommonShiroConfig
- shiro对应JWTToken实现
下面一一实现上面对应的四个基础类信息
2.4.1 自定义customRealm
此部分实现通过token获取redis中用户信息,以及jwt中token校验信息,全部通过后则封装好SimpleAuthenticationInfo认证信息即可,注意封装认证信息,在DefaultWebSecurityManager配置中没有指定Matcher密码验证器,所以这里不验证密码,封装SimpleAuthenticationInfo直接通过。
package cn.git.shiro.realm;
import cn.git.shiro.constant.ShiroJWTConstant;
import cn.git.shiro.entity.User;
import cn.git.shiro.token.JwtToken;
import cn.git.shiro.util.JWTUtil;
import cn.git.shiro.util.RedisUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSONObject;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @description: 自定义shiro权限认证realm
* @program: bank-credit-sy
* @author: lixuchun
* @create: 2024-03-27
*/
@Component
public class CustomRealm extends AuthorizingRealm {
@Autowired
private JWTUtil jwtUtil;
@Autowired
private RedisUtil redisUtil;
/**
* 判断是否支持此token,必须重写此方法,不然Shiro会报错
*
* @param token
* @return
*/
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JwtToken;
}
/**
* 授权
*
* @param principals
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
// 权限信息
return null;
}
/**
* 认证
*
* @param token
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
// 获取jwtToken
String jwtToken = StrUtil.toString(token.getPrincipal());
// 校验jwtToken合法性
if (!jwtUtil.verifyJWTToken(jwtToken) || jwtUtil.ifTokenExpire(jwtToken)) {
throw new RuntimeException("当前token非法!");
}
// 获取用户信息
if (!redisUtil.hHasKey(ShiroJWTConstant.JWT_TOKEN_FLAG, jwtToken)) {
throw new RuntimeException("缓存获取登录用户信息失败!");
}
String userJSONStr = (String) redisUtil.hget(ShiroJWTConstant.JWT_TOKEN_FLAG, jwtToken);
User loginUser = JSONObject.parseObject(userJSONStr, User.class);
// 封装认证信息,在DefaultWebSecurityManager配置中没有指定Matcher密码验证器,所以这里不验证密码,封装SimpleAuthenticationInfo直接通过
return new SimpleAuthenticationInfo(
loginUser,
jwtToken,
this.getName());
}
}
2.4.2 自定义shiro过滤器
主要进行过滤器实现,判定请求是否为登录,是则通过继续登录操作,否则请求必须带有token,然后进行token是否正确验证。验证部分subject.login会直接调用走到customRealm中,进行登录验证,失败则返回调用onAccessDenied方法
package cn.git.shiro.filter;
import cn.git.shiro.constant.ShiroJWTConstant;
import cn.git.shiro.result.Result;
import cn.git.shiro.token.JwtToken;
import cn.git.shiro.util.LogUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.RequestMethod;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
/**
* @description: 自定义shiro-jwt过滤器
* 方法执行顺序 : preHandle > isAccessAllowed > isLoginAttempt > executeLogin > onAccessDenied
* @program: bank-credit-sy
* @author: lixuchun
* @create: 2024-03-27
*/
@Component
public class CustomJWTFilter extends BasicHttpAuthenticationFilter {
/**
* 线程变量
*/
private static final ThreadLocal<Exception> lastException = new ThreadLocal<>();
/**
* 获取上下文路径
*/
@Value("${server.servlet.context-path}")
private String contextPath;
/**
* 判定请求是否带有Token,如果没有则直接放行
* @param request 请求
* @param response 响应
* @param mappedValue
* @return
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
// 判断用户是否是登入
if (this.isLoginAttempt(request, response)) {
return true;
}
try {
// 进行自定义登录
this.executeLogin(request, response);
return true;
} catch (Exception e) {
lastException.set(e);
return false;
}
}
/**
* 登录
* @param request
* @param response
* @return
* @throws Exception
*/
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
// 获取请求token信息
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String token = httpServletRequest.getHeader(ShiroJWTConstant.JWT_TOKEN_FLAG);
JwtToken jwtToken = new JwtToken(token);
// 自定义realm登录
super.getSubject(request, response).login(jwtToken);
return true;
}
/**
* 判断是否是登录请求
* @param request
* @param response
* @return
*/
@Override
protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
// 获取请求地址信息
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
String url = httpServletRequest.getRequestURI();
String token = httpServletRequest.getHeader(ShiroJWTConstant.JWT_TOKEN_FLAG);
// 判断是否是登录请求
if (contextPath.concat(ShiroJWTConstant.LOGIN_PATH).equals(url)) {
return true;
}
// 登录请求token信息为空
return StrUtil.isBlank(token);
}
/**
* 当访问被拒绝时的处理逻辑。
*
* @param request Servlet请求对象,用于获取客户端请求信息。
* @param response Servlet响应对象,用于向客户端发送响应。
* @return 总是返回false,表示访问被拒绝且不会尝试其他拦截器或过滤器。
* @throws Exception 抛出异常,可用于处理访问被拒绝时的各类异常情况。
*/
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
// 设置响应状态码为访问拒绝的自定义状态码
httpServletResponse.setStatus(ShiroJWTConstant.ON_ACCESS_DENIED_NUM);
// 设置响应内容类型为JSON
httpServletResponse.setContentType(ShiroJWTConstant.JSON_CONTENT_TYPE);
PrintWriter out = httpServletResponse.getWriter();
// 获取并输出最后的异常信息
Exception exception = lastException.get();
// 清除最后的异常信息
lastException.remove();
out.println(JSONUtil.toJsonStr(Result.error(exception.getCause().getMessage())));
out.flush();
out.close();
return false;
}
/**
* 跨域请求支持
* @param request
* @param response
* @return
* @throws Exception
*/
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
httpServletResponse.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
httpServletResponse.setHeader("Access-Control-Max-Age", "3600");
httpServletResponse.setHeader("Access-Control-Allow-Headers", "x-requested-with,Authorization,token, content-type"); //这里要加上content-type
httpServletResponse.setHeader("Access-Control-Allow-Credentials", "true");
// 跨域时会首先发送一个option请求,这里给option请求直接返回正常状态
if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
httpServletResponse.setStatus(HttpStatus.OK.value());
return false;
}
return super.preHandle(request, response);
}
}
2.4.3 自定义shiro配置文件
主要实现shiro的基本配置,包括请求路径匹配,权限匹配,关闭session形式会话管理,自定义过滤器配置等,其中还可以配置加密解密方式(matcher),shiro缓存机制(ehCache)等。
package cn.git.shiro.config;
import cn.git.shiro.constant.ShiroJWTConstant;
import cn.git.shiro.filter.CustomJWTFilter;
import cn.git.shiro.realm.CustomRealm;
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.servlet.Filter;
import java.util.HashMap;
import java.util.Map;
/**
* @description: shiro通用配置类
* @program: bank-credit-sy
* @author: lixuchun
* @create: 2024-03-27
*/
@Configuration
public class CommonShiroConfig {
@Autowired
private CustomJWTFilter customJWTFilter;
/**
* 创建并配置DefaultWebSecurityManager实例。
* 该方法不接受任何参数,返回配置好的DefaultWebSecurityManager实例。
* 主要进行以下配置:
* 1. 设置自定义的Realm。
* 2. 配置SubjectDAO以禁用session存储
*
* @return 配置好的DefaultWebSecurityManager实例。
*/
@Bean
public DefaultWebSecurityManager defaultWebSecurityManager(CustomRealm customRealm) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 给安全管理器设置realm
securityManager.setRealm(customRealm);
// 关闭shiro的session(无状态的方式使用shiro)
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
securityManager.setSubjectDAO(subjectDAO);
return securityManager;
}
/**
* 创建并配置 ShiroFilterFactoryBean 实例。
*
* @param defaultWebSecurityManager Shiro 的 Web 安全管理器,用于管理安全逻辑和权限控制。
* @return 配置好的 ShiroFilterFactoryBean 实例,它定义了 Shiro 过滤器链和相关配置。
*/
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager defaultWebSecurityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 设置安全管理器
shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
// 设置登录URL
shiroFilterFactoryBean.setLoginUrl(ShiroJWTConstant.LOGIN_PATH);
// 定义自定义过滤器并映射到名称
Map<String, Filter> filterMap = new HashMap<>();
filterMap.put(ShiroJWTConstant.JWT_FILTER, customJWTFilter);
shiroFilterFactoryBean.setFilters(filterMap);
// 定义过滤链,指定哪些 URL 用哪些过滤器处理
Map<String, String> map = new HashMap<>();
map.put(ShiroJWTConstant.LOGIN_PATH, ShiroJWTConstant.ANON);
map.put(ShiroJWTConstant.ALL_PATH, ShiroJWTConstant.JWT_FILTER);
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
return shiroFilterFactoryBean;
}
}
2.4.4 自定义jwtToken实现
登录时候必须要使用自定义jwtToken,传入token信息,供realm验证使用,具体实现如下
package cn.git.shiro.token;
import lombok.Data;
import org.apache.shiro.authc.AuthenticationToken;
/**
* @description: jwtToken
* @program: bank-credit-sy
* @author: lixuchun
* @create: 2024-03-27
*/
@Data
public class JwtToken implements AuthenticationToken {
private static final long serialVersionUID = 7586393396529236574L;
/**
* token
*/
private String token;
/**
* 构造方法
* @param token
*/
public JwtToken(String token) {
this.token = token;
}
/**
* 获取用户信息 username,从token获取
* @return
*/
@Override
public Object getPrincipal() {
return token;
}
/**
* 获取凭证信息 password,从token获取
* @return
*/
@Override
public Object getCredentials() {
return token;
}
}
2.5 controller以及service实现
2.5.1 LoginController
登录controller,其中登录方法调用完成后,可以调用获取用户信息方法,验证前端携带token验证是否可以正确验证token信息,登出方法则直接处理redis中的缓存信息即可,jwt生成token信息则过期自动失效
package cn.git.shiro.controller;
import cn.git.shiro.entity.User;
import cn.git.shiro.result.Result;
import cn.git.shiro.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
/**
* @description: 登录controller
* @program: bank-credit-sy
* @author: lixuchun
* @create: 2024-03-27
*/
@RestController
@RequestMapping("/shiro")
public class LoginController {
@Autowired
private UserService userService;
/**
* 登录
*
* @param username
* @param password
* @return
*/
@GetMapping("/login")
public Result<String> login(@RequestParam String username, @RequestParam String password) {
// 从数据库中查找用户的信息,信息正确生成token
return userService.login(username, password);
}
/**
* 获取用户信息
*
* @param id
* @return
*/
@GetMapping("/user/{id}")
public Result<User> detail(@PathVariable(value = "id") String id) {
return Result.ok(userService.getUserById(id));
}
/**
* 登出
*
* @param jwtToken
* @return
*/
@GetMapping("/logout")
public Result<String> logout(@RequestHeader(value = ShiroJWTConstant.JWT_TOKEN_FLAG) String jwtToken) {
// 直接redis登出即可,jwtToken过期自动登出
userService.logout(jwtToken);
return Result.ok("登出成功");
}
}
2.5.2 userService以及实现类
service方法
package cn.git.shiro.service;
import cn.git.shiro.entity.User;
import cn.git.shiro.result.Result;
/**
* @description: 用户登录service
* @program: bank-credit-sy
* @author: lixuchun
* @create: 2024-03-27
*/
public interface UserService {
/**
* 用户登录
* @param username 用户名
* @param password 密码
*/
Result<String> login(String username, String password);
/**
* 登出
* @param jwtToken
*/
void logout(String jwtToken);
/**
* 根据id查询用户
* @param id
* @return
*/
User getUserById(String id);
}
service实现方法,方法逻辑很简单,就是登录,查看传入用户密码与数据库存入密码是否比对成功(方便测试,没有使用数据库获取用户,直接写死了,密码加密解密使用hutool工具)
package cn.git.shiro.service.impl;
import cn.git.shiro.constant.ShiroJWTConstant;
import cn.git.shiro.entity.User;
import cn.git.shiro.result.Result;
import cn.git.shiro.service.UserService;
import cn.git.shiro.util.JWTUtil;
import cn.git.shiro.util.PassUtil;
import cn.git.shiro.util.RedisUtil;
import com.alibaba.fastjson.JSONObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
/**
* @description: 用户登录serviceImpl
* @program: bank-credit-sy
* @author: lixuchun
* @create: 2024-03-27
*/
@Service
public class UserServiceImpl implements UserService {
/**
* 模拟数据库密码
*/
private static final String DB_PASSWORD = "$2a$10$a7nOTTarTPXrzEU4DTcHrOSS2NM7LLZpMWcnOYwcsaFqvRznsuPCe";
@Autowired
private PassUtil passUtil;
@Autowired
private JWTUtil jwtUtil;
@Autowired
private RedisUtil redisUtil;
/**
* 用户登录
*
* @param username 用户名
* @param password 密码
*/
@Override
public Result<String> login(String username, String password) {
// 模拟用户登录
User user = new User();
user.setUsername(username);
user.setPassword(password);
user.setRoleId("1");
// 校验加密后密码是否等于数据库查询出密码,DB_PASSWORD = passUtil.getPassHash(password)
if (!passUtil.checkPass(password, DB_PASSWORD)) {
throw new RuntimeException("登录失败!");
}
// 生成jwtToken信息
Map<String, String> customClaimsMap = new HashMap<>();
customClaimsMap.put("username", username);
String jwtToken = jwtUtil.createJWTToken(username, customClaimsMap);
// 将登录信息存储到redis中,1小时生效时间
redisUtil.hset(ShiroJWTConstant.JWT_TOKEN_FLAG,
jwtToken,
JSONObject.toJSONString(user),
ShiroJWTConstant.TIME_HOUR);
return Result.ok(jwtToken);
}
/**
* 登出
*
* @param jwtToken
*/
@Override
public void logout(String jwtToken) {
if (ObjectUtil.isNotNull(redisUtil.hget(ShiroJWTConstant.JWT_TOKEN_FLAG, jwtToken))) {
redisUtil.hdel(ShiroJWTConstant.JWT_TOKEN_FLAG, jwtToken);
} else {
throw new RuntimeException("传入token信息未获取到用户信息,登出失败!");
}
}
/**
* 根据id查询用户
*
* @param id
* @return
*/
@Override
public User getUserById(String id) {
User user = new User();
user.setPassword(DB_PASSWORD);
user.setId(id);
user.setRoleId("1");
user.setUsername("jack");
return user;
}
}
2.6 一些其他实体类以及工具类
2.6.1 RSA加密解密工具类
package cn.git.shiro.util;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.crypto.Cipher;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
/**
* @description: RSA加解密工具类
* @program: bank-credit-sy
* @author: lixuchun
* @create: 2024-01-05 08:58:58
*/
@Slf4j
@Component
public class DBRSAUtil {
/**
* 生成好公钥
*/
public static final String PUB_KEY = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCMZ/Q5CIvZjBTLD4sTwxbWYNNZWd7rImD+sAgwOAf+nH1vWN+Qk63iBGQYHzYK5EhSAFqsqX04lsTiJUH42YRJ9yIXUnvd6Osp2JYOXW8WTGgMtfDJp5WzbYzJRxQ0RmJNcfqMcHPVepgpjs2HNj59aTqNPLdiouRSRlh2E0umGwIDAQAB";
/**
* 生成好私钥
*/
public static final String PRI_KEY = "MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAIxn9DkIi9mMFMsPixPDFtZg01lZ3usiYP6wCDA4B/6cfW9Y35CTreIEZBgfNgrkSFIAWqypfTiWxOIlQfjZhEn3IhdSe93o6ynYlg5dbxZMaAy18MmnlbNtjMlHFDRGYk1x+oxwc9V6mCmOzYc2Pn1pOo08t2Ki5FJGWHYTS6YbAgMBAAECgYEAiQRNNXccmsDz7bGOZEumtrAor/Je8xFKnGCGrR+Q1aw7UHTnPvyO3JiyYUPcBkb+OF+2HPcNhzLCkXoQZltGlznwOwGvHl4qEheVAMwdgijuYQZpsiGQyVyr4C506ydoPjPXbWD+9GGLuakHtIlRP9FAGvwQe/5fkUYsiAJD8mkCQQDop8hxDCrsH9eBQ7PusaGjmW213zmX0O5yAntwuznX3zsQOo+AgeM00ottF4J5BXWCeyF5ZxKi6WoPjSgTw77fAkEAmn6QNwMSEbcWHbuaC3ofjcYhnOl47aQWNr+56G+Wc7vs4xs8PXdd3sVmlepOFSdJLWCxguUBO60Dg6cpnAFMRQJABZLPaHXkKVfx77TRgKxctPCeAjdgx9RHgg+xKVgy4IsGfTMJ8Qgriz5n/KsNgxywXfnZKXFgrupskgbNqPuNfQJAQlOjxnpi/4gCzrED6Xl8onk1ZRA3Ao83mjmlrsx5YyaDBN1kd18PxdwptqLo8tvy5rBkhTWb2erlX1gc3QURoQJABakHaa+GNmKyc7pasJHQ3HMR39k888TQPKEQiE461oc5//Cyqek7u91rCG0HR7O4Hk+ostH9lkxc4bG+HYgWgg==";
/**
* 加密类型RSA
*/
public static final String RSA_TYPE = "RSA";
/**
* RSA
* 算法名称/加密模式/数据填充方式
*/
public static final String RSA_MODE = "RSA/ECB/PKCS1Padding";
/**
* 加密密匙
*/
public static final String DB_LOCK_KEY = "DMZ:XdXt:loan==,";
/**
* RSA密钥长度
*/
private static final Integer NUM_1024 = 1024;
/**
* RSA初始化key pair,初始化公钥私钥方法
* @return KeyPair
*/
public KeyPair getRSAKeyPair() {
try {
// 生成RSA密钥对
KeyPairGenerator keyGen = KeyPairGenerator.getInstance(RSA_TYPE);
// 根据种子生成秘钥对,生成秘钥固定
SecureRandom secureRandom = new SecureRandom(DB_LOCK_KEY.getBytes(StandardCharsets.UTF_8));
keyGen.initialize(NUM_1024, secureRandom);
return keyGen.generateKeyPair();
} catch (Exception e) {
String errorMsg = LogUtil.getStackTraceInfo(e, LogUtil.LENGTH_INT_2000);
log.error(errorMsg);
throw new RuntimeException("RSA初始化keyPairs失败");
}
}
/**
* RSA加密
* @param data 待加密数据
* @param publicKeyStr 公钥
* @return 加密后的数据
*/
public String encryptRSA(String data, String publicKeyStr) {
try {
byte[] keyBytes = Base64.getDecoder().decode(publicKeyStr);
// 初始化公钥key
X509EncodedKeySpec x509KeySpec = new X509EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance(RSA_TYPE);
PublicKey publicKey = keyFactory.generatePublic(x509KeySpec);
// 使用公钥进行加密
Cipher encryptCipher = Cipher.getInstance(RSA_MODE);
encryptCipher.init(Cipher.ENCRYPT_MODE, publicKey);
byte[] encryptedBytes = encryptCipher.doFinal(data.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(encryptedBytes);
} catch (Exception e) {
String errorMsg = LogUtil.getStackTraceInfo(e, LogUtil.LENGTH_INT_2000);
log.error(errorMsg);
throw new RuntimeException("RSA加密失败");
}
}
/**
* 进行RSA解密
* @param content 解密文本
* @param privateKeyStr 私钥字符串
* @return
*/
public String decryptRSA(String content, String privateKeyStr) {
try {
byte[] keyBytes = Base64.getDecoder().decode(privateKeyStr);
// 初始化私钥
PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(keyBytes);
KeyFactory keyFactory = KeyFactory.getInstance(RSA_TYPE);
PrivateKey privateKey = keyFactory.generatePrivate(pkcs8KeySpec);
// 使用私钥进行解密
Cipher decryptCipher = Cipher.getInstance(RSA_MODE);
decryptCipher.init(Cipher.DECRYPT_MODE, privateKey);
byte[] decryptedBytes = decryptCipher.doFinal(Base64.getDecoder().decode(content));
return new String(decryptedBytes, StandardCharsets.UTF_8);
} catch (Exception e) {
String errorMsg = LogUtil.getStackTraceInfo(e, LogUtil.LENGTH_INT_2000);
log.error(errorMsg);
throw new RuntimeException("RSA解密失败");
}
}
}
2.6.2 用户密码加密解密类
使用hutool工具类,简单方便
package cn.git.shiro.util;
import cn.hutool.crypto.digest.BCrypt;
import org.springframework.stereotype.Component;
/**
* @description: hutool密码工具类
* @program: bank-credit-sy
* @author: lixuchun
* @create: 2024-03-27
*/
@Component
public class PassUtil {
/**
* 获取加密密码
*
* @param password
* @return
*/
public String getPassHash(String password) {
return BCrypt.hashpw(password, BCrypt.gensalt());
}
/**
* 验证密码
*
* @param password 前端加密密码
* @param dbPassword 数据库查询用户密码
* @return
*/
public boolean checkPass(String password, String dbPassword) {
return BCrypt.checkpw(password, dbPassword);
}
}
2.6.3 错误日志工具类
方便获取一些错误信息异常信息
package cn.git.shiro.util;
import cn.hutool.core.util.StrUtil;
import java.io.PrintWriter;
import java.io.StringWriter;
/**
* 日志通用方法
*
* @program: bank-credit-sy
* @author: liudong
* @create: 2022-06-15
*/
public class LogUtil {
/**
* 日志长度
*/
public static final Integer LENGTH_INT_0 = 0;
public static final Integer LENGTH_INT_1000 = 1000;
public static final Integer LENGTH_INT_2000 = 2000;
public static final Integer LENGTH_INT_3500 = 3500;
/**
* 打印异常信息转字符串
*
* @param exception 异常
* @return 异常信息
*/
public static String getStackTraceInfo(Exception exception) {
return getStackTraceInfo(exception, LENGTH_INT_1000);
}
/**
* 打印异常信息转字符串
*
* @param exception 异常
* @param errorMessageLength 打印异常信息长度
* @return 异常信息
*/
public static String getStackTraceInfo(Exception exception, Integer errorMessageLength) {
StringWriter stringWriter = new StringWriter();
PrintWriter printWriter = new PrintWriter(stringWriter);
try {
exception.printStackTrace(printWriter);
printWriter.flush();
stringWriter.flush();
String errorMessage = stringWriter.toString();
if (StrUtil.isNotBlank(errorMessage) && errorMessage.length() > LENGTH_INT_3500) {
errorMessage =
errorMessage.substring(LENGTH_INT_0, errorMessageLength);
}
return errorMessage;
} finally {
try {
printWriter.close();
stringWriter.close();
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
}
2.6.4 Result统一响应结果类
前后端分离,通用返回结果
package cn.git.shiro.result;
import cn.git.shiro.enums.ResultEnum;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @description: 响应结果
* @program: bank-credit-sy
* @author: lixuchun
* @create: 2024-01-05 10:18:16
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result<T> {
/**
* 响应结果
*/
private T result;
/**
* 是否成功
* true/false
*/
private boolean issuccess;
/**
* 响应编码
* 0000 成功
*/
private String rtncode;
/**
* 响应消息
* 交易成功/交易失败
*/
private String rtnmessage;
/**
* 支持信息
* 请联系服务商
*/
private String solution;
/**
* 构造函数
*/
private Result(ResultEnum ResultEnum) {
this.issuccess = ResultEnum.isIssuccess();
this.rtncode = ResultEnum.getRtncode();
this.rtnmessage = ResultEnum.getRtnmessage();
this.solution = ResultEnum.getSolution();
}
/**
* 构造函数
*/
private Result(ResultEnum ResultEnum, T data) {
this.result = data;
this.issuccess = ResultEnum.isIssuccess();
this.rtncode = ResultEnum.getRtncode();
this.rtnmessage = ResultEnum.getRtnmessage();
this.solution = ResultEnum.getSolution();
}
/**
* 构造函数
*/
private Result(ResultEnum ResultEnum, String message) {
this.result = null;
this.issuccess = ResultEnum.isIssuccess();
this.rtncode = ResultEnum.getRtncode();
this.rtnmessage = message;
this.solution = ResultEnum.getSolution();
}
/**
* 构造函数
*/
private Result(ResultEnum ResultEnum, String message, String rtncode) {
this.result = null;
this.issuccess = ResultEnum.isIssuccess();
this.rtncode = ResultEnum.getRtncode();
this.rtnmessage = message;
this.solution = ResultEnum.getSolution();
}
/**
* 构造函数
* @param result 响应结果
* @param issuccess 是否成功
* @param rtncode 响应编码
* @param rtnmessage 响应消息
*/
private Result(T result, boolean issuccess, String rtncode, String rtnmessage) {
this.result = result;
this.issuccess = issuccess;
this.rtncode = rtncode;
this.rtnmessage = rtnmessage;
}
/**
* 构造函数
* @param result 响应结果
* @param rtncode 响应编码
* @param rtnmessage 响应消息
*/
private Result(T result, String rtncode, String rtnmessage) {
this.result = result;
this.rtncode = rtncode;
this.rtnmessage = rtnmessage;
}
/**
* 构造函数
* @param rtncode 响应编码
* @param rtnmessage 响应消息
*/
private Result(String rtncode, String rtnmessage) {
this.rtncode = rtncode;
this.rtnmessage = rtnmessage;
this.solution = "请联系服务商!";
}
/**
* 业务请求成功,返回业务代码和描述信息
*/
public static Result<Void> ok() {
return new Result<>(ResultEnum.SUCCESS, null);
}
/**
* 业务请求成功,返回业务代码和描述信息
*/
public static Result<Void> ok(Result Result) {
return Result;
}
/**
* 业务请求成功,返回业务代码和描述信息,数据
*/
public static <T> Result<T> ok(T data) {
return new Result<>(ResultEnum.SUCCESS, data);
}
/**
* 业务请求失败
*/
public static <T> Result<T> error() {
return new Result<>(ResultEnum.NORMAL_ERROR, null);
}
/**
* 业务请求失败,返回错误响应码
*/
public static <T> Result<T> error(ResultEnum ResultEnum) {
return new Result<>(ResultEnum);
}
/**
* 业务请求失败,返回错误响应码
*/
public static <T> Result<T> error(ResultEnum ResultEnum, String message, String rtncode) {
return new Result<>(ResultEnum, message, rtncode);
}
/**
* 业务请求失败
*/
public static <T> Result<T> error(String message) {
return new Result<>(ResultEnum.NORMAL_ERROR, message);
}
/**
* 业务请求失败
*/
public static <T> Result<T> error(String rtncode, String rtnmessage) {
return new Result<>(rtncode, rtnmessage);
}
/**
* 业务请求失败
*/
public static <T> Result<T> error(ResultEnum ResultEnum, String rtnmessage) {
return new Result<>(ResultEnum, rtnmessage);
}
}
2.6.5 响应结果枚举类
package cn.git.shiro.enums;
import lombok.Getter;
/**
* @description: DMZ网关响应enum
* @program: bank-credit-sy
* @author: lixuchun
* @create: 2024-01-05 10:20:51
*/
@Getter
public enum ResultEnum {
/**
* 交易枚举类型
*/
SUCCESS(true, "0000", "交易成功!", "交易成功!"),
NORMAL_ERROR(false, "000X", "代码异常!", "请联系供应商!"),
DECRYPT_ERROR(false, "000X", "解密入参失败!", "请确认请求信息是否正确!"),
REQUEST_METHOD_ERROR(false, "0001", "请求方法必须为post方法!", "请确认请求方式!"),
REQUEST_NO_BODY_ERROR(false, "0002", "请求信息没有请求体!", "请确认请求信息是否正确!"),
SIGN_VALID_FORMAT_ERROR(false, "0003", "验签请求信息补全!", "请确认请验签请求信息是合法!"),
SIGN_VALID_ERROR(false, "0004", "验签失败!", "验签失败!"),
REQUEST_PARAM_VALID_ERROR(false, "0005", "请求参数格式校验失败!", "请求参数格式校验失败!"),
REQUEST_PARAM_TRANSCODE_ERROR(false, "0006", "transcode解析失败!", "请确认请求参数transcode!"),
REQUEST_GET_TRAIN_ERROR(false, "0007", "获取TRAN执行类失败!", "请联系供应商!"),
CUSTOM_TRAIN_ERROR(false, "0008", "自定义异常信息!", "请联系供应商!"),
;
/**
* 是否成功
* true/false
*/
private boolean issuccess;
/**
* 响应编码
* 0000 成功
*/
private String rtncode;
/**
* 响应消息
* 交易成功/交易失败
*/
private String rtnmessage;
/**
* 支持信息
* 请联系服务商
*/
private String solution;
/**
* 构造函数
*
* @param issuccess
* @param rtncode
* @param rtnmessage
* @param solution
*/
ResultEnum(boolean issuccess, String rtncode, String rtnmessage, String solution) {
this.issuccess = issuccess;
this.rtncode = rtncode;
this.rtnmessage = rtnmessage;
this.solution = solution;
}
}
2.6.6 全局异常处理
暂时全局异常处理只接收一个Exception异常,没有细分异常处理信息,后期细分。当前请求业务层controller抛出异常可以接收处理,而jwtFilter中,调用realm验证token抛出异常则无法捕捉,最后通过onAccessDenied,当访问被拒绝时的处理逻辑处理。
package cn.git.shiro.exception;
import cn.git.shiro.result.Result;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.shiro.authz.UnauthorizedException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
/**
* @description: 全局异常处理,现在只标记权限校验失败处理
* @program: bank-credit-sy
* @author: lixuchun
* @create: 2024-03-27
*/
@ControllerAdvice
public class GlobalExceptionHandler {
/**
* UnauthorizedException 权限验证失败处理器
* @param e
* @return
*/
@ResponseBody
@ExceptionHandler(Exception.class)
public Result Exception(Exception e) {
return Result.error(e.getMessage());
}
}
2.6.7 用户实体user类
package cn.git.shiro.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* @description: 用户信息实体
* @program: bank-credit-sy
* @author: lixuchun
* @create: 2024-03-26
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User implements Serializable {
private static final long serialVersionUID = -6930098636698422806L;
/**
* 用户id
*/
private String id;
/**
* 用户名
*/
private String username;
/**
* 密码
*/
private String password;
/**
* 角色id
*/
private String roleId;
}
2.6.8 常量类
package cn.git.shiro.constant;
/**
* @description: shiro常量类
* @program: bank-credit-sy
* @author: lixuchun
* @create: 2024-03-27
*/
public class ShiroJWTConstant {
/**
* JWT token
*/
public static final String JWT_TOKEN_FLAG = "JWT_TOKEN";
/**
* json content type
*/
public static final String JSON_CONTENT_TYPE = "application/json;charset=utf-8";
/**
* 访问拒绝时返回的错误码
*/
public static final Integer ON_ACCESS_DENIED_NUM = 400;
/**
* 登录路径
*/
public static final String LOGIN_PATH = "/shiro/login";
/**
* 所有路径
*/
public static final String ALL_PATH = "/**";
/**
* jwt过滤器
*/
public static final String JWT_FILTER = "jwt_filter";
/**
* 不需要过滤器处理
*/
public static final String ANON = "anon";
/**
* 1小时
*/
public static final Integer TIME_HOUR = 3600;
}
2.6.9 服务启动类
package cn.git.shiro;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* @description: shiro-jwt-demo服务启动类
* @program: bank-credit-sy
* @author: lixuchun
* @create: 2024-03-27
*/
@SpringBootApplication(scanBasePackages = "cn.git")
public class JWTShiroApplication {
public static void main(String[] args) {
SpringApplication.run(JWTShiroApplication.class, args);
}
}
3.启动服务进行测试
浏览器 http://localhost:8099/shiro-server/shiro/login?username=tom&password=Loan2022 进行登录测试,登录成功,result获取token信息
修改密码模拟登录失败
获取token信息,访问用户获取接口,不带token访问如下
带token访问正确返回获取信息如下
进行登出测试
登出后再使用该token进行用户信息查询
4. 不足与改进
- 1.当前生成的token信息在redis中一直存在,正常应该也设置失效时间为1小时,与jwt发布令牌相同
- 2.当用户进行了操作,应该将redis中令牌生效时间进行延长,但是jwt令牌时间无法延长,这时候就需要考虑jwt的会话延长问题了,一般设置为双jwt令牌策略,登录成功返回双jwt令牌,一个为登录成功jwtToken,时效为一个小时,另一个为refresh刷新令牌,时效为1天,将两个令牌传送给前端,如果前端送过来jwtToken,发现失效了,那么校验刷新令牌是否失效,没有的话新建登录成功令牌给到前端,重置失效时间为1小时,并且同步处理redis中jwtToken信息,那么前端便可以再次正确携带新jwtToken进行数据访问。一天之内,只要refreshToken不失效,那么用户可以一直登录,这样便间接处理了jwtToken失效问题。