Login
网站首页 > 文章中心 > 其它

swagger◆jwt◆shiro◆redis

作者:小编 更新时间:2023-09-17 19:05:13 浏览量:391人看过

swagger◆jwt◆shiro◆redis

swagger◆jwt◆shiro◆redis

第一段:前言

最近在项目中想整合swagger◆jwt◆shiro◆redis过程中遇到诸多问题和困难,现重新写一个demo并记录解决步骤.存在的问题:

shiro默认的拦截跳转都是跳转url页面,而前后端分离后,后端并无权干涉页面跳转.

shiro默认的登录拦截校验机制是使用的session.

参考资料:SpringBoot结合JWT◆Shiro◆Redis实现token无状态登录授权

第二段:需求及相关说明

需求首次通过post请求进行登入;

登录成功后返回token;

服务端负责token生命周期的刷新

用户权限的校验;

Shiro ◆ JWT实现无状态鉴权机制

首先post用户名与密码到login进行登入,如果成功在请求头Header返回一个加密的Authorization,失败的话直接返回未登录,以后访问都带上这个Authorization即可.

鉴权流程主要是要重写shiro的入口过滤器BasicHttpAuthenticationFilter,在此基础上进行拦截、token验证授权等操作

关于AccessToken及RefreshToken概念说明

关于Redis中保存RefreshToken信息(做到JWT的可控性)

登录认证通过后返回AccessToken信息(在AccessToken中保存当前的时间戳和帐号),同时在Redis中设置一条以帐号为Key,Value为当前时间戳(登录时间)的RefreshToken,现在认证时必须AccessToken没失效以及Redis存在所对应的RefreshToken,且RefreshToken时间戳和AccessToken信息中时间戳一致才算认证通过,这样可以做到JWT的可控性,如果重新登录获取了新的AccessToken,旧的AccessToken就认证不了,因为Redis中所存放的的RefreshToken时间戳信息只会和最新的AccessToken信息中携带的时间戳一致,这样每个用户就只能使用最新的AccessToken认证.

Redis的RefreshToken也可以用来判断用户是否在线,如果删除Redis的某个RefreshToken,那这个RefreshToken所对应的AccessToken之后也无法通过认证了,就相当于控制了用户的登录,可以剔除用户

关于根据RefreshToken自动刷新AccessToken

同时前端进行获取替换,下次用新的AccessToken进行访问即可.

第三段:实现步骤



1.8
③③2
1.⑥0
2.9.2
③9.0




    org.springframework.boot
    spring-boot-starter-data-jdbc


    org.springframework.boot
    spring-boot-starter-web


    org.springframework.boot
    spring-boot-starter-test


    org.springframework.boot
    spring-boot-starter-logging



    com.auth0
    java-jwt
    ${jwt.version}




    com.baomidou
    mybatis-plus-boot-starter
    ${mybatis-plus.version}



    org.springframework.boot
    spring-boot-starter-data-redis




    org.projectlombok
    lombok




    org.apache.shiro
    shiro-spring
    ${shiro.version}




    io.springfox
    springfox-swagger2
    ${swagger.version}



    io.springfox
    springfox-swagger-ui
    ${swagger.version}


    mysql
    mysql-connector-java
    runtime




配置文件


server:
  port: 8081
jwt:
 #  AccessToken 过期时间单位分钟
 EXPIRE_TIME: 5
 # RefreshToken 过期时间单位分钟
 REFRESH_EXPIRE_TIME: 30
 # 密钥盐
 TOKEN_SECRET: 50eFa4d9W8ba4d*bb276f^11
swagger:
  enabled: true
spring:
  datasource:
username: root
password: maple1234
url: jdbc:mysql://12⑦0.0.1:3306/db2020?useSSL=falseuseUnicode=truecharacterEncoding=utf-8serverTimezone=GMT+8
driver-class-name: com.mysql.cj.jdbc.Driver
  redis:
host: 12⑦0.0.1
port: 6379
mybatis-plus:
  mapper-locations: classpath:com/maplexl/shiroJwtRedis/mapper/xml/*.xml
logging:
  level:
   com.maplexl: debug



@Configuration
@EnableSwagger2
public class SwaggerConfig {
//是否开启swagger,根据环境来选择
@Value(value = "${swagger.enabled}")
Boolean swaggerEnabled;

@Bean
public Docket api() {
    //全局参数
    Parameter token = new ParameterBuilder().name("token")
            .description("用户登陆令牌")
            .parameterType("header")
            .modelRef(new ModelRef("String"))
            //是否必须
            .required(false)
            .build();
    ArrayList parameters = new ArrayList<>();
    parameters.add(token);
    return new Docket(DocumentationType.SWAGGER_2)
            .globalOperationParameters(parameters)
            .apiInfo(apiInfo())
            .enable(swaggerEnabled)
            .groupName("api")
            .select()
            .apis(RequestHandlerSelectors.basePackage("com.maplexl.shiroJwtRedis.controller"))
            .paths(PathSelectors.any())
            .build();
}


private ApiInfo apiInfo() {
    return new ApiInfoBuilder()
            .title("jwt-shiro-redis")
            .description("api")
            // 作者信息
            .contact(new Contact("枫叶", "https://www.cnblogs.com/junlinsky/", "203051919@qq.com"))
            .version("1.0.0")
            .build();
}
}


主要实现token的签发、验证和数据解析.


@Slf4j
@SuppressWarnings("unused")
public class JwtUtil {

/**
 * token到期时间,毫秒为单位
 */
