简单的shiro-jwt-demo实例

简单的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失效问题。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值