springboot+shiro+redis+jwt实现多端登录:PC端和移动端同时在线(不同终端可同时在线)

前言

之前写了篇 springboot+shiro+redis多端登录:单点登录+移动端和PC端同时在线 的文章,但是token用的不是 jwt 而是 sessionID,虽然已经实现了区分pc端和移动端,但是还是有些问题存在的,比如:自定义的Session管理器中,生成的sessionid无法区分不同终端;还有就是登录用的是 subject.login(token) shiro帮我们自动登录,要实现的是移动端需要保持长期登录;

关于移动端保持长期登录,我想的是,另外建一张存储用户信息和token的表,登录成功时,将用户id或用户名和生成的token存入到数据库表中,在拦截器中,判断请求是否来自移动端,来自移动端如果token过期的话,根据token去数据库中查询,如果有数据,则自动重新登录,将新的token响应给前端,无数据则提示用户登录过期重新登录。自动重新登录这一步操作用户是感觉不到的,他以为自己一直在登录状态,实际上是我们静默帮他刷新了登录状态。如果使用 subject.login(token) 来登录的话,需要用户名和密码,但是我的密码是加密的,无法解密,所以就算知道用户id去用户表查询用户密码也没用,也不可能将明文密码放到token表里面吧。所以自动登录这一步怎么想都感觉不合理,目前的话,也没有好的解决方法。

最后还是决定使用jwt来生成token,这样token的可操作性更大一些。

正文

一、pom.xml和数据库表

1、pom.xml

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter</artifactId>
</dependency>

<!-- AOP依赖,一定要加,否则权限拦截验证不生效 -->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-test</artifactId>
	<scope>test</scope>
</dependency>

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
	<groupId>org.projectlombok</groupId>
	<artifactId>lombok</artifactId>
	<optional>true</optional>
</dependency>

<!-- mysql 驱动 -->
<dependency>
	<groupId>mysql</groupId>
	<artifactId>mysql-connector-java</artifactId>
	<version>8.0.22</version>
	<scope>runtime</scope>
</dependency>

<!-- mybatis_plus -->
<dependency>
	<groupId>com.baomidou</groupId>
	<artifactId>mybatis-plus-boot-starter</artifactId>
	<version>3.1.2</version>
</dependency>

<!-- json -->
<dependency>
	<groupId>net.sf.json-lib</groupId>
	<artifactId>json-lib</artifactId>
	<version>2.4</version>
	<classifier>jdk15</classifier>	<!-- 就是这句 -->
</dependency>
<dependency>
	<groupId>com.alibaba</groupId>
	<artifactId>fastjson</artifactId>
	<version>1.2.7</version>
</dependency>

<!--	redis	-->
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<!-- Shiro 核心依赖 -->
<dependency>
	<groupId>org.apache.shiro</groupId>
	<artifactId>shiro-spring</artifactId>
	<version>1.4.0</version>
</dependency>

<!-- jwt token -->
<dependency>
	<groupId>com.auth0</groupId>
	<artifactId>java-jwt</artifactId>
	<version>3.2.0</version>
</dependency>

<dependency>
	<groupId>org.apache.commons</groupId>
	<artifactId>commons-pool2</artifactId>
	<version>2.0</version>
</dependency>

<!-- StringUtilS工具 -->
<dependency>
	<groupId>org.apache.commons</groupId>
	<artifactId>commons-lang3</artifactId>
	<version>3.5</version>
</dependency>