public static long EXPIRE_TIME;
/**
 * RefreshToken到期时间为,秒为单位
 */
public static long REFRESH_EXPIRE_TIME;
/**
 * 密钥盐
 */
private static String TOKEN_SECRET;

/**
 * 设置token过期时间及密钥盐
 *
 * @param expireTime        客户端token过期时间
 * @param refreshExpireTime 服务器token过期时间
 * @param tokenSecret       token加密使用的盐值
 */
public static void setProperties(long expireTime, long refreshExpireTime, String tokenSecret) {
    JwtUtil.EXPIRE_TIME = expireTime;
    JwtUtil.REFRESH_EXPIRE_TIME = refreshExpireTime;
    JwtUtil.TOKEN_SECRET = tokenSecret;
}

/**
 * 校验token是否正确
 *
 * @param token 密钥
 * @return 是否正确
 */
public static boolean verify(String token) {
    //创建token验证器
    JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(TOKEN_SECRET)).build();
    DecodedJWT decodedjwt = jwtVerifier.verify(token);
    log.info("认证通过");
    log.info("userId: [{}]", decodedjwt.getClaim("userId").asLong());
    log.info("过期时间:      [{}]", decodedjwt.getExpiresAt());
    return true;
}

/**
 * 获得token中的信息
 *
 * @return token中包含的用户id
 */
public static long getUserId(String token) {
    try {
        DecodedJWT jwt = JWT.decode(token);
        return jwt.getClaim("userId").asLong();
    } catch (JWTDecodeException e) {
        return -1L;
    }
}

/**
 * 获取currentTime
 * @param token 密钥
 * @return currentTime
 */
public static Long getCurrentTime(String token){
    try{
        DecodedJWT decodedjwt =JWT.decode(token);
        return decodedjwt .getClaim("currentTime").asLong();
    }catch (JWTCreationException e){
        return null;
    }
}
/**
 * 根据request中的token获取currentTime
 *
 * @param request request
 * @return CurrentTime
 */
public static Long getCurrentTime(HttpServletRequest request) {
    String accessToken = request.getHeader("token");
    Long currentTime = getCurrentTime(accessToken);
    if (currentTime == null) {
        throw new RuntimeException("未获取到currentTime");
    }
    return currentTime;
}

/**
 * 生成签名,5min后过期
 *
 * @param userId 用户名
 * @return 加密的token
 */
public static String sign(Long userId, Long currentTime) {
    String token = null;
    try {
        Date expireAt = new Date(currentTime ◆ EXPIRE_TIME);
        token = JWT.create()
                //存放数据
                .withClaim("userId", userId)
                .withClaim("currentTime", currentTime)
                //过期时间
                .withExpiresAt(expireAt)
                .sign(Algorithm.HMAC256(TOKEN_SECRET));
    } catch (RuntimeException e) {
        e.printStackTrace();
    }
    return token;
}


/**
 * 根据request中的token获取账号
 *
 * @param request request
 * @return 用户的账号
 */
public static Long getUserId(HttpServletRequest request) {
    String accessToken = request.getHeader("token");
    return getUserId(accessToken);
}
}


上面的工具类在使用前需需要设置EXPIRE_TIME、TOKEN_SECRET和REFRESH_EXPIRE_TIME

配置类


@Configuration
public class JwtUtilConfig {
@Value("${jwt.EXPIRE_TIME}")
private Long expireTime;
@Value("${jwt.REFRESH_EXPIRE_TIME}")
private Long refreshExpireTime;
@Value("${jwt.TOKEN_SECRET}")
private String tokenSecret;

@PostConstruct
public void init() {
    JwtUtil.setProperties(expireTime * 1000 * 60, refreshExpireTime * 60, tokenSecret);
}
}


RedisConfig


@Configuration
public class RedisConfig {
@Bean
public RedisTemplate redisTemplate(RedisConnectionFactory factory) {

    RedisTemplate template = new RedisTemplate<>();
    // 配置连接工厂
    template.setConnectionFactory(factory);

    //使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值(默认使用JDK的序列化方式)
    Jackson2JsonRedisSerializer jacksonSeial = new Jackson2JsonRedisSerializer<>(Object.class);

    ObjectMapper om = new ObjectMapper();
    // 指定要序列化的域,field,get和set,以及修饰符范围,ANY是都有包括private和public
    om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
    // 指定序列化输入的类型,类必须是非final修饰的,final修饰的类,比如String,Integer等会跑出异常
    om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,ObjectMapper.DefaultTyping.NON_FINAL);
    jacksonSeial.setObjectMapper(om);

    // 值采用json序列化
    template.setValueSerializer(jacksonSeial);
    //使用StringRedisSerializer来序列化和反序列化redis的key值
    template.setKeySerializer(new StringRedisSerializer());

    // 设置hash key 和value序列化模式
    template.setHashKeySerializer(new StringRedisSerializer());
    template.setHashValueSerializer(jacksonSeial);
    template.afterPropertiesSet();

    //设置RedisUtil工具类
    RedisUtil.setRedisTemplate(template);
    return template;
}

/**
 * 对hash类型的数据操作
 */
@Bean
public HashOperations hashOperations(RedisTemplate redisTemplate) {
    return redisTemplate.opsForHash();
}

/**
 * 对redis字符串类型数据操作
 */
@Bean
public ValueOperations valueOperations(RedisTemplate redisTemplate) {
    return redisTemplate.opsForValue();
}

/**
 * 对链表类型的数据操作
 */
@Bean
public ListOperations listOperations(RedisTemplate redisTemplate) {
    return redisTemplate.opsForList();
}

/**
 * 对无序集合类型的数据操作
 */
@Bean
public SetOperations setOperations(RedisTemplate redisTemplate) {
    return redisTemplate.opsForSet();
}

/**
 * 对有序集合类型的数据操作
 */
@Bean
public ZSetOperations zSetOperations(RedisTemplate redisTemplate) {
    return redisTemplate.opsForZSet();
}

/**
 * 设置默认缓存管理器
 * @param factory 链接工厂
 */
@Primary
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory factory){
    return RedisCacheManager.create(factory);
}
}



RedisUtil