2、数据库

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for sys_dept
-- ----------------------------
DROP TABLE IF EXISTS `sys_dept`;
CREATE TABLE `sys_dept`  (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '部门id',
  `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
  `create_by` bigint NULL DEFAULT NULL COMMENT '创建用户Id',
  `update_time` datetime NULL DEFAULT NULL COMMENT '修改时间',
  `update_by` bigint NULL DEFAULT NULL COMMENT '修改用户Id',
  `parent_id` bigint NULL DEFAULT 0 COMMENT '父部门id',
  `ancestors` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '祖级列表',
  `dept_name` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '部门名称',
  `dept_type` tinyint(1) NULL DEFAULT 1 COMMENT '类型 1 公司 2 部门',
  `status` tinyint(1) NULL DEFAULT 0 COMMENT '部门状态(0正常 1停用)',
  `sort` int NULL DEFAULT 0 COMMENT '显示顺序',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 216 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '部门表' ROW_FORMAT = DYNAMIC;

-- ----------------------------
-- Table structure for sys_dict
-- ----------------------------
DROP TABLE IF EXISTS `sys_dict`;
CREATE TABLE `sys_dict`  (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号',
  `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
  `create_by` bigint NULL DEFAULT NULL COMMENT '创建用户Id',
  `update_time` datetime NULL DEFAULT NULL COMMENT '修改时间',
  `update_by` bigint NULL DEFAULT NULL COMMENT '修改用户Id',
  `parent_id` bigint NULL DEFAULT NULL COMMENT '父级id',
  `dict_code` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '字典代码',
  `dict_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '字典名称',
  `dict_value` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '字典值',
  `sort` int NULL DEFAULT NULL COMMENT '排序',
  `remark` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '备注',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = COMPACT;

-- ----------------------------
-- Table structure for sys_log
-- ----------------------------
DROP TABLE IF EXISTS `sys_log`;
CREATE TABLE `sys_log`  (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '日志编号',
  `log_time` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '操作日期',
  `log_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '操作账号',
  `log_method` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '操作',
  `log_ip` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '主机地址',
  `log_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '请求URL',
  `status` tinyint(1) NULL DEFAULT 0 COMMENT '操作状态(0成功 1失败)',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 4240 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '操作日志记录' ROW_FORMAT = DYNAMIC;

-- ----------------------------
-- Table structure for sys_menu
-- ----------------------------
DROP TABLE IF EXISTS `sys_menu`;
CREATE TABLE `sys_menu`  (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '菜单ID',
  `name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '菜单名称',
  `parent_id` bigint NULL DEFAULT 0 COMMENT '父菜单ID',
  `sort` int NULL DEFAULT 0 COMMENT '显示顺序',
  `url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '地址',
  `type` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '菜单类型(M目录 C菜单 F按钮)',
  `perms` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '权限标识',
  `icon` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '#' COMMENT '菜单图标',
  `create_by` bigint NULL DEFAULT NULL COMMENT '创建者',
  `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
  `update_by` bigint NULL DEFAULT NULL COMMENT '更新者',
  `update_time` datetime NULL DEFAULT NULL COMMENT '更新时间',
  `remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '备注',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2044 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '菜单权限表' ROW_FORMAT = DYNAMIC;

-- ----------------------------
-- Table structure for sys_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_role`;
CREATE TABLE `sys_role`  (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '角色ID',
  `role_name` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '角色名称',
  `role_key` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '角色代码',
  `create_by` bigint NULL DEFAULT NULL COMMENT '创建者',
  `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',
  `update_by` bigint NULL DEFAULT NULL COMMENT '更新者',
  `update_time` datetime NULL DEFAULT NULL COMMENT '更新时间',
  `remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '备注',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '角色信息表' ROW_FORMAT = DYNAMIC;

-- ----------------------------
-- Table structure for sys_token
-- ----------------------------
DROP TABLE IF EXISTS `sys_token`;
CREATE TABLE `sys_token`  (
  `id` bigint NOT NULL COMMENT '主键',
  `user_id` bigint NULL DEFAULT NULL COMMENT '用户Id',
  `token` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '用户token',
  `type` tinyint(1) NULL DEFAULT NULL COMMENT '终端类型(1 web端 2 app端)',
  `status` tinyint(1) NULL DEFAULT NULL COMMENT '登录状态 (1 已登录 2 已注销)',
  `login_time` datetime NULL DEFAULT NULL COMMENT '登录时间',
  `logout_time` datetime NULL DEFAULT NULL COMMENT '退出时间',
  `last_request_time` datetime NULL DEFAULT NULL COMMENT '最后一次登录时间(最后一次请求时间)',
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;

-- ----------------------------
-- Table structure for sys_user
-- ----------------------------
DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user`  (
  `id` bigint NOT NULL,
  `dept_id` bigint NOT NULL AUTO_INCREMENT COMMENT '部门id',
  `user_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '用户名称',
  `real_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '用户姓名',
  `password` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '密码',
  `salt` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '密码加密盐值',
  `roles` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,
  `login_date` datetime NULL DEFAULT NULL COMMENT '登录时间',
  `error_num` int NOT NULL DEFAULT 0 AUTO_INCREMENT COMMENT '密码错误次数',
  `update_pwd_time` datetime NULL DEFAULT NULL COMMENT '密码更改时间'
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;

SET FOREIGN_KEY_CHECKS = 1;

二、JWTToken,继承 AuthenticationToken

import org.apache.shiro.authc.AuthenticationToken;

public class JWTToken implements AuthenticationToken {

    private String token;
    //登录类型,区分PC端和移动端
    private String loginType;

    public JWTToken(String token,String loginType) {
        this.token = token;
        this.loginType=loginType;
    }

    public String getLoginType() {
        return loginType;
    }

    public void setLoginType(String loginType) {
        this.loginType = loginType;
    }

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

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

三、工具类

1、JWTUtil 生成token,校验token,获取用户信息

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.entity.sys.SysUser;
import org.apache.shiro.SecurityUtils;

import java.io.UnsupportedEncodingException;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * jwt工具类
 */
public class JWTUtil {

    //token有效时长(30分钟)
    private static final long EXPIRE=30*60*1000L;
    //token的密钥
    private static final String SECRET="jwt+shiro";

    /**
     * 生成token
     * @param userName 用户名
     * @param current 当前时间截点
     * @param loginType 登录类型
     * @return
     */
    public static String createToken(String userName,Long current,String loginType) {
        //token过期时间
        Date date=new Date(current+EXPIRE);

        //jwt的header部分
        Map<String ,Object>map=new HashMap<>();
        map.put("alg","HS256");
        map.put("typ","JWT");

        //使用jwt的api生成token
        String token= null;//签名
        try {
            token = JWT.create()
                    .withHeader(map)
                    .withClaim("userName", userName+"_"+loginType)//私有声明
                    .withClaim("current",current)//当前时间截点
                    .withExpiresAt(date)//过期时间
                    .withIssuedAt(new Date())//签发时间
                    .sign(Algorithm.HMAC256(SECRET));
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        return token;
    }

    //校验token的有效性,1、token的header和payload是否没改过;2、没有过期
    public static boolean verify(String token){
        try {
            //解密
            JWTVerifier verifier=JWT.require(Algorithm.HMAC256(SECRET)).build();
            verifier.verify(token);
            return true;
        }catch (Exception e){
            return false;
        }
    }
    //根据token获取用户名(无需解密也可以获取token的信息)
    public static String getUserName(String token){
        try {
            DecodedJWT jwt = JWT.decode(token);
            String userName = jwt.getClaim("userName").asString();
            userName = userName.substring(0,userName.lastIndexOf("_"));
            return userName;
        } catch (JWTDecodeException e) {
            return null;
        }
    }

	/**
     * 获取当前用户信息
     */
    public static SysUser getUserInfo(){
        SysUser user =(SysUser) SecurityUtils.getSubject().getPrincipal();
        return user;
    }

    public static Long getUserId(){
        return getUserInfo().getId();
    }

    //获取过期时间
    public static Long getExpire(String token){
        try {
            DecodedJWT jwt = JWT.decode(token);
            return jwt.getClaim("current").asLong();
        }catch (Exception e){
            return null;
        }
    }
}

2、RedisUtil

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;

/**
 * redis工具类
 */
@Component
public class RedisUtil {
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    /**
     * 指定缓存失效时间
     * @param key  键
     * @param time 时间(秒)
     */
    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) {
            e.printStackTrace();
            return false;
        }
    }


    /**
     * 删除缓存
     * @param key 可以传一个值 或多个
     */
    public void del(String... key) {
        if (key != null && key.length > 0) {
            if (key.length == 1) {
                redisTemplate.delete(key[0]);
            } else {
                redisTemplate.delete((Collection<String>) 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;
        }
    }


    /**
     * 递增
     * @param key   键
     * @param delta 要增加几(大于0)
     */
    public 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 long decr(String key, long delta) {
        if (delta < 0) {
            throw new RuntimeException("递减因子必须大于0");
        }
        return redisTemplate.opsForValue().decrement(key,delta);
//        return redisTemplate.opsForValue().increment(key, -delta);
    }

    public long strLen(String key){
        return redisTemplate.opsForValue().get(key).toString().length();
    }

    /*
     * 追加字符
     * @param key   键
     * @param str   要追加的字符
     * */
    public boolean append(String key,String str){
        try {
            redisTemplate.opsForValue().append(key,str);
            return true;
        }catch (Exception e){
            return false;
        }
    }


    /**
     * HashGet
     * @param key  键 不能为null
     * @param item 项 不能为null
     */
    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);
    }

    /**
     * HashSet
     * @param key 键
     * @param map 对应多个键值
     */
    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<String, 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)
     */
    public double hincr(String key, String item, double by) {
        return redisTemplate.opsForHash().increment(key, item, by);
    }


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


    /**
     * 根据key获取Set中的所有值
     * @param key 键
     */
    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 键
     */
    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代表所有值
     */
    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 键
     */
    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倒数第二个元素,依次类推
     */
    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 值
     */
    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  时间(秒)
     */
    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;
        }
    }

}

3、SpringUtil

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

/**
 * Spring上下文工具类
 */
@Component
public class SpringUtil implements ApplicationContextAware {
    private static ApplicationContext context;
    /**
     * Spring在bean初始化后会判断是不是ApplicationContextAware的子类
     * 如果该类是,setApplicationContext()方法,会将容器中ApplicationContext作为参数传入进去
     * @Author Sans
     * @CreateTime 2019/6/17 16:58
     */
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        context = applicationContext;
    }
    /**
     * 通过Name返回指定的Bean
     * @Author Sans
     * @CreateTime 2019/6/17 16:03
     */
    public static <T> T getBean(Class<T> beanClass) {
        return context.getBean(beanClass);
    }
}

四、realm

1、ModularRealm 多realm管理器

import com.common.vo.JWTToken;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.pam.ModularRealmAuthenticator;
import org.apache.shiro.realm.Realm;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Collection;
import java.util.HashMap;

/**
 * 自定义的Realm管理,主要针对多realm
 */
public class ModularRealm extends ModularRealmAuthenticator {

    private static final Logger log = LoggerFactory.getLogger(ModularRealm.class);

    @Override
    protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
        // 判断getRealms()是否返回为空
        assertRealmsConfigured();
        // 所有Realm
        Collection<Realm> realms = getRealms();
        // 登录类型对应的所有Realm
        HashMap<String, Realm> realmHashMap = new HashMap<>(realms.size());
        for (Realm realm : realms) {
            realmHashMap.put(realm.getName(), realm);
        }
        JWTToken token = (JWTToken) authenticationToken;
        // 登录类型
        String type = token.getLoginType();
        //根据登录类型,走对应的realm
        if (realmHashMap.get(type) != null) {
            return doSingleRealmAuthentication(realmHashMap.get(type), token);
        } else {
            return doMultiRealmAuthentication(realms, token);
        }
    }
}

2、MobileRealm 移动端的realm

import com.auth0.jwt.exceptions.TokenExpiredException;
import com.common.constant.UserConstant;
import com.common.enums.ResultEnum;
import com.common.util.JWTUtil;
import com.common.util.RedisUtil;
import com.common.vo.CustomException;
import com.common.vo.JWTToken;
import com.entity.sys.SysUser;
import com.service.sys.SysUserService;
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;


/**
 * app端登录的Realm管理
 */
public class MobileRealm extends AuthorizingRealm {
    @Autowired
    private SysUserService userService;
    @Autowired
    private RedisUtil redisUtil;

    /**
     * 使用JWT替代原生Token
     * @param token
     * @return
     */
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JWTToken;
    }

    private static final String ADMIN_LOGIN_TYPE = UserConstant.APP;
    {
        super.setName("mobile");    //设置realm的名字,非常重要
    }

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
    	//这里根据自己的需求进行授权和处理
        return null;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        String jwt= (String) authenticationToken.getCredentials();
        String userName= JWTUtil.getUserName(jwt);
        SysUser user = userService.getUserByName(userName);
        //判断账号是否存在
        if (user == null ) {
            throw new CustomException(ResultEnum.USER_NOT_ERROR,"");
        }
        String userNameType = userName+"_"+UserConstant.APP;
        if (redisUtil.hasKey(userNameType)){
            //判断AccessToken有无过期
            if (!JWTUtil.verify(jwt)){
                throw new TokenExpiredException("token认证失效,token过期,重新登陆");
            }else {
                //判断AccessToken和refreshToken的时间节点是否一致
                long current = (long) redisUtil.hget(userNameType, "current");
                if (current==JWTUtil.getExpire(jwt)){
                    return new SimpleAuthenticationInfo(user,jwt,getName());
                }
            }
        }
        return null;
    }
}

3、WebRealm PC端的realm

import com.auth0.jwt.exceptions.TokenExpiredException;
import com.common.constant.UserConstant;
import com.common.enums.ResultEnum;
import com.common.util.JWTUtil;
import com.common.util.RedisUtil;
import com.common.vo.CustomException;
import com.common.vo.JWTToken;
import com.entity.sys.SysMenu;
import com.entity.sys.SysRole;
import com.entity.sys.SysUser;
import com.service.sys.SysMenuService;
import com.service.sys.SysRoleService;
import com.service.sys.SysUserService;
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.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * web端登录的Realm管理
 */
public class WebRealm extends AuthorizingRealm {
    @Autowired
    private SysUserService userService;
    @Autowired
    private SysRoleService roleService;
    @Autowired
    private SysMenuService menuService;
    @Autowired
    private RedisUtil redisUtil;

    /**
     * 使用JWT替代原生Token
     * @param token
     * @return
     */
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JWTToken;
    }

    private static final String ADMIN_LOGIN_TYPE = UserConstant.WEB;
    {
        super.setName("web");    //设置realm的名字,非常重要
    }

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        SysUser user = (SysUser)principalCollection.getPrimaryPrincipal();
        //这里可以进行授权和处理
        Set<String> rolesSet = new HashSet<>();
        Set<String> permsSet = new HashSet<>();
        //查询角色和权限(这里根据业务自行查询)
        List<SysRole> roleList = roleService.selectRoleByUserId(user);
        for (SysRole role:roleList) {
            rolesSet.add(role.getRoleName());
            List<SysMenu> menuList = menuService.selectMenuByRoleId(role.getRoleId());
            for (SysMenu menu :menuList) {
                permsSet.add(menu.getPerms());
            }
        }
        //将查到的权限和角色分别传入authorizationInfo中
        authorizationInfo.setStringPermissions(permsSet);
        authorizationInfo.setRoles(rolesSet);
        return authorizationInfo;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        String jwt= (String) authenticationToken.getCredentials();
        String userName= JWTUtil.getUserName(jwt);
        SysUser user = userService.getUserByName(userName);
        //判断账号是否存在
        if (user == null ) {
            throw new CustomException(ResultEnum.USER_NOT_ERROR,"");
        }
        String userNameType = userName+"_"+UserConstant.WEB;
        if (redisUtil.hasKey(userNameType)){
            if (!JWTUtil.verify(jwt)){
                throw new TokenExpiredException("token认证失效,token过期,重新登陆");
            }else {
                //判断AccessToken和refreshToken的时间节点是否一致
                long current = (long) redisUtil.hget(userNameType, "current");
                if (current==JWTUtil.getExpire(jwt)){
                    return new SimpleAuthenticationInfo(user,jwt,getName());
                }
            }
        }
        return null;
    }
}

五、JWTFilter 过滤器

import com.alibaba.fastjson.JSONObject;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.common.constant.UserConstant;
import com.common.util.JWTUtil;
import com.common.vo.JWTToken;
import com.entity.sys.SysToken;
import com.entity.sys.SysUser;
import com.common.util.RedisUtil;
import com.common.util.SpringUtil;
import com.common.vo.ResultVo;
import com.service.sys.SysTokenService;
import com.service.sys.SysUserService;
import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;
import org.springframework.http.HttpStatus;
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.IOException;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

public class JWTFilter extends BasicHttpAuthenticationFilter {

    //是否允许访问,如果带有 token,则对 token 进行检查
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        HttpServletRequest req= (HttpServletRequest) request;
        //判断请求的请求头是否带上 "Token"
        if (isLoginAttempt(request, response)){
            String loginType=req.getHeader("loginType");
            try {
                 //如果存在,则进入 executeLogin 方法执行登入,检查 token 是否正确
                 executeLogin(request, response);
                 return true;
            }catch (Exception e){
                 if (UserConstant.APP.equals(loginType)){
                     return refreshTokenApp(req,request,response);
                 }else {
                 	/*
                     * 注意这里捕获的异常其实是在Realm抛出的,但是由于executeLogin()方法抛出的异常是从login()来的,
                     * login抛出的异常类型是AuthenticationException,所以要去获取它的子类异常才能获取到我们在Realm抛出的异常类型。
                     * */
                 	Throwable cause = e.getCause();
                    if (cause!=null&&cause instanceof TokenExpiredException){
                        //AccessToken过期,尝试去刷新token
                        return refreshToken(request, response);
                    }
                 }
             }
        }
        return false;
    }

    @Override
    protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
        HttpServletRequest req= (HttpServletRequest) request;
        String token=req.getHeader("Authorization");
        return token !=null;
    }
    /*
     * executeLogin实际上就是先调用createToken来获取token,这里我们重写了这个方法,就不会自动去调用createToken来获取token
     * 然后调用getSubject方法来获取当前用户再调用login方法来实现登录
     * 这也解释了我们为什么要自定义jwtToken,因为我们不再使用Shiro默认的UsernamePasswordToken了。
     * */
    @Override
    protected boolean executeLogin(ServletRequest request, ServletResponse response){
        HttpServletRequest req= (HttpServletRequest) request;
        String token=req.getHeader("Authorization");
        String loginType=req.getHeader("loginType");
        JWTToken jwt=new JWTToken(token,loginType);
        //交给自定义的realm对象去登录,如果错误他会抛出异常并被捕获
        getSubject(request, response).login(jwt);
        return true;
    }

    @Override
    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest req= (HttpServletRequest) request;
        HttpServletResponse res= (HttpServletResponse) response;
        res.setHeader("Access-control-Allow-Origin",req.getHeader("Origin"));
        res.setHeader("Access-control-Allow-Methods","GET,POST,OPTIONS,PUT,DELETE");
        res.setHeader("Access-control-Allow-Headers",req.getHeader("Access-Control-Request-Headers"));
        // 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态
        if (req.getMethod().equals(RequestMethod.OPTIONS.name())) {
            res.setStatus(HttpStatus.OK.value());
            return false;
        }
        return super.preHandle(request, response);
    }

    /**
     * isAccessAllowed返回false时,执行该方法
     * 在访问controller前判断是否登录,返回json,不进行重定向。
     * @return true-继续往下执行,false-该filter过滤器已经处理,不继续执行其他过滤器
     */
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws IOException {
        HttpServletResponse httpServletResponse = (HttpServletResponse) response;
        //这里是个坑,如果不设置的接受的访问源,那么前端都会报跨域错误,因为这里还没到corsConfig里面
        httpServletResponse.setHeader("Access-Control-Allow-Origin", ((HttpServletRequest) request).getHeader("Origin"));
        httpServletResponse.setHeader("Access-Control-Allow-Credentials", "true");
        httpServletResponse.setCharacterEncoding("UTF-8");
        httpServletResponse.setContentType("application/json");
        ResultVo resultVo = new ResultVo();
        resultVo.setCode(1003);
        resultVo.setMessage("用户未登录,请进行登录");
        httpServletResponse.getWriter().write(JSONObject.toJSON(resultVo).toString());
        return false;
    }

    //刷新token
    private boolean refreshToken(ServletRequest request,ServletResponse response) {
        HttpServletRequest req= (HttpServletRequest) request;
        RedisUtil redisUtil= SpringUtil.getBean(RedisUtil.class);
        //获取传递过来的accessToken
        String accessToken=req.getHeader("Authorization");
        String loginType=req.getHeader("loginType");
        //获取token里面的用户名
        String userName = JWTUtil.getUserName(accessToken);
        String userNameType = userName+"_"+loginType;
        //判断refreshToken是否过期了,过期了那么所含的username的键不存在
        if (redisUtil.hasKey(userNameType)){
            //判断refresh的时间节点和传递过来的accessToken的时间节点是否一致,不一致校验失败
            long current= (long) redisUtil.hget(userNameType,"current");
            if (current==JWTUtil.getExpire(accessToken)){
                //获取当前时间节点
                long currentTimeMillis = System.currentTimeMillis();
                //生成刷新的token
                String token=JWTUtil.createToken(userName,currentTimeMillis,loginType);
                //刷新redis里面的refreshToken,过期时间是30min
                Map<String,Object> setMap = new HashMap<>();
                setMap.put("current",currentTimeMillis);
                setMap.put("userInfo",redisUtil.hget(userNameType,"userInfo"));
                redisUtil.hmset(userNameType,setMap,30*60);
                //再次交给shiro进行认证
                JWTToken jwtToken=new JWTToken(token,loginType);
                try {
                    getSubject(request, response).login(jwtToken);
                    // 最后将刷新的AccessToken存放在Response的Header中的Authorization字段返回
                    HttpServletResponse httpServletResponse = (HttpServletResponse) response;
                    httpServletResponse.setHeader("Authorization", token);
                    httpServletResponse.setHeader("loginType", loginType);
                    httpServletResponse.setHeader("Access-Control-Expose-Headers", "Authorization");
                    return true;
                }catch (Exception e){
                    return false;
                }
            }
        }
        return false;
    }

    /**
     * app刷新token
     */
    private Boolean refreshTokenApp(ServletRequest request,ServletResponse response) {
        HttpServletRequest req= (HttpServletRequest) request;
        RedisUtil redisUtil=SpringUtil.getBean(RedisUtil.class);

        // 如果是app端登录,则根据token去数据库查询,有数据则刷新token。
        // 并将新的token保存到数据库中,没有数据则提示用户重新登录。
        SysTokenService tokenService = SpringUtil.getBean(SysTokenService.class);
        SysUserService userService = SpringUtil.getBean(SysUserService.class);
        String token = req.getHeader("Authorization");
        String loginType=req.getHeader("loginType");
        SysToken sysToken = tokenService.getByToken(token);
        if (sysToken != null) {
            SysUser user = userService.getById(sysToken.getUserId());
            long currentTimeMillis = System.currentTimeMillis();
            String newToken = JWTUtil.createToken(user.getUserName(), currentTimeMillis,UserConstant.APP);
            sysToken.setLoginTime(new Date());
            sysToken.setToken(newToken);
            tokenService.updateById(sysToken);

            Map<String,Object> setMap = new HashMap<>();
            setMap.put("current",currentTimeMillis);
            setMap.put("userInfo",user);
            redisUtil.hmset(user.getUserName()+"_"+UserConstant.APP,setMap,30*60);
            JWTToken jwtToken = new JWTToken(newToken,loginType);
            try {
                getSubject(request, response).login(jwtToken);
                // 最后将刷新的AccessToken存放在Response的Header中的Authorization字段返回
                HttpServletResponse httpServletResponse = (HttpServletResponse) response;
                httpServletResponse.setHeader("Authorization", newToken);
                httpServletResponse.setHeader("loginType", loginType);
                httpServletResponse.setHeader("Access-Control-Expose-Headers", "Authorization");
                return true;
            }catch (Exception e){
                e.getMessage();
            }
        }
        return false;
    }
}

六、config

1、RedisConfig

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
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.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.net.UnknownHostException;

/**
 * redis序列化
 * @author fuhua
 */
@Configuration
public class RedisConfig {
    //编写我们自己的redisTemplate
    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
        //我们为了自己开发使用方便,一般使用<String, Object>类型
        RedisTemplate<String, Object> template = new RedisTemplate();
        template.setConnectionFactory(redisConnectionFactory);
        //序列化配置
        //json序列化
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer=new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper om=new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);

        //String序列化
        StringRedisSerializer stringRedisSerializer=new StringRedisSerializer();

        //key使用String的序列化方式
        template.setKeySerializer(stringRedisSerializer);
        //hash的key也使用String序列化
        template.setHashKeySerializer(stringRedisSerializer);
        //value使用json序列化
        template.setValueSerializer(jackson2JsonRedisSerializer);
        //hash的value使用json序列化
        template.setHashValueSerializer(jackson2JsonRedisSerializer);
        template.afterPropertiesSet();
        return template;
    }
}

2、ShiroConfig

import com.common.constant.UserConstant;
import com.common.filter.JWTFilter;
import com.common.realm.MobileRealm;
import com.common.realm.ModularRealm;
import com.common.realm.WebRealm;
import org.apache.shiro.authc.Authenticator;
import org.apache.shiro.authc.pam.AtLeastOneSuccessfulStrategy;
import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;
import org.apache.shiro.mgt.DefaultSubjectDAO;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.realm.Realm;
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.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.servlet.Filter;
import java.util.*;

/**
 * @Description Shiro配置类
 */
@Configuration
public class ShiroConfig {

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

    /**
     * 开启Shiro-aop注解支持
     * @Attention 使用代理方式所以需要开启代码支持
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }

    /**
     * Shiro基础配置
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactory(SecurityManager securityManager){
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        // 注意过滤器配置顺序不能颠倒
        // 配置过滤:不会被拦截的链接
        filterChainDefinitionMap.put("/swagger-ui.html", "anon");
        filterChainDefinitionMap.put("/swagger/**", "anon");
        filterChainDefinitionMap.put("/swagger-resources/**", "anon");
        filterChainDefinitionMap.put("/v2/**", "anon");
        filterChainDefinitionMap.put("/webjars/**", "anon");

//        filterChainDefinitionMap.put("/static/**", "anon");
        filterChainDefinitionMap.put("/uploads/**", "anon");
        filterChainDefinitionMap.put("/api/user/getCode", "anon");
        filterChainDefinitionMap.put("/api/user/login/web", "anon");
        filterChainDefinitionMap.put("/api/user/login/app", "anon");
        //将所有请求指向我们自己定义的jwt过滤器
        filterChainDefinitionMap.put("/**", "jwt");
        //获取filters
        Map<String, Filter> filters = shiroFilterFactoryBean.getFilters();
        //设置我们自定义的JWT过滤器,并且取名为jwt
        filters.put("jwt",new JWTFilter());
        shiroFilterFactoryBean.setFilters(filters);
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;
    }

    @Bean
    public Authenticator authenticator() {
        ModularRealm modularRealm = new ModularRealm();
        modularRealm.setRealms(Arrays.asList(webRealm(), mobileRealm()));
        modularRealm.setAuthenticationStrategy(new AtLeastOneSuccessfulStrategy());
        return modularRealm;
    }

    /**
     * 安全管理器
     */
    @Bean
    public SecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        //多realm
        Set<Realm> realms = new HashSet<Realm>();
        realms.add(mobileRealm());
        realms.add(webRealm());
        securityManager.setRealms(realms);
        //关闭session
        DefaultSubjectDAO subjectDAO=new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator sessionStorageEvaluator=new DefaultSessionStorageEvaluator();
        sessionStorageEvaluator.setSessionStorageEnabled(false);
        subjectDAO.setSessionStorageEvaluator(sessionStorageEvaluator);
        securityManager.setSubjectDAO(subjectDAO);
        securityManager.setAuthenticator(authenticator());//解决多realm的异常问题重点在此
        return securityManager;
    }

    /**
     * app端的身份验证器
     */
    @Bean
    public MobileRealm mobileRealm() {
        MobileRealm mobileRealm = new MobileRealm();
        mobileRealm.setName(UserConstant.APP);
        return mobileRealm;
    }
    /**
     * web端的身份验证器
     */
    @Bean
    public WebRealm webRealm() {
        WebRealm webRealm = new WebRealm();
        webRealm.setName(UserConstant.WEB);
        return webRealm;
    }
}

七、SysLoginController 登录的controller

import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.common.constant.UserConstant;
import com.common.enums.ResultEnum;
import com.common.util.*;
import com.common.vo.CustomException;
import com.common.vo.ResultVo;
import com.entity.sys.SysToken;
import com.entity.sys.SysUser;
import com.service.sys.SysTokenService;
import com.service.sys.SysUserService;
import lombok.AllArgsConstructor;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.LockedAccountException;
import org.apache.shiro.authz.annotation.RequiresAuthentication;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.OutputStream;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

@AllArgsConstructor
@RestController
@RequestMapping("/${api.url.prefix}/login")
public class SysLoginController {

    @Autowired
    private SysUserService userService;

    @Autowired
    private RedisUtil redisUtil;

    @Autowired
    private SysTokenService tokenService;

    //密码最大错误次数
    private static int ERROR_COUNT = 3;

    /**
     * web端登录
     */
    @PostMapping("/web")
    public ResultVo web(String userName, String password,String code){
        try {
            Object verCode = redisUtil.get("verCode");
            if (null == verCode)
                return ResultUtil.error("验证码已失效,请重新输入");
            String verCodeStr = verCode.toString();
            if (verCodeStr == null || StringUtils.isEmpty(code) || !verCodeStr.equalsIgnoreCase(code))
                return ResultUtil.error("验证码错误");
            else if (!redisUtil.hasKey("verCode"))
                return ResultUtil.error("验证码已过期,请重新输入");
            else
                redisUtil.del("verCode");

            String salt = userService.getSalt(userName);
            password = SHA256Util.sha256(password, salt);
            //验证用户名和密码
            SysUser user = passwordErrorNum(userName,password);

            long currentTimeMillis = System.currentTimeMillis();
            String token= JWTUtil.createToken(user.getUserName(),currentTimeMillis,UserConstant.WEB);
            Map<String, Object> map = new HashMap<>();
            map.put("current",currentTimeMillis);
            map.put("userInfo",user);
            redisUtil.hmset(userName+"_"+UserConstant.WEB,map,60*30);

            return ResultUtil.success(token);
        }catch (IncorrectCredentialsException e) {
            return ResultUtil.error(1000,e.getMessage());
        } catch (LockedAccountException e) {
            return ResultUtil.error(1004,e.getMessage());
        } catch (AuthenticationException e) {
            return ResultUtil.error(ResultEnum.USER_NOT_ERROR);
        } catch (Exception e) {
            return ResultUtil.error(ResultEnum.UNKNOWN_EXCEPTION);
        }
    }

    /**
     * web登录获取验证码
     */
    @RequestMapping(value = "/getCode", method = RequestMethod.GET)
    public void getCode(HttpServletRequest request, HttpServletResponse response) {
        try {
            response.setHeader("Pragma", "No-cache");
            response.setHeader("Cache-Control", "no-cache");
            response.setDateHeader("Expires", 0);
            response.setContentType("image/jpeg");
            // 生成随机字串
            String verifyCode = VerifyCodeUtils.generateVerifyCode(4);
            //将验证码存入redis中,设置有效期为一分钟
            redisUtil.set("verCode",verifyCode,60);
            // 生成图片
            int w = 200, h = 50;
            OutputStream out = response.getOutputStream();
            VerifyCodeUtils.outputImage(w, h, out, verifyCode);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * app端登录
     */
    @PostMapping("/app")
    public ResultVo app(String userName, String password){
        try {
            String salt = userService.getSalt(userName);
            password = SHA256Util.sha256(password, salt);
            //验证用户名和密码
            SysUser user = passwordErrorNum(userName,password);
            long currentTimeMillis = System.currentTimeMillis();
            String token= JWTUtil.createToken(user.getUserName(),currentTimeMillis,UserConstant.APP);
            SysToken sysToken = new SysToken();
            sysToken.setUserId(user.getId());
            sysToken.setToken(token);
            sysToken.setLoginTime(new Date());
            tokenService.save(sysToken);

            Map<String, Object> map = new HashMap<>();
            map.put("current",currentTimeMillis);
            map.put("userInfo",user);
            redisUtil.hmset(userName+"_"+UserConstant.APP,map,60*30);

            return ResultUtil.success(token);
        }catch (IncorrectCredentialsException e) {
            return ResultUtil.error(1000,e.getMessage());
        } catch (LockedAccountException e) {
            return ResultUtil.error(1004,e.getMessage());
        } catch (AuthenticationException e) {
            return ResultUtil.error(ResultEnum.USER_NOT_ERROR);
        } catch (Exception e) {
            return ResultUtil.error(ResultEnum.UNKNOWN_EXCEPTION);
        }
    }

    /**
     * 退出登录
     */
    @DeleteMapping("/logout")
    @RequiresAuthentication
    public ResultVo logout(HttpServletRequest request){
        String token = request.getHeader("Authorization");
        String loginType = request.getHeader("loginType");
        if (UserConstant.APP.equals(loginType)){
            SysToken sysToken = tokenService.getByToken(token);
            if (sysToken != null){
                tokenService.removeById(sysToken.getId());
            }
        }
        String username=JWTUtil.getUserName(token);
        redisUtil.del(username+"_"+loginType);
        return ResultUtil.success("退出登录成功");
    }

    /**
     * 密码错误次数验证
     * @param userName
     * @param password
     * @return
     */
    private SysUser passwordErrorNum(String userName,String password){
        //查询用户
        SysUser user = userService.getUserByName(userName);
        if (null == user){
            throw new AuthenticationException();
        }
        /*Safe securitySet = securitySetService.getById(1);
        //密码登录限制(0:连续错3次,锁定账号15分钟。1:连续错5次,锁定账号30分钟)
        if (securitySet.getPwdLoginLimit()==1){
            ERROR_COUNT = 5;
        }*/
        //登录时间
        Date allowTime = user.getLoginDate() == null ? new Date() : user.getLoginDate();
        //当前时间
        Date currentTime = new Date();
        try {
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            allowTime = sdf.parse(sdf.format(allowTime));
        }catch (ParseException e){
            throw new CustomException(-1,"日期转换异常","");
        }
        UpdateWrapper<SysUser> updateWrapper = new UpdateWrapper<>();
        //如果当前登录时间大于允许登录时间
        if (allowTime == null || currentTime.getTime() > allowTime.getTime()) {
            // 判断用户账号和密码是否正确
            user = userService.getUserByPass(userName, password);
            if (user != null) {
                //正确密码错误次数清零
                updateWrapper.set("error_num",0);
                updateWrapper.set("login_date",new Date());
                updateWrapper.eq("id",user.getId());
                userService.update(updateWrapper);
            } else {
                //登录错误次数
                int errorNum =  user.getErrorNum();
                //最后登录的时间
                long allowTimes = user.getLoginDate() == null ? 0 : user.getLoginDate().getTime();
                //错误的次数
                if (errorNum < ERROR_COUNT-1) {
                    int surplusCount = ERROR_COUNT - errorNum-1;
                    boolean result;
                    //每次输入错误密码间隔时间在2分钟内 (如果上次登录错误时间距离相差小于定义的时间(毫秒))
                    if ((currentTime.getTime() - allowTimes) <= 120000) {
                        updateWrapper.set("error_num",errorNum + 1);
                        updateWrapper.set("login_date",new Date());
                        updateWrapper.eq("id",user.getId());
                        result = userService.update(updateWrapper);
                    } else {
                        updateWrapper.set("error_num",1);
                        updateWrapper.set("login_date",new Date());
                        updateWrapper.eq("id",user.getId());
                        result = userService.update(updateWrapper);
                    }
                    if (result) {
                        //抛出密码错误异常
                        throw new IncorrectCredentialsException("密码错误,总登录次数"+ERROR_COUNT+"次,剩余次数: " + surplusCount);
                    }
                } else {
                    //错误3次,锁定15分钟后才可登陆 允许时间加上定义的登陆时间(毫秒)
                    Date dateAfterAllowTime = new Date(currentTime.getTime() + 900000);
                    String str = "15";
                    if (ERROR_COUNT == 5){
                        //错误5次,锁定30分钟后才可登陆 允许时间加上定义的登陆时间(毫秒)
                        dateAfterAllowTime = new Date(currentTime.getTime() + 1800000);
                        str = "30";
                    }
                    updateWrapper.set("error_num",0);
                    updateWrapper.set("login_date",dateAfterAllowTime);
                    updateWrapper.eq("id",user.getId());
                    if (userService.update(updateWrapper)) {
                        throw new LockedAccountException("您的密码已错误"+ERROR_COUNT+"次,现已被锁定,请"+str+"分钟后再尝试");
                    }
                }
            }
        }else {
            Calendar calendar = Calendar.getInstance();
            calendar.setTime(allowTime);
            long time1 = calendar.get(Calendar.MINUTE);
            calendar.setTime(currentTime);
            long time2 = calendar.get(Calendar.MINUTE);
            long between_minute=(time1-time2);
            throw new LockedAccountException("账号锁定,还没到允许登录的时间,请"+between_minute+"分钟后再尝试");
        }
        return user;
    }
}

八、yml

server:
  # 服务器端口号
  port: 8081

spring:
  # 配置数据库连接池
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/my_shiro?useUnicode=true&characterEncoding=utf8&allowMultiQueries=true&useSSL=false&serverTimezone=UTC
    username: root
    password: root
    type: com.zaxxer.hikari.HikariDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    hikari:
      # 最小连接
      minimum-idle: 5
      # 最大连接
      maximum-pool-size: 15
      # 自动提交
      auto-commit: true
      # 最大空闲时间
      idle-timeout: 30000
      # 连接池名称
      pool-name: DatebookHikariCP
      # 最大生命周期
      max-lifetime: 900000
      # 连接超时时间
      connection-timeout: 15000
      # 心跳检测
      connection-test-query: select 1

  # 配置Redis
  redis:
    host: localhost
    port: 6379
    timeout: 6000 #以秒为单位
    password: 123456
    database: 0
    lettuce:
      pool:
        max-active: -1
        max-wait: -1
        max-idle: 16
        min-idle: 8

  main:
    allow-bean-definition-overriding: true

  servlet:
    multipart:
      max-file-size: -1
      max-request-size: -1
  # mybatis_plus
  #mybatis-plus:
  # xml路径
#  mapper-locations: classpath:mapping/*Mapper.xml

# mybatis-plus相关配置
mybatis-plus:
  # xml扫描,多个目录用逗号或者分号分隔(告诉 Mapper 所对应的 XML 文件位置)
  mapper-locations: classpath:mapper/*/*.xml
  # 注意:对应实体类的路径
  type-aliases-package: com.entity.sys,;com.common.basic.entity
  # 以下配置均有默认值,可以不设置
  global-config:
    db-config:
      #主键类型 AUTO:"数据库ID自增" INPUT:"用户输入ID",ID_WORKER:"全局唯一ID (数字类型唯一ID)", UUID:"全局唯一ID UUID";
      id-type: auto
      #字段策略 IGNORED:"忽略判断"  NOT_NULL:"非 NULL 判断")  NOT_EMPTY:"非空判断"
      field-strategy: NOT_EMPTY
      #数据库类型
      db-type: MYSQL
  configuration:
    # 是否开启自动驼峰命名规则映射:从数据库列名到Java属性驼峰命名的类似映射
    map-underscore-to-camel-case: true
    # 返回map时true:当查询数据为空时字段返回为null,false:不加这个查询数据为空时,字段将被隐藏
    call-setters-on-nulls: true
    # 这个配置会将执行的sql打印出来,在开发或测试的时候可以用
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

api.url.prefix: /api

九、其他

1、GlobalExceptionConfig 全局异常处理

import com.common.enums.ResultEnum;
import com.common.util.ResultUtil;
import com.common.vo.CustomException;
import com.common.vo.ResultVo;
import lombok.extern.slf4j.Slf4j;
import org.apache.shiro.authz.UnauthorizedException;
import org.springframework.http.HttpStatus;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;

import java.util.Objects;


/**
 * 全局异常处理
 */
@Slf4j
@RestControllerAdvice
public class GlobalExceptionConfig {

    /**
     * 自定义异常
     */
    @ExceptionHandler(value = CustomException.class)
    public ResultVo processException(CustomException e) {
        log.error("位置:{} -> 错误信息:{}", e.getMethod() ,e.getLocalizedMessage());
        return ResultUtil.error(Objects.requireNonNull(ResultEnum.getByCode(e.getCode())));
    }

    /**
     * 拦截表单参数校验
     */
    @ResponseStatus(HttpStatus.OK)
    @ExceptionHandler({BindException.class})
    public ResultVo bindException(BindException e) {
        BindingResult bindingResult = e.getBindingResult();
        return ResultUtil.error(Objects.requireNonNull(bindingResult.getFieldError()).getDefaultMessage());
    }

    /**
     * 拦截JSON参数校验
     */
    @ResponseStatus(HttpStatus.OK)
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ResultVo bindException(MethodArgumentNotValidException e) {
        BindingResult bindingResult = e.getBindingResult();
        return ResultUtil.error(Objects.requireNonNull(bindingResult.getFieldError()).getDefaultMessage());
    }

    /**
     * 参数格式错误
     */
    @ExceptionHandler(MethodArgumentTypeMismatchException.class)
    public ResultVo methodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e) {
        log.error("错误信息{}", e.getLocalizedMessage());
        return ResultUtil.error(ResultEnum.ARGUMENT_TYPE_MISMATCH);
    }

    /**
     * 参数格式错误
     */
    @ExceptionHandler(HttpMessageNotReadableException.class)
    public ResultVo httpMessageNotReadable(HttpMessageNotReadableException e) {
        log.error("错误信息:{}", e.getLocalizedMessage());
        return ResultUtil.error(ResultEnum.FORMAT_ERROR);
    }

    /**
     * 请求方式不支持
     */
    @ExceptionHandler(HttpRequestMethodNotSupportedException.class)
    public ResultVo httpReqMethodNotSupported(HttpRequestMethodNotSupportedException e) {
        log.error("错误信息:{}", e.getLocalizedMessage());
        return ResultUtil.error(ResultEnum.REQ_METHOD_NOT_SUPPORT);
    }

    /**
     * 通用异常
     */
    @ResponseStatus(HttpStatus.OK)
    @ExceptionHandler(Exception.class)
    public ResultVo exception(Exception e) {
        //权限不足异常
        if (e instanceof UnauthorizedException) {
            return ResultUtil.error(ResultEnum.SHIRO_ERROR);
        }
        e.printStackTrace();
        return ResultUtil.error(ResultEnum.UNKNOWN_EXCEPTION);
    }
}

2、UserConstant

public interface UserConstant {
    String APP = "app";
    String WEB = "web";
}

3、ResultEnum 返回状态枚举类

import lombok.Getter;

/**
 * 返回状态枚举类
 */
@Getter
public enum ResultEnum {

    /**
     * 未知异常
     */
    UNKNOWN_EXCEPTION(100, "未知异常"),
    /**
     * 请求方式不支持
     */
    REQ_METHOD_NOT_SUPPORT(101,"请求方式不支持"),
    /**
     * 格式错误
     */
    FORMAT_ERROR(102, "参数格式错误"),
    /**
     * 文件格式错误
     */
    FILE_FORMAT_ERROR(103,"文件格式错误"),
    FILE_PATH_ERROR(105,"文件上传路径错误"),
    FILE_NAME_NOT_NULL(106,"文件名不可为空"),
    /**
     * 参数类型不匹配
     */
    ARGUMENT_TYPE_MISMATCH(104, "参数类型不匹配"),


    /**
     * 添加失败
     */
    ADD_ERROR(2000, "添加失败"),

    /**
     * 更新失败
     */
    UPDATE_ERROR(2001, "更新失败"),

    /**
     * 删除失败
     */
    DELETE_ERROR(2002, "删除失败"),

    /**
     * 查找失败
     */
    GET_ERROR(2003, "查询失败,数据可能不存在"),
    /**
     * 导入失败
     */
    IMPORT_ERROR(2004,"导入失败"),


    /**
     * 用户名或密码错误
     * */
    USER_PWD_ERROR(1000, "用户名或密码错误"),
    /**
     * 用户不存在
     * */
    USER_NOT_ERROR(1001, "用户不存在"),
    /** 登录超时,请重新登录 */
    LOGIN_TIME_OUT(1002,"登录超时,请重新登录"),
    /** 用户未登录,请进行登录 */
    USER_NOT_LOGIN(1003,"用户未登录,请进行登录"),
    /** 账号锁定 */
    USER_LOCK(1004,"账号锁定中"),

    /**
     * 非法令牌
     */
    ILLEGAL_TOKEN(5000,"非法令牌"),
    /**
     * 其他客户端登录
     */
    OTHER_CLIENT_LOGIN(5001,"其他客户端登录"),
    /**
     * 令牌过期
     */
    TOKEN_EXPIRED(5002,"令牌过期"),
    /**
     * 权限不足
     */
    SHIRO_ERROR(403,"权限不足");
    ;

    private Integer code;

    private String msg;

    ResultEnum(Integer code, String msg) {
        this.code = code;
        this.msg = msg;
    }

    /**
     * 通过状态码获取枚举对象
     * @param code 状态码
     * @return 枚举对象
     */
    public static ResultEnum getByCode(int code){
        for (ResultEnum resultEnum : ResultEnum.values()) {
            if(code == resultEnum.getCode()){
                return resultEnum;
            }
        }
        return null;
    }
}

4、ResultUtil 返回数据工具类

import com.common.enums.ResultEnum;
import com.common.vo.ResultVo;

import java.util.List;
import java.util.Map;

/**
 * 返回数据工具类
 */
public class ResultUtil {

    /**
     * 私有化工具类 防止被实例化
     */
    private ResultUtil() {}

    /**
     * 成功
     * @param object 需要返回的数据
     * @return data
     */
    public static ResultVo success(Object object) {
        ResultVo result = new ResultVo();
        result.setCode(0);
        result.setMessage("ok");
        result.setData(object);
        return result;
    }

    /**
     * 成功
     * @param map 需要返回的数据
     * @return data
     */
    public static ResultVo success(Map<String, List> map) {
        ResultVo result = new ResultVo();
        result.setCode(0);
        result.setMessage("ok");
        result.setData(map);
        return result;
    }

    /**
     * 成功
     */
    public static ResultVo success(Integer code,String msg) {
        ResultVo result = new ResultVo();
        result.setCode(code);
        result.setMessage(msg);
        return result;
    }

    /**
     * 成功
     * @return 返回空
     */
    public static ResultVo success() {
        return success(null);
    }

    /**
     * 错误
     * @param resultEnum 错误枚举类
     * @return 错误信息
     */
    public static ResultVo error(ResultEnum resultEnum) {
        ResultVo result = new ResultVo();
        result.setCode(resultEnum.getCode());
        result.setMessage(resultEnum.getMsg());
        return result;
    }

    /**
     * 错误
     * @param code 状态码
     * @param msg 消息
     * @return ResultBean
     */
    public static ResultVo error(Integer code, String msg) {
        ResultVo result = new ResultVo();
        result.setCode(code);
        result.setMessage(msg);
        return result;
    }

    /**
     * 错误
     * @param msg 错误信息
     * @return ResultBean
     */
    public static ResultVo error(String msg) {
        return error(-1, msg);
    }
}

5、SHA256Util 密码加密工具类

import org.apache.shiro.crypto.hash.SimpleHash;

/**
 * Sha-256加密工具
 */
public class SHA256Util {
    /**  私有构造器 **/
    private SHA256Util(){};
    /**  加密算法 **/
    public final static String HASH_ALGORITHM_NAME = "SHA-256";
    /**  循环次数 **/
    public final static int HASH_ITERATIONS = 15;
    /**  执行加密-采用SHA256和盐值加密 **/
    public static String sha256(String password, String salt) {
        return new SimpleHash(HASH_ALGORITHM_NAME, password, salt, HASH_ITERATIONS).toString();
    }
}

6、VerifyCodeUtils 验证码生成工具类

import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.Random;

/**
 * 生成验证码
 */
public class VerifyCodeUtils {
    //使用到Algerian字体,系统里没有的话需要安装字体,字体只显示大写,去掉了1,0,i,o几个容易混淆的字符
    public static final String VERIFY_CODES = "23456789ABCDEFGHJKLMNPQRSTUVWXYZ";
    private static Random random = new Random();


    /**
     * 使用系统默认字符源生成验证码
     *
     * @param verifySize 验证码长度
     * @return
     */
    public static String generateVerifyCode(int verifySize) {
        return generateVerifyCode(verifySize, VERIFY_CODES);
    }

    /**
     * 使用指定源生成验证码
     *
     * @param verifySize 验证码长度
     * @param sources    验证码字符源
     * @return
     */
    public static String generateVerifyCode(int verifySize, String sources) {
        if (sources == null || sources.length() == 0) {
            sources = VERIFY_CODES;
        }
        int codesLen = sources.length();
        Random rand = new Random(System.currentTimeMillis());
        StringBuilder verifyCode = new StringBuilder(verifySize);
        for (int i = 0; i < verifySize; i++) {
            verifyCode.append(sources.charAt(rand.nextInt(codesLen - 1)));
        }
        return verifyCode.toString();
    }

    /**
     * 生成随机验证码文件,并返回验证码值
     *
     * @param w
     * @param h
     * @param outputFile
     * @param verifySize
     * @return
     * @throws IOException
     */
    public static String outputVerifyImage(int w, int h, File outputFile, int verifySize) throws IOException {
        String verifyCode = generateVerifyCode(verifySize);
        outputImage(w, h, outputFile, verifyCode);
        return verifyCode;
    }

    /**
     * 输出随机验证码图片流,并返回验证码值
     *
     * @param w
     * @param h
     * @param os
     * @param verifySize
     * @return
     * @throws IOException
     */
    public static String outputVerifyImage(int w, int h, OutputStream os, int verifySize) throws IOException {
        String verifyCode = generateVerifyCode(verifySize);
        outputImage(w, h, os, verifyCode);
        return verifyCode;
    }

    /**
     * 生成指定验证码图像文件
     *
     * @param w
     * @param h
     * @param outputFile
     * @param code
     * @throws IOException
     */
    public static void outputImage(int w, int h, File outputFile, String code) throws IOException {
        if (outputFile == null) {
            return;
        }
        File dir = outputFile.getParentFile();
        if (!dir.exists()) {
            dir.mkdirs();
        }
        try {
            outputFile.createNewFile();
            FileOutputStream fos = new FileOutputStream(outputFile);
            outputImage(w, h, fos, code);
            fos.close();
        } catch (IOException e) {
            throw e;
        }
    }

    /**
     * 输出指定验证码图片流
     *
     * @param w
     * @param h
     * @param os
     * @param code
     * @throws IOException
     */
    public static void outputImage(int w, int h, OutputStream os, String code) throws IOException {
        int verifySize = code.length();
        BufferedImage image = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
        Random rand = new Random();
        Graphics2D g2 = image.createGraphics();
        g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        Color[] colors = new Color[5];
        Color[] colorSpaces = new Color[]{Color.WHITE, Color.CYAN,
                Color.GRAY, Color.LIGHT_GRAY, Color.MAGENTA, Color.ORANGE,
                Color.PINK, Color.YELLOW};
        float[] fractions = new float[colors.length];
        for (int i = 0; i < colors.length; i++) {
            colors[i] = colorSpaces[rand.nextInt(colorSpaces.length)];
            fractions[i] = rand.nextFloat();
        }
        Arrays.sort(fractions);

        g2.setColor(Color.GRAY);// 设置边框色
        g2.fillRect(0, 0, w, h);

        Color c = getRandColor(200, 250);
        g2.setColor(c);// 设置背景色
        g2.fillRect(0, 2, w, h - 4);

        //绘制干扰线
        Random random = new Random();
        g2.setColor(getRandColor(160, 200));// 设置线条的颜色
        for (int i = 0; i < 20; i++) {
            int x = random.nextInt(w - 1);
            int y = random.nextInt(h - 1);
            int xl = random.nextInt(6) + 1;
            int yl = random.nextInt(12) + 1;
            g2.drawLine(x, y, x + xl + 40, y + yl + 20);
        }

        // 添加噪点
        float yawpRate = 0.05f;// 噪声率
        int area = (int) (yawpRate * w * h);
        for (int i = 0; i < area; i++) {
            int x = random.nextInt(w);
            int y = random.nextInt(h);
            int rgb = getRandomIntColor();
            image.setRGB(x, y, rgb);
        }

        shear(g2, w, h, c);// 使图片扭曲

        g2.setColor(getRandColor(100, 160));
        int fontSize = h - 4;
        Font font = new Font("Algerian", Font.ITALIC, fontSize);
        g2.setFont(font);
        char[] chars = code.toCharArray();
        for (int i = 0; i < verifySize; i++) {
            AffineTransform affine = new AffineTransform();
            affine.setToRotation(Math.PI / 4 * rand.nextDouble() * (rand.nextBoolean() ? 1 : -1), (w / verifySize) * i + fontSize / 2, h / 2);
            g2.setTransform(affine);
            g2.drawChars(chars, i, 1, ((w - 10) / verifySize) * i + 5, h / 2 + fontSize / 2 - 10);
        }

        g2.dispose();
        ImageIO.write(image, "jpg", os);
    }

    private static Color getRandColor(int fc, int bc) {
        if (fc > 255)
            fc = 255;
        if (bc > 255)
            bc = 255;
        int r = fc + random.nextInt(bc - fc);
        int g = fc + random.nextInt(bc - fc);
        int b = fc + random.nextInt(bc - fc);
        return new Color(r, g, b);
    }

    private static int getRandomIntColor() {
        int[] rgb = getRandomRgb();
        int color = 0;
        for (int c : rgb) {
            color = color << 8;
            color = color | c;
        }
        return color;
    }

    private static int[] getRandomRgb() {
        int[] rgb = new int[3];
        for (int i = 0; i < 3; i++) {
            rgb[i] = random.nextInt(255);
        }
        return rgb;
    }

    private static void shear(Graphics g, int w1, int h1, Color color) {
        shearX(g, w1, h1, color);
        shearY(g, w1, h1, color);
    }

    private static void shearX(Graphics g, int w1, int h1, Color color) {

        int period = random.nextInt(2);

        boolean borderGap = true;
        int frames = 1;
        int phase = random.nextInt(2);

        for (int i = 0; i < h1; i++) {
            double d = (double) (period >> 1)
                    * Math.sin((double) i / (double) period
                    + (6.2831853071795862D * (double) phase)
                    / (double) frames);
            g.copyArea(0, i, w1, 1, (int) d, 0);
            if (borderGap) {
                g.setColor(color);
                g.drawLine((int) d, i, 0, i);
                g.drawLine((int) d + w1, i, w1, i);
            }
        }

    }

    private static void shearY(Graphics g, int w1, int h1, Color color) {

        int period = random.nextInt(40) + 10; // 50;

        boolean borderGap = true;
        int frames = 20;
        int phase = 7;
        for (int i = 0; i < w1; i++) {
            double d = (double) (period >> 1)
                    * Math.sin((double) i / (double) period
                    + (6.2831853071795862D * (double) phase)
                    / (double) frames);
            g.copyArea(i, 0, 1, h1, 0, (int) d);
            if (borderGap) {
                g.setColor(color);
                g.drawLine(i, (int) d, i, 0);
                g.drawLine(i, (int) d + h1, i, h1);
            }
        }
    }
}

7、CustomException 自定义异常处理类

import com.common.enums.ResultEnum;
import lombok.Data;
import lombok.EqualsAndHashCode;

/**
 * 自定义异常
 */
@Data
@EqualsAndHashCode(callSuper = false)
public class CustomException extends RuntimeException {

    /**
     * 状态码
     */
    private final Integer code;

    /**
     * 方法名称
     */
    private final String method;


    /**
     * 自定义异常
     *
     * @param resultEnum 返回枚举对象
     * @param method     方法
     */
    public CustomException(ResultEnum resultEnum, String method) {
        super(resultEnum.getMsg());
        this.code = resultEnum.getCode();
        this.method = method;
    }

    /**
     * @param code    状态码
     * @param message 错误信息
     * @param method  方法
     */
    public CustomException(Integer code, String message, String method) {
        super(message);
        this.code = code;
        this.method = method;
    }
}

8、ResultVo 固定返回格式

import lombok.Data;

/**
 * 固定返回格式
 */
@Data
public class ResultVo {

    /**
     * 错误码
     */
    private Integer code;

    /**
     * 提示信息
     */
    private String message;

    /**
     * 具体的内容
     */
    private Object data;
}

最后

源码

2021-05-13 更新

1、

JWTUtil 中将token有效时长改为30分钟(一开始为了方便测试,写的是一分钟,redis也是一分钟,后面正式开始使用redis改为30分钟,这个忘记改了,导致每次请求经常 JWTUtil.verify 出现异常,我就很奇怪,后面才发现是过期时间忘记改了。。)

2、JWTFilterisAccessAllowed 方法:

// 将下面这段代码,
/*
Throwable cause = e.getCause();
if (cause!=null && cause instanceof TokenExpiredException){
     String result = refreshToken(request, response);
     if (result.equals("success")){
         return true;
     }else {
         Boolean flag = refreshTokenApp(request,response);
         return flag;
     }
 }*/
 // 改为:
 return refreshTokenApp(request,response);

3、JWTFilter 中 refreshTokenApp方法:

//将这两句:
// JWTToken jwtToken = new JWTToken(token,loginType);
// httpServletResponse.setHeader("Authorization", token);

//改为:(粗心了,参数不小心写的和 refreshToken 中的一样。。其实是因为下面这一段代码是直接复制的 refreshToken 中的。。。)
JWTToken jwtToken = new JWTToken(newToken,loginType);
httpServletResponse.setHeader("Authorization", newToken);
  • 19
    点赞
  • 46
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 9
    评论
Spring Boot是一个开源的Java框架,用于构建独立的、可执行的、生产级的Spring应用程序。它极大地简化了Spring应用程序的搭建和部署过程,提供了一整套开箱即用的特性和插件,极大地提高了开发效率。 Shiro是一个强大且灵活的开源Java安全框架,提供了身份验证、授权、加密和会话管理等功能,用于保护应用程序的安全。它采用插件化的设计,支持与Spring等常用框架的无缝集成,使开发者能够轻松地在应用程序中添加安全功能。 JWT(JSON Web Token)是一种用于在客户和服务之间传输安全信息的开放标准。它使用JSON格式对信息进行包装,并使用数字签名进行验证,确保信息的完整性和安全性。JWT具有无状态性、可扩展性和灵活性的特点,适用于多种应用场景,例如身份验证和授权。 Redis是一个开源的、高性能的、支持多种数据结构的内存数据库,同时也可以持久化到磁盘中。它主要用于缓存、消息队列、会话管理等场景,为应用程序提供高速、可靠的数据访问服务。Redis支持丰富的数据类型,并提供了强大的操作命令,使开发者能够灵活地处理各种数据需求。 综上所述,Spring Boot结合ShiroJWTRedis可以构建一个安全、高性能的Java应用程序。Shiro提供了强大的安全功能,包括身份验证和授权,保护应用程序的安全;JWT用于安全传输信息,确保信息的完整性和安全性;Redis作为缓存和持久化数据库,提供了高速、可靠的数据访问服务。通过使用这些技术,开发者能够快速、高效地构建出符合安全和性能需求的应用程序。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 9
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

符华-

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值