@SuppressWarnings("unused")
public final class RedisUtil {

private static RedisTemplate redisTemplate;


public static void setRedisTemplate(RedisTemplate redisTemplate) {
    RedisUtil.redisTemplate = redisTemplate;
}

/**
 * 指定缓存失效时间
 * @param key  键
 * @param time 时间(秒)
 */
public static void expire(String key, long time) {
    try {
        if (time > 0) {
            redisTemplate.expire(key, time, TimeUnit.SECONDS);
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}

/**
 * 根据key 获取过期时间
 * @param key 键 不能为null
 * @return 时间(秒) 返回0代表为永久有效
 */
public static Long getExpire(String key) {
    return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}

/**
 * 判断key是否存在
 * @param key 键
 * @return true 存在 false不存在
 */
public static Boolean hasKey(String key) {
    try {
        return redisTemplate.hasKey(key);
    } catch (Exception e) {
        e.printStackTrace();
        return false;
    }
}

public static boolean hasKey(long key) {
   return hasKey(Long.toString(key));
}

/**
 * 删除缓存
 * @param key 可以传一个值 或多个
 */
@SuppressWarnings("all")
public static void del(String... key) {
    if (key != null  key.length > 0) {
        if (key.length == 1) {
            redisTemplate.delete(key[0]);
        } else {
            redisTemplate.delete(CollectionUtils.arrayToList(key));
        }
    }
}

// ============================String=============================

/**
 * 普通缓存获取
 * @param key 键
 * @return 值
 */
public static Object get(String key) {
    return key == null ? null : redisTemplate.opsForValue().get(key);
}

public static Object get(long key) {
    return get(Long.toString(key));
}

/**
 * 普通缓存放入
 * @param key   键
 * @param value 值
 */

public static void set(String key, Object value) {
    try {
        redisTemplate.opsForValue().set(key, value);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

/**
 * 普通缓存放入并设置时间
 * @param key   键
 * @param value 值
 * @param time  时间(秒) time要大于0 如果time小于等于0 将设置无限期
 */

public static void set(String key, Object value, long time) {
    try {
        if (time > 0) {
            redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
        } else {
            set(key, value);
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}

/**
 * 普通缓存放入并设置时间
 * @param key   键
 * @param value 值
 * @param time  时间(秒) time要大于0 如果time小于等于0 将设置无限期
 */
public static void set(long key, Object value, long time) {
    set(Long.toString(key),value,time);
}

/**
 * 递增
 * @param key   键
 * @param delta 要增加几(大于0)
 */
public static Long incr(String key, long delta) {
    if (delta < 0) {
        throw new RuntimeException("递增因子必须大于0");
    }
    return redisTemplate.opsForValue().increment(key, delta);
}

/**
 * 递减
 * @param key   键
 * @param delta 要减少几(小于0)
 */
public static Long decr(String key, long delta) {
    if (delta < 0) {
        throw new RuntimeException("递减因子必须大于0");
    }
    return redisTemplate.opsForValue().increment(key, -delta);
}

// ================================Map=================================
/**
 * HashGet
 * @param key  键 不能为null
 * @param item 项 不能为null
 */
public static Object hget(String key, String item) {
    return redisTemplate.opsForHash().get(key, item);
}

/**
 * 获取hashKey对应的所有键值
 * @param key 键
 * @return 对应的多个键值
 */
public static Map hmget(String key) {
    return redisTemplate.opsForHash().entries(key);
}

/**
 * HashSet
 * @param key 键
 * @param map 对应多个键值
 */
public static boolean hmset(String key, Map 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 static boolean hmset(String key, Map 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 static 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 static 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 static void hdel(String key, Object... item) {
    redisTemplate.opsForHash().delete(key, item);
}

/**
 * 判断hash表中是否有该项的值
 *
 * @param key  键 不能为null
 * @param item 项 不能为null
 * @return true 存在 false不存在
 */
public static boolean hHasKey(String key, String item) {
    return redisTemplate.opsForHash().hasKey(key, item);
}

/**
 * hash递增 如果不存在,就会创建一个 并把新增后的值返回
 *
 * @param key  键
 * @param item 项
 * @param by   要增加几(大于0)
 */
public static double hincr(String key, String item, double by) {
    return redisTemplate.opsForHash().increment(key, item, by);
}

/**
 * hash递减
 *
 * @param key  键
 * @param item 项
 * @param by   要减少记(小于0)
 */
public static double hdecr(String key, String item, double by) {
    return redisTemplate.opsForHash().increment(key, item, -by);
}

// ============================set=============================
/**
 * 根据key获取Set中的所有值
 * @param key 键
 */
public static Set 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 static 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 static Long sSet(String key, Object... values) {
    try {
        return redisTemplate.opsForSet().add(key, values);
    } catch (Exception e) {
        e.printStackTrace();
    }
    return 0L;
}

/**
 * 将set数据放入缓存
 *
 * @param key    键
 * @param time   时间(秒)
 * @param values 值 可以是多个
 * @return 成功个数
 */
public static 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 0L;
    }
}

/**
 * 获取set缓存的长度
 *
 * @param key 键
 */
public static Long sGetSetSize(String key) {
    try {
        return redisTemplate.opsForSet().size(key);
    } catch (Exception e) {
        e.printStackTrace();
        return 0L;
    }
}

/**
 * 移除值为value的
 *
 * @param key    键
 * @param values 值 可以是多个
 * @return 移除的个数
 */

public static Long setRemove(String key, Object... values) {
    try {
        return redisTemplate.opsForSet().remove(key, values);
    } catch (Exception e) {
        e.printStackTrace();
        return 0L;
    }
}
// ===============================list=================================

/**
 * 获取list缓存的内容
 *
 * @param key   键
 * @param start 开始
 * @param end   结束 0 到 -1代表所有值
 */
public static List 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 键
 */
public static Long lGetListSize(String key) {
    try {
        return redisTemplate.opsForList().size(key);
    } catch (Exception e) {
        e.printStackTrace();
        return 0L;
    }
}

/**
 * 通过索引 获取list中的值
 *
 * @param key   键
 * @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
 */
public static 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 值
 */
public static 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  时间(秒)
 */
public static 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 值
 */
public static boolean lSet(String key, List value) {
    try {
        redisTemplate.opsForList().rightPushAll(key, value);
        return true;
    } catch (Exception e) {
        e.printStackTrace();
        return false;
    }
}

/**
 * 将list放入缓存
 *
 * @param key   键
 * @param value 值
 * @param time  时间(秒)
 */
public static boolean lSet(String key, List 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 值
 */

public static 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 static Long lRemove(String key, long count, Object value) {
    try {
        return redisTemplate.opsForList().remove(key, count, value);
    } catch (Exception e) {
        e.printStackTrace();
        return 0L;
    }

}
}



判断请求接口是否需要进行登录认证授权,如果需要则该请求就必须在Header中添加token字段存AccessToken,无需授权则直接访问.

需要授权的接口就调用getSubject(request, response).login(token),将AccessToken提交给shiro中的CustomRealm进行认证.

AccessToken刷新:判断RefreshToken是否过期,未过期就返回新的AccessToken及RefreshToken并让请求继续正常访问.


@Slf4j
public class JwtFilter extends BasicHttpAuthenticationFilter {
/**
 * 判断是否允许通过
 *
 */
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
    log.info("isAccessAllowed方法");
    try{
        return executeLogin(request,response);
    }catch (Exception e){
        log.info("错误"◆e);
        responseError(response,"shiro fail");
        return false;
    }
}

/**
 * 是否进行登录请求
 *
 */
@Override
protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
    log.info("isLoginAttempt方法");
    String token=((HttpServletRequest)request).getHeader("token");
    return token != null;
}

/**
 * 创建shiro token
 *
 */
@Override
protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) {
    log.info("createToken方法");
    String jwtToken = ((HttpServletRequest)request).getHeader("token");
    if(jwtToken!=null) {
        return new JwtToken(jwtToken);
    }
    return null;
}

/**
 * isAccessAllowed为false时调用,验证失败
 */
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) {
    log.info("onAccessDenied");
    this.sendChallenge(request,response);
    responseError(response,"token verify fail");
    return false;
}



/**
 * shiro验证成功调用
 */
@Override
protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request, ServletResponse response) {
    log.info("onLoginSuccess:");
    String jwttoken= (String) token.getPrincipal();
    if (jwttoken!=null){
        try{
            if(JwtUtil.verify(jwttoken)){
                //判断Redis是否存在所对应的RefreshToken
                long userId = JwtUtil.getUserId(jwttoken);
                Long currentTime=JwtUtil.getCurrentTime(jwttoken);
                if (RedisUtil.hasKey(userId)) {
                    Long currentTimeMillisRedis = (Long) RedisUtil.get(userId);
                    return currentTimeMillisRedis.equals(currentTime);
                }
            }
            return false;
        }catch (Exception e){
            log.info("token验证:"◆e.getClass());
            if (e instanceof TokenExpiredException){
                log.info("TokenExpiredException");
                return refreshToken(request, response);
            }
        }
    }
    return true;
}



/**
 * 拦截器的前置方法,此处进行跨域处理
 */
@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","GET,POST,OPTIONS,PUT,DELETE");
    httpServletResponse.setHeader("Access-Control-Allow-Headers",httpServletRequest.getHeader("Access-Control-Resquest-Headers"));
    if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())){
        httpServletResponse.setStatus(HttpStatus.OK.value());
    }

    //如果不带token,不去验证shiro
    if (!isLoginAttempt(request,response)){
        responseError(httpServletResponse,"no token");
        return false;
    }
    return super.preHandle(request,response);
}


/**
 * 刷新AccessToken,进行判断RefreshToken是否过期,未过期就返回新的AccessToken且继续正常访问
 */
private boolean refreshToken(ServletRequest request, ServletResponse response) {
    String token = ((HttpServletRequest)request).getHeader("token");
    long userId = JwtUtil.getUserId(token);
    Long currentTime=JwtUtil.getCurrentTime(token);
    // 判断Redis中RefreshToken是否存在
    if (RedisUtil.hasKey(userId)) {
        // Redis中RefreshToken还存在,获取RefreshToken的时间戳
        Long currentTimeMillisRedis = (Long) RedisUtil.get(userId);
        // 获取当前AccessToken中的时间戳,与RefreshToken的时间戳对比,如果当前时间戳一致,进行AccessToken刷新
        if (currentTimeMillisRedis.equals(currentTime)) {
            // 获取当前最新时间戳
            Long currentTimeMillis =System.currentTimeMillis();
            RedisUtil.set(userId, currentTimeMillis,JwtUtil.REFRESH_EXPIRE_TIME);
            // 刷新AccessToken,设置时间戳为当前最新时间戳
            token = JwtUtil.sign(userId, currentTimeMillis);
            HttpServletResponse httpServletResponse = (HttpServletResponse) response;
            httpServletResponse.setHeader("Authorization", token);
            httpServletResponse.setHeader("Access-Control-Expose-Headers", "Authorization");
            return true;
        }
    }
    return false;
}

private void responseError(ServletResponse response, String msg){

    HttpServletResponse httpResponse = (HttpServletResponse) response;
    httpResponse.setStatus(401);
    httpResponse.setCharacterEncoding("UTF-8");
    httpResponse.setContentType("application/json;charset=UTF-8");
    try {
        String rj = new ObjectMapper().writeValueAsString(new Result<>(401,msg));
        httpResponse.getWriter().append(rj);
    } catch (IOException e) {
        e.printStackTrace();
    }
}
}


JwtFilter需要用到的几个类说明

Result

JwtToken:shiro中Subje.login接收参数为AuthenticationToken,所以我们需要对我们的token进行封装.

这里解析一下该类的执行流程:首先需要授权的请求经过preHandle进行跨域处理后进入isAccessAllowed方法,isAccessAllowed方法直接调用BasicHttpAuthenticationFilter类的父类AuthenticatingFilter中executeLogin方法,executeLogin方法源码如下:


protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
    AuthenticationToken token = this.createToken(request, response);
    if (token == null) {
        String msg = "createToken method implementation returned null. A valid non-null AuthenticationToken must be created in order to execute a login attempt.";
        throw new IllegalStateException(msg);
    } else {
        try {
            Subject subject = this.getSubject(request, response);
            subject.login(token);
            return this.onLoginSuccess(token, subject, request, response);
        } catch (AuthenticationException var5) {
            return this.onLoginFailure(token, var5, request, response);
        }
    }
}


该方法会先调用createToken方法创建token,然后调用this.getSubject(request, response)进行shiro授权但subject.login参数类型为AuthenticationToken和我们的token不一致,所以呢我们需要重写createToken方法,并创建一个实现了AuthenticationToken接口的JwtToken包装类

JwtToken


public class JwtToken implements AuthenticationToken {
private String token;

public JwtToken(String token) {
    this.token = token;
}

@Override
public Object getPrincipal() {
    return token;
}

@Override
public Object getCredentials() {
    return token;
}
}


executeLogin方法后,授权成功会进入onLoginSuccess方法,该方法进行token的检验,token的检验失败进入onAccessDenied.


@Component
@Slf4j
public class MyReailm extends AuthorizingRealm {
@Resource
UserMapper userMapper;

@Override
public boolean supports(AuthenticationToken token) {
    return token instanceof JwtToken;
}

/**
 * 用户授权
 */
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
    log.info("用户授权");
    long userId = JwtUtil.getUserId(principalCollection.toString());
    SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
    User user = userMapper.selectById(userId);
    Integer authority = user.getAuthority();
    Set role = new HashSet<>();
    /*
     * 权限说明:2查看权限,3查看和编辑权限,
     * 6查看和授权权限,7查看、编辑、授权权限
     */
    switch (authority) {
        case 2:
            role.add("view");
            break;
        case 3:
            role.add("view");
            role.add("edit");
            break;
        case 6:
            role.add("view");
            role.add("authorization");
            break;
        case 7:
            role.add("view");
            role.add("edit");
            role.add("authorization");
            break;
        default:
            role.add("tourist");
    }
    //设置角色集合
    //设置权限方式与设置角色类似使用info.setStringPermissions();
    info.setRoles(role);
    return info;
}

/**
 * 用户身份认证
 */
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
    log.info("身份认证");
    String token = (String) authenticationToken.getCredentials();
    long userId = JwtUtil.getUserId(token);
    log.info(userId◆"");
    User user = userMapper.selectById(userId);
    if (userId <= 0 || user == null) {
        throw new AuthenticationException("认证失败!");
    }
    return new SimpleAuthenticationInfo(token, token, "MyRealm");
}
}


Shiro配置ShiroConfig


import com.maplexl.shiroJwtRedis.bean.MyReailm;
import com.maplexl.shiroJwtRedis.filter.JwtFilter;
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;

import javax.servlet.Filter;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * shiro 配置类
 */
@Configuration
public class ShiroConfig {
@Bean("shiroFilter")
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
    ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
    shiroFilterFactoryBean.setSecurityManager(securityManager);
    // 过滤器
    Map filterChainDefinitionMap = new LinkedHashMap<>();
    // 配置不会被拦截的链接 顺序判断
    // 登录接口排除
    filterChainDefinitionMap.put("/login", "anon");
    // 注册接口排除
    filterChainDefinitionMap.put("/register", "anon");
    //登出接口排除
    filterChainDefinitionMap.put("/logout", "anon");
    //欢迎页排除
    filterChainDefinitionMap.put("/", "anon");
    filterChainDefinitionMap.put("/**/*.js", "anon");
    filterChainDefinitionMap.put("/**/*.css", "anon");
    filterChainDefinitionMap.put("/**/*.html", "anon");
    filterChainDefinitionMap.put("/**/*.jpg", "anon");
    filterChainDefinitionMap.put("/**/*.png", "anon");
    filterChainDefinitionMap.put("/**/*.ico", "anon");
    //swagger资源排除
    filterChainDefinitionMap.put("/swagger-ui.html/**","anon");
    filterChainDefinitionMap.put("/swagger-resources/**","anon");
    filterChainDefinitionMap.put("/v2/**","anon");

    // 添加自己的过滤器并且取名为jwt
    Map filterMap = new HashMap<>(1);
    filterMap.put("jwt", new JwtFilter());
    shiroFilterFactoryBean.setFilters(filterMap);
    // 过滤链定义,从上向下顺序执行,一般将放在最为下边
    filterChainDefinitionMap.put("/**", "jwt");

    //未授权界面返回JSON
    shiroFilterFactoryBean.setUnauthorizedUrl("/sys/common/403");
    shiroFilterFactoryBean.setLoginUrl("/sys/common/403");
    shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
    return shiroFilterFactoryBean;
}

@Bean("securityManager")
public DefaultWebSecurityManager securityManager(MyReailm myRealm) {
    DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
    securityManager.setRealm(myRealm);

    /*
     * 关闭shiro自带的session,详情见文档
     * http://shiro.apache.org/session-management.html#SessionManagement-
     * StatelessApplications(Sessionless)
     */
    DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
    DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
    defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
    subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
    securityManager.setSubjectDAO(subjectDAO);

    return securityManager;
}

/**
 * 下面的代码是添加注解支持
 */
@Bean
@DependsOn("lifecycleBeanPostProcessor")
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
    DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
    defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
    return defaultAdvisorAutoProxyCreator;
}

@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
    return new LifecycleBeanPostProcessor();
}

@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
    AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
    advisor.setSecurityManager(securityManager);
    return advisor;
}
}



@ControllerAdvice
@ResponseBody
@SuppressWarnings("unused")
public class GlobalExceptionHandler {


/**
 * 捕捉所有Shiro异常
 */
@ExceptionHandler(ShiroException.class)
public Result handle401(ShiroException e) {
    return new Result<>(401, "无权访问(Unauthorized):" ◆ e.getMessage());
}

/**
 * 单独捕捉Shiro(UnauthorizedException)异常 该异常为访问有权限管控的请求而该用户没有所需权限所抛出的异常
 */
@ExceptionHandler(UnauthorizedException.class)
public Result handle401(UnauthorizedException e) {
    return new Result<>(401, "无权访问(Unauthorized):当前Subject没有此请求所需权限(" ◆ e.getMessage() ◆ ")");
}

/**
 * 单独捕捉Shiro(UnauthenticatedException)异常
 * 该异常为以游客身份访问有权限管控的请求无法对匿名主体进行授权,而授权失败所抛出的异常
 */
@ExceptionHandler(UnauthenticatedException.class)
public Result handle401(UnauthenticatedException e) {
    e.printStackTrace();
    return new Result<>(401, "无权访问(Unauthorized):当前Subject是匿名Subject,请先登录(This subject is anonymous.)");
}

/**
 * 捕捉校验异常(BindException)
 */
@ExceptionHandler(BindException.class)
public Result validException(BindException e) {
    List fieldErrors = e.getBindingResult().getFieldErrors();
    Map error = this.getValidError(fieldErrors);
    return new Result<>(400, error.get("errorMsg").toString(), error.get("errorList"));
}


/**
 * 捕捉404异常
 */
@ExceptionHandler(NoHandlerFoundException.class)
public Result handle(NoHandlerFoundException e) {
    return new Result<>(404, e.getMessage());
}

/**
 * 捕捉其他所有异常
 */
@ExceptionHandler(Exception.class)
public Result globalException(HttpServletRequest request, Throwable ex) {
    return new Result<>(500, ex.toString() ◆ ": " ◆ ex.getMessage());
}


/**
 * 获取状态码
 */
private HttpStatus getStatus(HttpServletRequest request) {
    Integer statusCode = (Integer) request.getAttribute("javax.servlet.error.status_code");
    if (statusCode == null) {
        return HttpStatus.INTERNAL_SERVER_ERROR;
    }
    return HttpStatus.valueOf(statusCode);
}

/**
 * 获取校验错误信息
 */
private Map getValidError(List fieldErrors) {
    Map map = new HashMap<>(16);
    List errorList = new ArrayList<>();
    StringBuffer errorMsg = new StringBuffer("校验异常(ValidException):");
    for (FieldError error : fieldErrors) {
        errorList.add(error.getField() ◆ "-" ◆ error.getDefaultMessage());
        errorMsg.append(error.getField()).append("-").append(error.getDefaultMessage()).append(".");
    }
    map.put("errorList", errorList);
    map.put("errorMsg", errorMsg);
    return map;
}
}




@RestController
@Slf4j
public class LoginController {
@Resource
UserService userService;

/**
 * 登录接口
 * @param user 用户
 * @param response 响应
 * @return 登录成功则返回token
 */
@ApiOperation("登录接口")
@PostMapping("/login")
public Result login(@RequestBody User user, HttpServletResponse response) {
    String userName = user.getUserName();
    String password = user.getPassword();
    if (StringUtils.isEmpty(userName) || StringUtils.isEmpty(password)) {
        return new Result<>(CommonEnum.INVALID_CHARACTER);
    }
    //查询是否有此用户
    QueryWrapper wrapper = new QueryWrapper<>();
    wrapper.eq("user_name", userName);
    User dbUser = userService.getOne(wrapper);
    if (dbUser == null) {
        return new Result<>(CommonEnum.NO_SUCH_USER);
    }
    //校验密码
    password = new SimpleHash("MD5", password, dbUser.getSalt(), 32).toString();
    if (password.equals(dbUser.getPassword())) {
        //密码正确
        long timeMillis = System.currentTimeMillis();
        String token = JwtUtil.sign(dbUser.getUserId(), timeMillis);
        dbUser.setPassword(null);
        //token放入redis
        RedisUtil.set(dbUser.getUserId(), timeMillis, JwtUtil.REFRESH_EXPIRE_TIME);
        response.setHeader("Authorization", token);
        response.setHeader("Access-Control-Expose-Headers", "Authorization");
        return new Result<>(CommonEnum.SUCCESS, dbUser);
    }
    return new Result<>(CommonEnum.PASSWORD_ERROR);
}

/**
 * 添加用户,即注册
 *
 * @param user 用户信息
 * @return 是否保存成功
 */
@ApiOperation("注册接口")
@PostMapping("/register")
public Result add(@RequestBody User user) {
    //验证用户名是否重复
    QueryWrapper wrapper = new QueryWrapper<>();
    wrapper.eq("user_name",user.getUserName());
    User dbUser = userService.getOne(wrapper);
    if (dbUser!=null){
        return new Result<>(CommonEnum.USER_EXIST);
    }
    //加密用户的密码
    String salt = UUID.randomUUID().toString().substring(0, 24);
    String password = new SimpleHash("MD5", user.getPassword(), salt, 32).toString();
    user.setSalt(salt);
    user.setPassword(password);
    boolean save = userService.save(user);
    if (save) {
        return new Result<>(CommonEnum.SUCCESS, true);
    }
    return new Result<>(CommonEnum.INVALID_INSERT, false);
}

/**
 * 登出
 * @return 是否登出
 */
@GetMapping("/logout")
public Result logout(HttpServletRequest request){
    /*
     * 清除redis中的RefreshToken即可
     */
    Long userId = JwtUtil.getUserId(request);
    RedisUtil.del(Long.toString(userId));
    return new Result<>(CommonEnum.SUCCESS);
}
}


UserController用于对用户CRUD


@RestController
public class UserController {
@Resource
UserService userService;

/**
 * 根据id删除用户
 *
 * @param userId 用户id
 * @return 是否删除成功
 */
@RequiresRoles("edit")
@DeleteMapping("/user/{id}")
public Result delete(@PathVariable("id") Long userId) {
    boolean del = userService.removeById(userId);
    if (del) {
        return new Result<>(CommonEnum.SUCCESS, true);
    }
    return new Result<>(CommonEnum.INVALID_DELETE, false);
}

/**
 * 修改用户信息
 *
 * @param user 需修改的信息
 * @return 是否成功
 */
@RequiresRoles("edit")
@PutMapping("/user")
public Result update(@RequestBody User user) {
    boolean up = userService.updateById(user);
    if (up) {
        return new Result<>(CommonEnum.SUCCESS, true);
    }
    return new Result<>(CommonEnum.INVALID_UPDATE, false);
}

/**
 * 根据用户id查询用户
 *
 * @param userId 用户id
 * @return 用户信息
 */
@RequiresRoles("view")
@GetMapping("/user/{id}")
public Result getById(@PathVariable("id") Long userId) {
    User user = userService.getById(userId);
    if (user != null) {
        return new Result<>(CommonEnum.SUCCESS, user);
    }
    return new Result<>(CommonEnum.NO_SUCH_USER);
}

/**
 * 获取用户列表
 *
 * @return 用户列表
 */
@RequiresRoles("view")
@GetMapping("/user/list")
public Result> getList() {
    List list = userService.list();
    return new Result<>(CommonEnum.SUCCESS, list);
}
}


swagger◆jwt◆shiro◆redis

有token(view身份)

swagger◆jwt◆shiro◆redis

删除操作

swagger◆jwt◆shiro◆redis

swagger◆jwt◆shiro◆redis

swagger◆jwt◆shiro◆redis

可以看到,请求是成功的,并且在响应头里返回了新的token

第四段:遗留问题解决

解决方案如下

配置jackson将Long序列化成字符串


@JsonComponent
public class JsonSerializerManage {

@Bean
public ObjectMapper jacksonObjectMapper(Jackson2ObjectMapperBuilder builder) {
    ObjectMapper objectMapper = builder.createXmlMapper(false).build();
    objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
    SimpleModule module = new SimpleModule();
    module.addSerializer(Long.class, ToStringSerializer.instance);
    module.addSerializer(Long.TYPE, ToStringSerializer.instance);
    objectMapper.registerModule(module);
    return objectMapper;
}
}


最后贴上项目目录结构和git地址

swagger◆jwt◆shiro◆redis

以上就是土嘎嘎小编为大家整理的swagger◆jwt◆shiro◆redis相关主题介绍,如果您觉得小编更新的文章只要能对粉丝们有用,就是我们最大的鼓励和动力,不要忘记讲本站分享给您身边的朋友哦!!

版权声明:倡导尊重与保护知识产权。未经许可,任何人不得复制、转载、或以其他方式使用本站《原创》内容,违者将追究其法律责任。本站文章内容,部分图片来源于网络,如有侵权,请联系我们修改或者删除处理。

编辑推荐

热门文章