基于spring boot的RBAC超详细实现

基于spring boot + mybatis + jwt + shiro + redis + postgresql的RBAC实现

码云源码地址:https://gitee.com/loveleaveslove/rbac-demo.git


一、搭建数据库

1、用户表

-- auto-generated definition
create table t_user
(
    id       bigint       not null
        constraint "T_USER_pkey"
            primary key,
    username varchar(64)  not null,
    password varchar(128) not null,
    name     varchar(64),
    roleid   bigint       not null,
    age      integer,
    icon     varchar(128),
    sex      varchar(2),
    phone    varchar(11),
    email    varchar(64),
    area     varchar(128)
);
comment on table t_user is '用户表';
comment on column t_user.id is '用户ID';
comment on column t_user.username is '用户名';
comment on column t_user.password is '用户密码';
comment on column t_user.roleid is '角色ID';
alter table t_user  owner to postgres;
字段 类型 备注
id serial4 用户编号
username varchar 用户名
passwords varchar 用户密码
roleid int4 用户权限编号

2、角色表(由于是简单实现其功能,所以此处简单代替)

create table t_role
(
    id       bigint not null
        constraint "T_ROLE_pkey"
            primary key,
    name     varchar(64),
    auths    varchar(128),
    parentid bigint
);
comment on table t_role is '角色表';
comment on column t_role.id is '角色ID ';
comment on column t_role.name is '角色编号';
comment on column t_role.auths is '权限ID';
comment on column t_role.parentid is '父级角色ID';
alter table t_role owner to postgres;
字段 类型 备注
id serial4 角色编号
rolename varchar 角色名称
auths varchar 角色描述
parentid Integer 父级角色ID

二、pom.xml中导入依赖



    4.0.0
    
        org.springframework.boot
        spring-boot-starter-parent
        2.3.7.RELEASE
         
    
    com.leaves.auth
    rbac-demo
    0.0.1-SNAPSHOT
    rbac-demo
    Demo project for Spring Boot
    
        1.8
    
    
        
        
            org.springframework.boot
            spring-boot-starter-web
        
        
        
            org.springframework.boot
            spring-boot-starter-data-redis
        
        
        
            org.springframework.boot
            spring-boot-starter-jdbc
        
        
        
            org.mybatis.spring.boot
            mybatis-spring-boot-starter
            2.1.4
        
        
        
            org.postgresql
            postgresql
            runtime
        
        
        
            org.projectlombok
            lombok
            true
        
        
        
            com.alibaba
            druid
            1.1.21
        
        
        
            redis.clients
            jedis
            2.9.0
        
        
        
            org.crazycake
            shiro-redis
            3.2.3
        
        
        
            org.apache.shiro
            shiro-spring
            1.3.2
        
        
            org.apache.shiro
            shiro-ehcache
            1.2.5
        
        
        
            com.auth0
            java-jwt
            3.5.0
        
        
        
            org.slf4j
            slf4j-api
            1.7.25
        
        
        
            commons-fileupload
            commons-fileupload
            1.3.1
        
        
            commons-io
            commons-io
            1.4
        
        
        
            tk.mybatis
            mapper-spring-boot-starter
            2.1.5
        
        
        
            com.github.pagehelper
            pagehelper-spring-boot-starter
            1.2.5
        
        
        
        
            cn.hutool
            hutool-all
            5.4.3
        
        
            org.springframework.boot
            spring-boot-starter-test
            test
            
                
                    org.junit.vintage
                    junit-vintage-engine
                
            
        
    
​
    
        RBAC-DEMO
        
            
                org.springframework.boot
                spring-boot-maven-plugin
                
                    
                        
                            org.projectlombok
                            lombok
                        
                    
                
            
        
    

三、配置application.yml

#端口号
server:
  port: 8088
spring:
  #数据库连接配置
  datasource:
    driver-class-name: org.postgresql.Driver
    type: com.alibaba.druid.pool.DruidDataSource
    url: jdbc:postgresql://localhost:5432/rbac?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&socketTimeout=30000
    username: rbac
    password: 123456
    jackson:
      date-format: yyyy-MM-dd HH:mm:ss
      time-zone: GMT+8
  #允许运行程序中存在多个main函数
  main:
    allow-bean-definition-overriding: true
  #Redis
  redis:
    host: localhost
    port: 6379
    database: 0
    jedis:
      pool:
        max-active: 8
        max-idle: 8
        max-wait: -1
        min-idle: 0
    timeout: 3000ms
    #文件上传配置
    autoconfigure:
      exclude: org.springframework.boot.autoconfigure.web.servlet.MultipartAutoConfiguration
#mybatis配置
mybatis:
  type-aliases-package: com.leaves.auth.entity
  mapper-locations: classpath*:/mapper/*.xml
#日志等级
logging:
  level:
    com:
      leaves:
        auth:
          mapper: debug
  config: classpath:logback-spring.xml
#文件上传保存地址
upload:
  local:
    path: D:/uploadFile/file

四、工具类redisUitls包

1、Constant

/**
 * @Author: LEAVES
 * @Date: 2020年12月30日 11时06分23秒
 * @Version 1.0
 * @Description:   常量
 */
private Constant() {}
​
    /**
     * redis-OK
     */
    public static final String OK = "OK";
​
    /**
     * redis过期时间,以秒为单位,一分钟
     */
    public static final int EXRP_MINUTE = 60;
​
    /**
     * redis过期时间,以秒为单位,一小时
     */
    public static final int EXRP_HOUR = 60 * 60;
​
    /**
     * redis过期时间,以秒为单位,一天
     */
    public static final int EXRP_DAY = 60 * 60 * 24;
​
    /**
     * redis-key-前缀-shiro:cache:
     */
    public static final String PREFIX_SHIRO_CACHE = "shiro:cache:";
​
    /**
     * redis-key-前缀-shiro:access_token:
     */
    public static final String PREFIX_SHIRO_ACCESS_TOKEN = "shiro:access_token:";
​
    /**
     * redis-key-前缀-shiro:refresh_token:
     */
    public static final String PREFIX_SHIRO_REFRESH_TOKEN = "shiro:refresh_token:";
​
    /**
     * JWT-account:
     */
    public static final String ACCOUNT = "account";
​
    /**
     * JWT-currentTimeMillis:
     */
    public static final String CURRENT_TIME_MILLIS = "currentTimeMillis";
​
    /**
     * PASSWORD_MAX_LEN
     */
    public static final Integer PASSWORD_MAX_LEN = 8;

2、SerializableUtil

/**
 * @Author: LEAVES
 * @Date: 2020年12月30日 11时06分23秒
 * @Version 1.0
 * @Description:   Serializable工具(JDK)(也可以使用Protobuf自行百度)
 */
@Slf4j
public class SerializableUtil {
​
    private SerializableUtil() {}
​
    /**
     * 序列化
     */
    public static byte[] serializable(Object object) {
        ByteArrayOutputStream baos = null;
        ObjectOutputStream oos = null;
        try {
            baos = new ByteArrayOutputStream();
            oos = new ObjectOutputStream(baos);
            oos.writeObject(object);
            return baos.toByteArray();
        } catch (IOException e) {
            log.error("SerializableUtil工具类序列化出现IOException异常:{}", e.getMessage());
            throw new CustomException("SerializableUtil工具类序列化出现IOException异常:" + e.getMessage());
        } finally {
            try {
                if (oos != null) {
                    oos.close();
                }
                if (baos != null) {
                    baos.close();
                }
            } catch (IOException e) {
                log.error("SerializableUtil工具类反序列化出现IOException异常:{}", e.getMessage());
                throw new CustomException("SerializableUtil工具类反序列化出现IOException异常:" + e.getMessage());
            }
        }
    }
​
    /**
     * 反序列化
     */
    public static Object unserializable(byte[] bytes) {
        ByteArrayInputStream bais = null;
        ObjectInputStream ois = null;
        try {
            bais = new ByteArrayInputStream(bytes);
            ois = new ObjectInputStream(bais);
            return ois.readObject();
        } catch (ClassNotFoundException e) {
            log.error("SerializableUtil工具类反序列化出现ClassNotFoundException异常:{}", e.getMessage());
            throw new CustomException("SerializableUtil工具类反序列化出现ClassNotFoundException异常:" + e.getMessage());
        } catch (IOException e) {
            log.error("SerializableUtil工具类反序列化出现IOException异常:{}", e.getMessage());
            throw new CustomException("SerializableUtil工具类反序列化出现IOException异常:" + e.getMessage());
        } finally {
            try {
                if (ois != null) {
                    ois.close();
                }
                if (bais != null) {
                    bais.close();
                }
            } catch (IOException e) {
                log.error("SerializableUtil工具类反序列化出现IOException异常:{}", e.getMessage());
                throw new CustomException("SerializableUtil工具类反序列化出现IOException异常:" + e.getMessage());
            }
        }
    }
​
}

3、StringUtil

/**
 * @Author: LEAVES
 * @Date: 2020年12月30日 11时06分23秒
 * @Version 1.0
 * @Description:   String工具
 */
public class StringUtil {
​
    private StringUtil() {}
​
    /**
     * 定义下划线
     */
    private static final char UNDERLINE = '_';
​
    /**
     * String为空判断(不允许空格)
     * @param str
     * @return
     */
    public static boolean isBlank(String str) {
        return str == null || "".equals(str.trim());
    }
​
    /**
     * String不为空判断(不允许空格)
     * @param str
     * @return
     */
    public static boolean isNotBlank(String str) {
        return !isBlank(str);
    }
​
    /**
     * Byte数组为空判断
     * @param bytes
     * @return boolean
     */
    public static boolean isNull(byte[] bytes) {
        // 根据byte数组长度为0判断
        return bytes == null || bytes.length == 0;
    }
​
    /**
     * Byte数组不为空判断
     * @param bytes
     * @return boolean
     */
    public static boolean isNotNull(byte[] bytes) {
        return !isNull(bytes);
    }
​
    /**
     * 驼峰转下划线工具
     * @param param
     * @return java.lang.String
     */
    public static String camelToUnderline(String param) {
        if (isNotBlank(param)) {
            int len = param.length();
            StringBuilder sb = new StringBuilder(len);
            for (int i = 0; i < len; i++) {
                char c = param.charAt(i);
                if (Character.isUpperCase(c)) {
                    sb.append(UNDERLINE);
                    sb.append(Character.toLowerCase(c));
                } else {
                    sb.append(c);
                }
            }
            return sb.toString();
        } else {
            return "";
        }
    }
​
    /**
     * 下划线转驼峰工具
     * @param param
     * @return java.lang.String
     */
    public static String underlineToCamel(String param) {
        if (isNotBlank(param)) {
            int len = param.length();
            StringBuilder sb = new StringBuilder(len);
            for (int i = 0; i < len; i++) {
                char c = param.charAt(i);
                if (c == 95) {
                    i++;
                    if (i < len) {
                        sb.append(Character.toUpperCase(param.charAt(i)));
                    }
                } else {
                    sb.append(c);
                }
            }
            return sb.toString();
        } else {
            return "";
        }
    }
​
    /**
     * 在字符串两周添加''
     * @param param
     * @return java.lang.String
     */
    public static String addSingleQuotes(String param) {
        return "\'" + param + "\'";
    }
}

五、配置类redis

1、JedisConfig

/**
 * @Author: LEAVES
 * @Date: 2020年11月02日 15时22分18秒
 * @Version 1.0
 */
@Configuration
public class JedisConfig {
​
    @Value("${spring.redis.host}")
    private String host;
    @Value("${spring.redis.port}")
    private int port ;
    @Bean
    public Jedis getJedis(){
       return  new Jedis("127.0.0.1",6379);
    }
//    封装jedispool对象(将配置对象注入其中)
    @Bean
    public JedisPool jedisPool(@Qualifier("jedisPoolConfig") JedisPoolConfig jedisPoolConfig ){
        JedisPool pool = new JedisPool(jedisPoolConfig,host,port);
        return pool;
    }
//    封装jedispool配置对象
    @Bean("jedisPoolConfig")
    public JedisPoolConfig jedisPoolConfig(){
        JedisPoolConfig poolConfig = new JedisPoolConfig();
        poolConfig.setMaxIdle(1);
        poolConfig.setMaxTotal(10);
        return poolConfig;
    }
}

2、JedisUtil

/**
 * 封装常见的jedis操作接口
 * @Author: LEAVES
 * @Date: 2020年11月02日 15时22分18秒
 * @Version 1.0
 */

@Component
public class JedisUtil {
    /**
     * 静态注入JedisPool连接池
     * 本来是正常注入JedisUtil,可以在Controller和Service层使用,但是重写Shiro的CustomCache无法注入JedisUtil
     * 现在改为静态注入JedisPool连接池,JedisUtil直接调用静态方法即可
     * https://blog.csdn.net/W_Z_W_888/article/details/79979103
     */
    private static JedisPool jedisPool;

    @Autowired
    public void setJedisPool(JedisPool jedisPool) {
        JedisUtil.jedisPool = jedisPool;
    }

    /**
     * 获取Jedis实例
     * @param
     * @return redis.clients.jedis.Jedis
     * @Author: LEAVES
     * @Date: 2020年11月02日 15时22分18秒
     */
    public static synchronized Jedis getJedis() {
        try {
            if (jedisPool != null) {
                return jedisPool.getResource();
            } else {
                return null;
            }
        } catch (Exception e) {
            throw new CustomException("获取Jedis资源异常:" + e.getMessage());
        }
    }

    /**
     * 释放Jedis资源
     * @param
     * @return void
     * @Author: LEAVES
     * @Date: 2020年11月02日 15时22分18秒
     */
    public static void closePool() {
        try {
            jedisPool.close();
        } catch (Exception e) {
            throw new CustomException("释放Jedis资源异常:" + e.getMessage());
        }
    }

    /**
     * 获取redis键值-object
     * @param key
     * @return java.lang.Object
     * @Author: LEAVES
     * @Date: 2020年11月02日 15时22分18秒
     */
    public static Object getObject(String key) {
        try (Jedis jedis = jedisPool.getResource()) {
            byte[] bytes = jedis.get(key.getBytes());
            if (StringUtil.isNotNull(bytes)) {
                return SerializableUtil.unserializable(bytes);
            }
        } catch (Exception e) {
            throw new CustomException("获取Redis键值getObject方法异常:key=" + key + " cause=" + e.getMessage());
        }
        return null;
    }

    /**
     * 设置redis键值-object
     * @param key
     * @param value
     * @return java.lang.String
     * @Author: LEAVES
     * @Date: 2020年11月02日 15时22分18秒
     */
    public static String setObject(String key, Object value) {
        try (Jedis jedis = jedisPool.getResource()) {
            return jedis.set(key.getBytes(), SerializableUtil.serializable(value));
        } catch (Exception e) {
            throw new CustomException("设置Redis键值setObject方法异常:key=" + key + " value=" + value + " cause=" + e.getMessage());
        }
    }

    /**
     * 设置redis键值-object-expiretime
     * @param key
     * @param value
     * @param expiretime
     * @return java.lang.String
     * @Author: LEAVES
     * @Date: 2020年11月02日 15时22分18秒
     */
    public static String setObject(String key, Object value, int expiretime) {
        String result;
        try (Jedis jedis = jedisPool.getResource()) {
            result = jedis.set(key.getBytes(), SerializableUtil.serializable(value));
            if (Constant.OK.equals(result)) {
                jedis.expire(key.getBytes(), expiretime);
            }
            return result;
        } catch (Exception e) {
            throw new CustomException("设置Redis键值setObject方法异常:key=" + key + " value=" + value + " cause=" + e.getMessage());
        }
    }

    /**
     * 获取redis键值-Json
     * @param key
     * @return java.lang.Object
     * @Author: LEAVES
     * @Date: 2020年11月02日 15时22分18秒
     */
    public static String getJson(String key) {
        try (Jedis jedis = jedisPool.getResource()) {
            return jedis.get(key);
        } catch (Exception e) {
            throw new CustomException("获取Redis键值getJson方法异常:key=" + key + " cause=" + e.getMessage());
        }
    }

    /**
     * 设置redis键值-Json
     * @param key
     * @param value
     * @return java.lang.String
     * @Author: LEAVES
     * @Date: 2020年11月02日 15时22分18秒
     */
    public static String setJson(String key, String value) {
        try (Jedis jedis = jedisPool.getResource()) {
            return jedis.set(key, value);
        } catch (Exception e) {
            throw new CustomException("设置Redis键值setJson方法异常:key=" + key + " value=" + value + " cause=" + e.getMessage());
        }
    }

    /**
     * 设置redis键值-Json-expiretime
     * @param key
     * @param value
     * @param expiretime
     * @return java.lang.String
     * @Author: LEAVES
     * @Date: 2020年11月02日 15时22分18秒
     */
    public static String setJson(String key, String value, int expiretime) {
        String result;
        try (Jedis jedis = jedisPool.getResource()) {
            result = jedis.set(key, value);
            if (Constant.OK.equals(result)) {
                jedis.expire(key, expiretime);
            }
            return result;
        } catch (Exception e) {
            throw new CustomException("设置Redis键值setJson方法异常:key=" + key + " value=" + value + " cause=" + e.getMessage());
        }
    }

    /**
     * 删除key
     * @param key
     * @return java.lang.Long
     * @Author: LEAVES
     * @Date: 2020年11月02日 15时22分18秒
     */
    public static Long delKey(String key) {
        try (Jedis jedis = jedisPool.getResource()) {
            return jedis.del(key.getBytes());
        } catch (Exception e) {
            throw new CustomException("删除Redis的键delKey方法异常:key=" + key + " cause=" + e.getMessage());
        }
    }

    /**
     * key是否存在
     * @param key
     * @return java.lang.Boolean
     * @Author: LEAVES
     * @Date: 2020年11月02日 15时22分18秒
     */
    public static Boolean exists(String key) {
        try (Jedis jedis = jedisPool.getResource()) {
            return jedis.exists(key.getBytes());
        } catch (Exception e) {
            throw new CustomException("查询Redis的键是否存在exists方法异常:key=" + key + " cause=" + e.getMessage());
        }
    }

    /**
     * 模糊查询获取key集合(keys的速度非常快,但在一个大的数据库中使用它仍然可能造成性能问题,生产不推荐使用)
     * @param key
     * @return java.util.Set
     * @Author: LEAVES
     * @Date: 2020年11月02日 15时22分18秒
     */
    public static Set keysS(String key) {
        try (Jedis jedis = jedisPool.getResource()) {
            return jedis.keys(key);
        } catch (Exception e) {
            throw new CustomException("模糊查询Redis的键集合keysS方法异常:key=" + key + " cause=" + e.getMessage());
        }
    }

    /**
     * 模糊查询获取key集合(keys的速度非常快,但在一个大的数据库中使用它仍然可能造成性能问题,生产不推荐使用)
     * @param key
     * @return java.util.Set
     * @Author: LEAVES
     * @Date: 2020年11月02日 15时22分18秒
     */
    public static Set keysB(String key) {
        try (Jedis jedis = jedisPool.getResource()) {
            return jedis.keys(key.getBytes());
        } catch (Exception e) {
            throw new CustomException("模糊查询Redis的键集合keysB方法异常:key=" + key + " cause=" + e.getMessage());
        }
    }

    /**
     * 获取过期剩余时间
     * @param key
     * @return java.lang.String
     * @Author: LEAVES
     * @Date: 2020年11月02日 15时22分18秒
     */
    public static Long ttl(String key) {
        Long result = -2L;
        try (Jedis jedis = jedisPool.getResource()) {
            result = jedis.ttl(key);
            return result;
        } catch (Exception e) {
            throw new CustomException("获取Redis键过期剩余时间ttl方法异常:key=" + key + " cause=" + e.getMessage());
        }
    }
}

3、RedisOperator

/**
 * @Author: LEAVES
 * @Date: 2020年11月02日 15时22分18秒
 * @Version 1.0
 * @Description: 使用redisTemplate的操作实现类
 */
@Component
public class RedisOperator {

    @Resource
    private StringRedisTemplate redisTemplate;

    // Key(键),简单的key-value操作

    /**
     * 实现命令:TTL key,以秒为单位,返回给定 key的剩余生存时间(TTL, time to live)。
     *
     * @param key
     * @return
     */
    public long ttl(String key) {
        return redisTemplate.getExpire(key);
    }

    /**
     * 实现命令:expire 设置过期时间,单位秒
     *
     * @param key
     * @return
     */
    public void expire(String key, long timeout) {
        redisTemplate.expire(key, timeout, TimeUnit.SECONDS);
    }

    /**
     * 实现命令:INCR key,增加key一次
     *
     * @param key
     * @return
     */
    public long incr(String key, long delta) {
        return redisTemplate.opsForValue().increment(key, delta);
    }

    /**
     * 实现命令:KEYS pattern,查找所有符合给定模式 pattern的 key
     */
    public Set keys(String pattern) {
        return redisTemplate.keys(pattern);
    }

    /**
     * 实现命令:DEL key,删除一个key
     *
     * @param key
     */
    public void del(String key) {
        redisTemplate.delete(key);
    }

    // String(字符串)

    /**
     * 实现命令:SET key value,设置一个key-value(将字符串值 value关联到 key)
     *
     * @param key
     * @param value
     */
    public void set(String key, String value) {
        redisTemplate.opsForValue().set(key, value);
    }

    /**
     * 实现命令:SET key value EX seconds,设置key-value和超时时间(秒)
     *
     * @param key
     * @param value
     * @param timeout
     *            (以秒为单位)
     */
    public void set(String key, String value, long timeout) {
        redisTemplate.opsForValue().set(key, value, timeout, TimeUnit.SECONDS);
    }

    /**
     * 实现命令:GET key,返回 key所关联的字符串值。
     *
     * @param key
     * @return value
     */
    public String get(String key) {
        return (String)redisTemplate.opsForValue().get(key);
    }

    // Hash(哈希表)

    /**
     * 实现命令:HSET key field value,将哈希表 key中的域 field的值设为 value
     *
     * @param key
     * @param field
     * @param value
     */
    public void hset(String key, String field, Object value) {
        redisTemplate.opsForHash().put(key, field, value);
    }

    /**
     * 实现命令:HGET key field,返回哈希表 key中给定域 field的值
     *
     * @param key
     * @param field
     * @return
     */
    public String hget(String key, String field) {
        return (String) redisTemplate.opsForHash().get(key, field);
    }

    /**
     * 实现命令:HDEL key field [field ...],删除哈希表 key 中的一个或多个指定域,不存在的域将被忽略。
     *
     * @param key
     * @param fields
     */
    public void hdel(String key, Object... fields) {
        redisTemplate.opsForHash().delete(key, fields);
    }

    /**
     * 实现命令:HGETALL key,返回哈希表 key中,所有的域和值。
     *
     * @param key
     * @return
     */
    public Map hgetall(String key) {
        return redisTemplate.opsForHash().entries(key);
    }

    // List(列表)

    /**
     * 实现命令:LPUSH key value,将一个值 value插入到列表 key的表头
     *
     * @param key
     * @param value
     * @return 执行 LPUSH命令后,列表的长度。
     */
    public long lpush(String key, String value) {
        return redisTemplate.opsForList().leftPush(key, value);
    }

    /**
     * 实现命令:LPOP key,移除并返回列表 key的头元素。
     *
     * @param key
     * @return 列表key的头元素。
     */
    public String lpop(String key) {
        return (String)redisTemplate.opsForList().leftPop(key);
    }

    /**
     * 实现命令:RPUSH key value,将一个值 value插入到列表 key的表尾(最右边)。
     *
     * @param key
     * @param value
     * @return 执行 LPUSH命令后,列表的长度。
     */
    public long rpush(String key, String value) {
        return redisTemplate.opsForList().rightPush(key, value);
    }

    //zset

    /**
     * 实现命令:zset存入值
     * @param key
     * @param value
     * @param score
     */
    public void zSet(String key, String value, double score){
        redisTemplate.opsForZSet().add(key,value,score);
    }

    public Set zGet(String key, long start, long end){
        return  redisTemplate.opsForZSet().range(key,start,end);
    }
}

六、统一异常处理Exception

1、CustomException

/**
 * @Author: LEAVES
 * @Date: 2020年12月30日 11时06分23秒
 * @Version 1.0
 * @Description:   自定义异常(CustomException)
 */
public class CustomException extends AuthenticationException {

    public CustomException(String msg){
        super(msg);
    }

    public CustomException() {
        super();
    }
}

2、DefaultExceptionHandler

/**
 * @Author: LEAVES
 * @Date: 2020年12月30日 11时06分23秒
 * @Version 1.0
 * @Description:   自定义异常处理器
 */
@Slf4j
@ControllerAdvice  //不指定包默认加了@Controller和@RestController都能控制
public class DefaultExceptionHandler {

    /**
     * @param  ex
     * @Description: 运行时异常
     */
    @ExceptionHandler(com.leaves.auth.exception.CustomException.class)
    public ResponseData CustomExceptionHandler(com.leaves.auth.exception.CustomException ex) {
        log.error(ex.getMessage(), ex);
        ResponseData responseData=new ResponseData<>();
        responseData.setCode(400);
        responseData.setMsg(ex.getMessage());
        return responseData;
    }

    /**
     * @param  ex
     * @Description: 权限认证异常
     */
    @ExceptionHandler(UnauthorizedException.class)
    @ResponseBody
    public ResponseData unauthorizedExceptionHandler(Exception ex) {
        log.error(ex.getMessage(), ex);
        return ResponseDataUtil.fail("对不起,您没有相关权限!");
    }

    @ExceptionHandler(UnauthenticatedException.class)
    @ResponseBody
    public ResponseData unauthenticatedException(UnauthenticatedException ex) {
        log.error(ex.getMessage(), ex);
        return ResponseDataUtil.fail("对不起,您未登录!");
    }

    /**
     *
     * @param ex
     * @return
     */
    @ExceptionHandler(AuthorizationException.class)
    @ResponseBody
    public ResponseData authorizationException(AuthorizationException ex) {
        log.error(ex.getMessage(), ex);
        return ResponseDataUtil.fail("无效token,请重新登录!");
    }

    /**
     * 空指针异常
     * @param ex
     * @return
     */
    @ExceptionHandler(NullPointerException.class)
    @ResponseBody
    public ResponseData nullPointerException(NullPointerException ex) {
        log.error(ex.getMessage(), ex);
        return ResponseDataUtil.fail(500,"空指针异常!");
    }

    /**
     * 认证异常
     * @param ex
     * @return
     */
    @ExceptionHandler(AuthenticationException.class)
    @ResponseBody
    public ResponseData authenticationException(AuthenticationException ex) {
        log.error(ex.getMessage(), ex);
        return ResponseDataUtil.fail("token为空,请重新登录!");
    }

    /**
     * 文件上传异常
     * @param ex
     * @return
     */
    @ExceptionHandler(IOException.class)
    @ResponseBody
    public ResponseData ioException(IOException ex) {
        log.error(ex.getMessage(), ex);
        return ResponseDataUtil.fail("文件上传异常!");
    }

    /**
     * 异常统一自定义处理
     */
    @ExceptionHandler({MyException.class})
    @ResponseBody
    public ResponseData MyException(MyException e) {
        return ResponseDataUtil.fail(500,e.getMessage());
    }

    /**
     * 异常统一处理(最后的异常处理)
     */
    @ExceptionHandler({Exception.class})
    @ResponseStatus(HttpStatus.OK)
    @ResponseBody
    public ResponseData allException(Exception e) {
        log.info(e.getMessage());
        return ResponseDataUtil.fail(500,"系统异常!");
    }
}

3、MyException

/**
 * @Author: LEAVES
 * @Date: 2020年12月30日 11时06分23秒
 * @Version 1.0
 * @Description:   自定义异常
 */
@Data
public class MyException extends RuntimeException {

    /**
     * 自定义返回状态码
     */
    private Integer code;


    /**
     * 返回自定义的状态码和异常描述
     * @param code
     * @param message
     */
    public MyException(Integer code, String message) {
        super(message);
        this.code = code;
    }

    /**
     * 只返回异常信息(默认返回500)
     * @param message
     */
    public MyException(String message) {

        super(message);
    }
}

七、统一返回格式response

1、ResponseData

/**
 * @Author: LEAVES
 * @Date: 2020年12月30日 11时06分23秒
 * @Version 1.0
 * @Description:    统一返回格式
 */
@Data
public class ResponseData {

    /**
     * 统一返回码
     */
    public Integer code;

    /**
     * 统一消息
     */
    public String msg;

    /**
     * 结果对象
     */
    @JsonInclude(JsonInclude.Include.NON_NULL)
    public T data;

}

2、ResponseDataUtil

/**
 * @Author: LEAVES
 * @Date: 2020年12月30日 11时06分23秒
 * @Version 1.0
 * @Description:    统一返回格式封装
 */
@Slf4j
public class ResponseDataUtil {

    /**
     * 返回成功的描述      状态码、说明
     * @param msg
     * @return
     */
    public static ResponseData success(String msg){
        ResponseData responseData = new ResponseData();
        responseData.setCode(200);
        responseData.setMsg(msg);
        return responseData;
    }

    /**
     * 返回成功的描述      状态码、说明
     * @param code
     * @param msg
     * @return
     */
    public static  ResponseData success(Integer code, String msg){
        ResponseData responseData = new ResponseData();
        responseData.setCode(code);
        responseData.setMsg(msg);
        return responseData;
    }

    /**
     * 返回成功的描述      状态码、说明、数据
     * @param msg
     * @param data
     * @param 
     * @return
     */
    public static  ResponseData success(String msg, T data){
        ResponseData responseData = new ResponseData();
        responseData.setCode(200);
        responseData.setMsg(msg);
        responseData.setData(data);
        return responseData;
    }

    /**
     * 返回成功的描述      状态码、说明、令牌
     * @param code
     * @param msg
     * @param data
     * @param 
     * @return
     */
    public static  ResponseData success(Integer code, String msg, T data){
        ResponseData responseData = new ResponseData();
        responseData.setCode(code);
        responseData.setMsg(msg);
        responseData.setData(data);
        return responseData;
    }

    /**
     * 返回失败的描述  状态码
     * @param msg
     * @return
     */
    public static ResponseData fail(String msg){
        ResponseData responseData=new ResponseData();
        responseData.setCode(405);
        responseData.setMsg(msg);
        return responseData;
    }

    /**
     * 返回失败的描述  状态码、说明
     * @param code
     * @param msg
     * @return
     */
    public static ResponseData fail(Integer code, String msg){
        ResponseData responseData=new ResponseData();
        responseData.setCode(code);
        responseData.setMsg(msg);
        return responseData;
    }

    /**
     * 返回失败的描述  状态码、说明
     * @param code
     * @param msg
     * @param data
     * @param 
     * @return
     */
    public static  ResponseData fail(Integer code, String msg, T data){
        ResponseData responseData=new ResponseData();
        responseData.setCode(code);
        responseData.setMsg(msg);
        responseData.setData(data);
        return responseData;
    }
}

八、工具类

1、IsNotEmptyUtil

* @Author: LEAVES
 * @Date: 2020年12月30日 14时29分15秒
 * @Version 1.0
 * @Description:    非空判断
 */
public class IsNotEmptyUtil {
    public static boolean isEmpty(Object object) {
        if (object == null) {
            return (true);
        }
        if ("".equals(object)) {
            return (true);
        }
        if ("null".equals(object)) {
            return (true);
        }
        return (false);
    }

    public static boolean isNotEmpty(Object object) {
        if (object != null && !object.equals("") && !object.equals("null")) {
            return (true);
        }
        return (false);
    }
}

2、MD5

/**
 * @Author: LEAVES
 * @Date: 2020年12月30日 17时55分06秒
 * @Version 1.0
 * @Description:     MD5加密
 */
public class MD5Util {

    /***
     * MD5加码 生成32位md5码
     */
    public static String string2MD5(String inStr) {
        MessageDigest md5 = null;
        try {
            md5 = MessageDigest.getInstance("MD5");
        } catch (Exception e) {
            System.out.println(e.toString());
            e.printStackTrace();
            return "";
        }
        char[] charArray = inStr.toCharArray();
        byte[] byteArray = new byte[charArray.length];

        for (int i = 0; i < charArray.length; i++) {
            byteArray[i] = (byte) charArray[i];
        }
        byte[] md5Bytes = md5.digest(byteArray);
        StringBuffer hexValue = new StringBuffer();
        for (int i = 0; i < md5Bytes.length; i++) {
            int val = ((int) md5Bytes[i]) & 0xff;
            if (val < 16){
                hexValue.append("0");
            }
            hexValue.append(Integer.toHexString(val));
        }
        return hexValue.toString();

    }

    /**
     * 加密解密算法 执行一次加密,两次解密
     */
    public static String convertMD5(String inStr) {

        char[] a = inStr.toCharArray();
        for (int i = 0; i < a.length; i++) {
            a[i] = (char) (a[i] ^ 't');
        }
        String s = new String(a);
        return s;

    }
}

3、含有NULL值属性对象转JSON时使其变成空字符串

/**
 * 处理返回值中的null值
 * @Author: LEAVES
 * @Date: 2020年12月30日 11时06分23秒
 * @Version 1.0
 */
//@EnableWebMvc
@Configuration
public class JsonConfig {

    @Bean
    public MappingJackson2HttpMessageConverter mappingJacksonHttpMessageConverter() {

        final MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();

        ObjectMapper mapper = converter.getObjectMapper();

        // 为mapper注册一个带有SerializerModifier的Factory,此modifier主要做的事情为:当序列化类型为array,list、set时,当值为空时,序列化成[]
        mapper.setSerializerFactory(mapper.getSerializerFactory().withSerializerModifier(new MyBeanSerializerModifier()));

        return converter;
    }

}
/**
 * 该类控制将null值处理成空集合还是空字符串
 * @Author: LEAVES
 * @Date: 2020年12月30日 11时06分23秒
 * @Version 1.0
 */
public class MyBeanSerializerModifier extends BeanSerializerModifier {

    //  数组类型
    private JsonSerializer _nullArrayJsonSerializer = new MyNullArrayJsonSerializer();
    // 字符串等类型
    private JsonSerializer _nullJsonSerializer = new MyNullJsonSerializer();
    // 对象类型
    private JsonSerializer _nullObjectSerializer = new MyNullObjectJsonSerializer();

    @Override
    public List changeProperties(SerializationConfig config, BeanDescription beanDesc,
                                                     List beanProperties) {
        //循环所有的beanPropertyWriter
        for (int i = 0; i < beanProperties.size(); i++) {
            BeanPropertyWriter writer = (BeanPropertyWriter) beanProperties.get(i);
            //判断字段的类型,如果是array,list,set则注册nullSerializer
            if (isArrayType(writer)) {
                //给writer注册一个自己的nullSerializer
                writer.assignNullSerializer(this._nullArrayJsonSerializer);
            } else if(isStringOrNumberType(writer)){
                writer.assignNullSerializer(this._nullJsonSerializer);
            }else{
                writer.assignNullSerializer(this._nullObjectSerializer);
            }
        }
        return beanProperties;
    }

    //判断是什么类型
    protected boolean isArrayType(BeanPropertyWriter writer) {
        Class clazz = writer.getPropertyType();
        return clazz.isArray() || clazz.equals(List.class) || clazz.equals(Set.class);
    }

    //判断是什么类型
    protected boolean isStringOrNumberType(BeanPropertyWriter writer) {
        Class clazz = writer.getPropertyType();
        return clazz.equals(String.class) || clazz.equals(Integer.class) || clazz.equals(int.class) || clazz.equals(Double.class) || clazz.equals(Short.class)
                || clazz.equals(Long.class) || clazz.equals(short.class) || clazz.equals(double.class) || clazz.equals(long.class) || clazz.equals(Date.class)
                || clazz.equals(BigDecimal.class);
    }
}
/**
 * 处理数组类型的null值
 * @Author: LEAVES
 * @Date: 2020年12月30日 11时06分23秒
 * @Version 1.0
 */
public class MyNullArrayJsonSerializer extends JsonSerializer {

    @Override
    public void serialize(Object value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonProcessingException {
        if (value == null) {
            jgen.writeStartArray();
            jgen.writeEndArray();
        }
    }
}
/**
 * 处理字符串等类型的null值
 * @Author: LEAVES
 * @Date: 2020年12月30日 11时06分23秒
 * @Version 1.0
 */
public class MyNullJsonSerializer extends JsonSerializer {

    @Override
    public void serialize(Object o, JsonGenerator jsonGenerator, SerializerProvider serializerProvider)
            throws IOException, JsonProcessingException {
        jsonGenerator.writeString("");
    }
}
/**
 * 处理对象类型的null值
 * @Author: LEAVES
 * @Date: 2020年12月30日 11时06分23秒
 * @Version 1.0
 */
public class MyNullObjectJsonSerializer extends JsonSerializer {
    @Override
    public void serialize(Object o, JsonGenerator jsonGenerator, SerializerProvider serializerProvider)
            throws IOException, JsonProcessingException {
        jsonGenerator.writeStartObject();
        jsonGenerator.writeEndObject();
    }
}

九 、JWT

1、JwtToken

/**
 * @Author: LEAVES
 * @Date: 2020年12月30日 14时25分08秒
 * @Version 1.0
 * @Description:    Authentication-Token: shiro中负责把username,password生成用于验证的token的封装类,需要自定义一个对象用来包装token。
 */
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;
    }
}

2、JwtUtil

/**
 * @Author: LEAVES
 * @Date: 2020年12月30日 14时25分08秒
 * @Version 1.0
 * @Description:
 */
@Slf4j
public class JwtUtil {

    //token过期时间     5小时
    private static final long EXPIRE_TIME = 1000 * 60 * 60 * 5;
    //redis中token过期时间   12小时
    public static final Integer REFRESH_EXPIRE_TIME = 60 * 60 * 12;
    //token密钥(自定义)
    private static final String TOKEN_SECRET = "^/zxc*123!@#$%^&*/";


    /**
     * 校验token是否正确
     * @param token token
     * @param username 用户名
     * @return 是否正确
     */
    public static boolean verify(String token, String username){
        log.info("JwtUtil==verify--->");
        try {
            log.info("JwtUtil==verify--->校验token是否正确");
            //根据密码生成JWT效验器
            Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET); //秘钥是密码则省略
            JWTVerifier verifier = JWT.require(algorithm)
                    .withClaim("username", username)
//                    .withClaim("secret",secret)  //秘钥是密码直接传入
                    .build();
            //效验TOKEN
            DecodedJWT jwt = verifier.verify(token);
            log.info("JwtUtil==verify--->jwt = "+jwt.toString());
            log.info("JwtUtil==verify--->JwtUtil验证token成功!");
            return true;
        }catch (Exception e){
            log.error("JwtUtil==verify--->JwtUtil验证token失败!");
            return false;
        }
    }

    /**
     * 获取token中的信息(包含用户名)
     * @param token
     * @return
     */
    public static String getUsername(String token) {
        log.info("JwtUtil==getUsername--->");
        if (IsNotEmptyUtil.isEmpty(token)){
            log.error("JwtUtil==getUsername--->token无效或token中未包含用户信息! ");
            throw new AuthenticationException("认证失败,token无效或token中未包含用户信息!");
        }
        try {
            DecodedJWT jwt = JWT.decode(token);
            System.out.println("token = " + jwt.getToken());
            log.info("JwtUtil==getUsername--->username = "+jwt.getClaim("username").asString());
            return jwt.getClaim("username").asString();
        } catch (JWTDecodeException e) {
            log.error("JwtUtil==getUsername--->JWTDecodeException: " + e.getMessage());
            return null;
        }
    }

    /**
     * 生成token签名
     * EXPIRE_TIME 分钟后过期
     * @param username 用户名
     * @return 加密的token
     */
    public static String sign(String username) {
        log.info("JwtUtil==sign--->");
        Map header = new HashMap<>();
        header.put("type","Jwt");
        header.put("alg","HS256");
        long currentTimeMillis = System.currentTimeMillis();
        //设置token过期时间
        Date date = new Date(currentTimeMillis + EXPIRE_TIME);
        Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);//秘钥是密码则省略
        //生成签名
        String sign = JWT.create()
                .withHeader(header)
                .withExpiresAt(date)
                .withClaim("username", username)
//                .withClaim("secret",secret)  //秘钥是密码直接传入
                .withClaim("currentTime", currentTimeMillis + EXPIRE_TIME)
                .sign(algorithm);
        log.info("JwtUtil==sign--->sign = " + sign);
        return sign;
    }

    /**
     * 获取token中用户信息
     * @param token
     * @return
     */
    public static String getAccount(String token){
        try{
            DecodedJWT decodedJWT=JWT.decode(token);
            return decodedJWT.getClaim("username").asString();

        }catch (JWTCreationException e){
            return null;
        }
    }

    /**
     * 获取token中用户密码
     * @param token
     * @return
     */
    public static String getSecret(String token){
        try{
            DecodedJWT decodedJWT=JWT.decode(token);
            return decodedJWT.getClaim("secret").asString();

        }catch (JWTCreationException e){
            return null;
        }
    }

    /**
     * 获取token的时间戳
     * @param token
     * @return
     */
    public static Long getCurrentTime(String token){
        try{
            DecodedJWT decodedJWT=JWT.decode(token);
            return decodedJWT.getClaim("currentTime").asLong();

        }catch (JWTCreationException e){
            return null;
        }
    }
}

3、JwtFilter

/**
 * 重写鉴权
 * @Author: LEAVES
 * @Date: 2020年12月30日 14时25分08秒
 * @Version 1.0
 * @Description:    执行流程 preHandle -> isAccessAllowed -> isLoginAttempt -> executeLogin 。
 */
@Slf4j
public class JwtFilter extends BasicHttpAuthenticationFilter {

    //默认需要放行的接口 shiroc处判断过,此处可写可不写
    private String[] defalutExcludeUrl = new String[] {
            "/login","/401", "/402", "/noaccess"
            //,"/formLogin",".jpg",".png",".gif",".css",".js",".jpeg"
    };

    /**
     * 检测用户是否想要登录
     * 检测header里面是否包含token字段即可
     * @param request
     * @param response
     * @return
     */
    @Override
    protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
        log.info("JwtFilter==isLoginAttempt--->");
        HttpServletRequest req = (HttpServletRequest) request;
        String authorization = req.getHeader("Authorization");
        if (authorization != null){
            //去掉token前缀
            authorization = authorization.substring(7);
            log.info("JwtFilter==isLoginAttempt--->authorization = " + authorization);
            log.info("JwtFilter==isLoginAttempt--->用户已经登录过了");
            return authorization != null;
        }else{
            log.info("JwtFilter==isLoginAttempt--->用户未登录");
            return authorization == null;
        }
    }

    /**
     * JwtToken实现了AuthenticationToken接口封装了token参数
     * 通过getSubject方法获取 subject对象
     * login()发送身份验证
     * 为什么需要在Filter中调用login,不能在controller中调用login?
     * 由于Shiro默认的验证方式是基于session的,在基于token验证的方式中,不能依赖session做为登录的判断依据.
     * 执行登录
     * @param request
     * @param response
     * @return
     * @throws Exception
     */
    @Override
    protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
        log.info("JwtFilter==executeLogin--->");
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        String authorization = httpServletRequest.getHeader("Authorization");
        //去掉token前缀
        authorization = authorization.substring(7);
        JwtToken token = new JwtToken(authorization);
        // 提交给realm进行登入,如果错误他会抛出异常并被捕获
        try {
            //触发 Shiro Realm 自身的登录控制
            getSubject(request, response).login(token);
            // 如果没有抛出异常则代表登入成功,返回true
            log.info("JwtFilter==executeLogin--->验证登入成功");
            //刷新token
            this.refreshToken(authorization, response);
            return true;
        } catch (Exception e) {
            log.error("JwtFilter==executeLogin--->没有访问权限,原因是:" + e.getMessage());
            //此处跳转到401接口返回错误信息!
            this.responseInvalid(response);
            //throw new AuthenticationException("无效token,请先登录!!!!" + e.getMessage());
            return false;
        }
    }

    /**
     * 这里详细说明下为什么最终返回的都是true,即允许访问
     * 例如提供一个地址 GET /article
     * 登入用户和游客看到的内容是不同的
     * 如果在这里返回了false,请求会被直接拦截,用户看不到任何东西
     * 所以在这里返回true,Controller中可以通过 subject.isAuthenticated() 来判断用户是否登入
     * 如果有些资源只有登入用户才能访问,我们只需要在方法上面加上 @RequiresAuthentication 注解即可
     * 但是这样做有一个缺点,就是不能够对GET,POST等请求进行分别过滤鉴权(因为我们重写了官方的方法),但实际上对应用影响不大
     * @param request
     * @param response
     * @param mappedValue
     * @return
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        log.info("JwtFilter==isAccessAllowed--->");
        //判断请求的请求头是否带上 "token"
        if (this.isLoginAttempt(request, response)) {
            try {
                //如果存在,则进入 executeLogin 方法执行登入,检查 token 是否正确
                if (this.executeLogin(request, response)){
                    String requestURL = ((HttpServletRequest) request).getRequestURL().toString();
                    log.info("JwtFilter==isAccessAllowed--->requestURL="+requestURL);
                    for(String excludeUrl : defalutExcludeUrl){
                        if (requestURL.endsWith(excludeUrl)){
                            return true;
                        }
                    }
                }
            } catch (Exception e) {
                log.error("JwtFilter==isAccessAllowed--->Token已失效或为空,JwtFilter过滤验证失败!");
                //此处跳转到402接口返回错误信息!
                this.responseInvalid(response);
//                throw new AuthenticationException("token为空,请重新登录!");
            }
        }
        //如果请求头不存在 token,则可能是执行登陆操作或者是游客状态访问,无需检查 token,直接返回 true
        return true;
    }

    /**
     * 对跨域提供支持
     * @param request
     * @param response
     * @return
     * @throws Exception
     */
    @Override
    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
        log.info("JwtFilter==preHandle--->");
        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-Request-Headers"));
        // 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态
        if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
            httpServletResponse.setStatus(HttpStatus.OK.value());
            return false;
        }
        return super.preHandle(request, response);
    }

    /**
     * 刷新token
     * @param authorization
     * @param response
     * @return
     * refreshTokens方法中,当redisToken不为空时返回true,并且如果redisToken验证成功,则将已有的token重新存入一遍,保持redis中的token不过期
     * 如果redisToken验证不通过,重新生成新的token,存入redis并返回给前端;当redisToken为空时返回false,需要重新登录.
     */
    @SneakyThrows
    protected void refreshToken(String authorization, ServletResponse response){
        log.info("-------------刷新token-------------");
        //获取token的用户名
        String account = JwtUtil.getAccount(authorization);
        //获取redis中的token
        String redisToken = JedisUtil.getJson(account);
        //判断redis中的token是否为空
        if (IsNotEmptyUtil.isNotEmpty(redisToken)){
            //将现有的token重新存入redis中
            JedisUtil.setJson(account, authorization, REFRESH_EXPIRE_TIME);
            //给前端返回新生成的token
            HttpServletResponse httpServletResponse = (HttpServletResponse) response;
            httpServletResponse.setHeader("token", authorization);
            httpServletResponse.setHeader("Access-Control-Expose-Headers", "token");
            log.info("redis中的token还未过期!");
        } else {
            //生成新的token
            String newToken = JwtUtil.sign(account);
            //将新生成的token重新存入redis中
            JedisUtil.setJson(account, newToken, REFRESH_EXPIRE_TIME);
            //给前端返回原来请求的token
            HttpServletResponse httpServletResponse = (HttpServletResponse) response;
            httpServletResponse.setHeader("token", newToken);
            httpServletResponse.setHeader("Access-Control-Expose-Headers", "token");
            log.info("redis中的token已过期!");
        }
    }

    /**
     * 将非法请求跳转到 /401    暂无token!
     * @param response
     */
    private void responseInvalid(ServletResponse response) {
        log.info("JwtFilter==response401--->");
        try {
            HttpServletResponse httpServletResponse = (HttpServletResponse) response;
            httpServletResponse.sendRedirect("/invalid");
        } catch (IOException e) {
            log.error(e.getMessage());
        }
    }

    // /**
    //  * 无需转发,直接返回Response信息
    //  */
    // private void response401(ServletResponse response, String msg) {
    //     HttpServletResponse httpServletResponse = WebUtils.toHttp(response);
    //     httpServletResponse.setStatus(HttpStatus.UNAUTHORIZED.value());
    //     httpServletResponse.setCharacterEncoding("UTF-8");
    //     httpServletResponse.setContentType("application/json; charset=utf-8");
    //     try (PrintWriter out = httpServletResponse.getWriter()) {
    //         String data = JsonConvertUtil.objectToJson(new ResponseBean(HttpStatus.UNAUTHORIZED.value(), "无权访问(Unauthorized):" + msg, null));
    //         out.append(data);
    //     } catch (IOException e) {
    //         log.error("直接返回Response信息出现IOException异常:{}", e.getMessage());
    //         throw new CustomException("直接返回Response信息出现IOException异常:" + e.getMessage());
    //     }
    // }
}

十、Shiro配置

1、MyRealm

/**
 * @Author: LEAVES
 * @Date: 2020年12月30日 14时25分08秒
 * @Version 1.0
 * @Description:
 */
@Slf4j
@Component
public class MyRealm extends AuthorizingRealm {

    @Resource
    private IRoleService roleService;
    @Resource
    private IUserService userService;
    
    /**
     * 必须重写此方法,否则Shiro会报错
     * @param token
     * @return
     */
    @Version
    public boolean supports(AuthenticationToken token) {
        return token instanceof JwtToken;
    }

    /**
     * 授权
     * 只有当需要检测用户权限的时候才会调用此方法,例如checkRole,checkPermission之类的
     * @param principals
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) throws AuthorizationException {
        //授权
        log.info("-------------用户授权-------------");
        log.info("MyRealm==doGetAuthorizationInfo--->");
        //如果身份认证的时候没有传入User对象,这里只能取到userName
        TUser tUser = (TUser) principals.getPrimaryPrincipal();
        String username = tUser.getUsername();
//        //通过调用JwtUtil.getUsername()方法得到token中的username
//        String username = JwtUtil.getUsername(principals.toString());
        if (IsNotEmptyUtil.isEmpty(username)){
            throw new AuthorizationException("无效token,请重新登录!");
        }
        //调用业务方法获取用户的角色
        Set permissions = roleService.getPermissionByUserName(username);
//        String role = userService.getRole(username);
        //调用业务方法获取用户权限
//        List list = roleService.getPermissionsByUsername(username);
        //每个用户可以设置新的权限
//        String permission = userService.getPermission(username);
        //将List换成set去掉重复权限
//        Set stringPermissions = new HashSet<>();
//        Set roleSet = new HashSet<>();
//        if (list !=null){
//            for (Permissions permissions : list){
//                log.info(username + "拥有的权限有:" + permissions);
//                stringPermissions.add(permissions.getPername());
//            }
//        }
    SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        //设置该用户拥有的角色和权限
//        authorizationInfo.setRoles(roleSet);
        authorizationInfo.setStringPermissions(permissions);
//        authorizationInfo.setStringPermissions(stringPermissions);
        return authorizationInfo;
    }

    /**
     * 认证
     * 默认使用此方法进行用户名正确与否验证,错误抛出异常即可。
     * @param auth
     * @return
     * @throws AuthenticationException
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
        log.info("-------------用户认证-------------");
        //获取用户token信息
        String token = (String) auth.getCredentials();
        // 帐号为空
        if (IsNotEmptyUtil.isEmpty(token)) {
            throw new AuthenticationException("暂无token!");
        }
        log.info("MyRealm==doGetAuthenticationInfo--->token = " + token);
        //判断token中是否包含用户信息
        String username = null;
        try {
            //这里工具类没有处理空指针等异常这里处理一下(这里处理科学一些)
            //解密获得username,用于和数据库进行对比
            username = JwtUtil.getUsername(token);
            log.info("MyRealm==doGetAuthenticationInfo--->从token中解析出的username = " + username);
        } catch (Exception e) {
            log.info("MyRealm==doGetAuthenticationInfo--->AuthenticationException:token拼写错误或者值为空!");
            throw new AuthenticationException("token拼写错误或者值为空");
        }
        if (username == null) {
            log.error("MyRealm==doGetAuthenticationInfo--->token无效(空''或者null都不行!)");
            throw new AuthenticationException("认证失败,token无效或token中未包含用户信息!");
        }
        //根据用户信息查询数据库获取后端的用户身份,转交给securityManager判定
        //调用业务方法从数据库中获取用户信息
        TUser tUser = userService.getUserByUserName(username);
        //判断从数据库中获取用户信息是否为空
        if (tUser == null) {
            log.error("MyRealm==doGetAuthenticationInfo--->用户不存在!)");
            throw new AuthenticationException("用户" + username + "不存在!");
        }
        //认证
        return new SimpleAuthenticationInfo(tUser, token, "my_realm");
    }
}

2、ShiroConfig

/**
 * @Author: LEAVES
 * @Date: 2020年12月30日 14时23分12秒
 * @Version 1.0
 * @Description:
 */
@Slf4j
@Configuration
public class ShiroConfig {

    @Bean
    public MyRealm myRealm() {
        return new MyRealm();
    }

    /**
     * 注入安全过滤器
     * @param securityManager
     * @return
     */
    @Bean("shiroFilter")
    public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
        log.info("ShiroConfig==shiroFilter--->");
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        //拦截器
        Map filterChainDefinitionMap = new LinkedHashMap();
        // 配置不会被拦截的链接 顺序判断
        filterChainDefinitionMap.put("/", "anon");
        filterChainDefinitionMap.put("/login", "anon");
        // 添加自己的过滤器并且取名为jwt
        Map filterMap = new HashMap(1);
        filterMap.put("jwt", new JwtFilter());
        log.info("ShiroConfig==shiroFilter--->new JwtFilter() : " + new JwtFilter());
        shiroFilterFactoryBean.setFilters(filterMap);
        //");
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(myRealm);

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

    }

    /**
     * 自动创建代理,没有这个鉴权可能会出错
     * @return
     */
    @Bean
    public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
        log.info("ShiroConfig==getDefaultAdvisorAutoProxyCreator--->");
        DefaultAdvisorAutoProxyCreator autoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        autoProxyCreator.setProxyTargetClass(true);
        return autoProxyCreator;
    }
    /**
     * 开启shiro aop注解支持.
     * 使用代理方式;所以需要开启代码支持;
     * @param
     * @return
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        log.info("ShiroConfig==authorizationAttributeSourceAdvisor--->");
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }
}

十一、业务实现

(整个过程中省略了mapper与xml的实现,以及实体类的。mapper、xml、entity、service、controller都可根据实际情况进行修改。)

1、UserService以及实现类

/**
 * @Author: LEAVES
 * @Date: 2020年12月30日 13时44分34秒
 * @Version 1.0
 * @Description:
 */
public interface IUserService {
    
    TUser getUserByUserName(String username);

    ResponseData login(String username, String password, HttpServletResponse response);

    ResponseData logout(HttpServletRequest request);

    ResponseData queryUserList();

    ResponseData queryUserByUserName(String username);
    
}
/**
 * @Author: LEAVES
 * @Date: 2020年12月30日 13时44分45秒
 * @Version 1.0
 * @Description:
 */
@Service
@Slf4j
public class UserService implements IUserService {

    @Resource
    private TUserMapper userMapper;
    @Resource
    private TRoleMapper roleMapper;

    /**
     * 根据用户名查询用户信息(程序内部调用)
     * @param username
     * @return
     */
    @Override
    public TUser getUserByUserName(String username) {
        TUser tUser = userMapper.selectByUserName(username);
        log.info("权限认证===>根据用户名查询用户信息:" + tUser.toString());
        return tUser;
    }

    /**
     * 登录实现
     * @param username
     * @param password
     * @param response
     * @return
     */
    @Override
    public ResponseData login(String username, String password, HttpServletResponse response) {
        log.info("用户登录");
        //为了简单实现此功能,并也未对前端传来的用户名和密码做正则校验
        //selectUserToRoleByUserName该方法是一对一查询
        TUser tUser = userMapper.selectUserToRoleByUserName(username);
//        Set roleByUserName = tRoleService.getRoleByUserName(username);
        //由于前端+controller两处对username和password进行了判断,所以此处不再做判断
        
        if (tUser != null) {
            //解密数据库中用户密码
            String dbpwd = MD5Util.convertMD5(tUser.getPassword());
            //判断输入的用户名和密码是否与数据库中的用户名、密码一致
            if(tUser.getUsername().equals(username) && dbpwd.equals(password)){
                //生成token
                String token = JwtUtil.sign(tUser.getUsername());
                log.info("登录时生成的token = " + token);
                //获取当前时间的时间戳
                long currentTimeMillis = System.currentTimeMillis();
                //向redis中存入token
                JedisUtil.setJson(username, token, JwtUtil.REFRESH_EXPIRE_TIME);
                //向前端返回token
                HttpServletResponse httpServletResponse = response;
                httpServletResponse.setHeader("token", token);
                httpServletResponse.setHeader("Access-Control-Expose-Headers", "token");
                log.info("登录成功:" + tUser.toString());
                tUser.setPassword(dbpwd);
                Map map = new HashMap<>();
                map.put("user", tUser);
                map.put("token", token);
                return ResponseDataUtil.success("登录成功", map);
            }
            return ResponseDataUtil.fail("登录失败,用户名或密码错误!");
        }
        return ResponseDataUtil.fail("登录失败,用户不存在!");
    }

    /**
     * 退出登录
     * @param request
     * @return
     */
    @Override
    public ResponseData logout(HttpServletRequest request) {
        log.info("退出登录");
        //获取token
        String token = request.getHeader("Authorization");
        //去掉token前缀
        token = token.substring(7);
        //token非空判断
        if (IsNotEmptyUtil.isEmpty(token)) {
            return ResponseDataUtil.fail("无效的token!");
        }
        //获取token中的用户名
        String username = JwtUtil.getAccount(token);
        //判断该token的用户是否存在
        TUser tUser = userMapper.selectByUserName(username);
        if (tUser != null) {
            log.info(" 用户名: " + username + " 退出登录成功! ");
            if (JedisUtil.getJson(username) == null){
                return ResponseDataUtil.fail("已经退出登录过了!");
            }
            //清空redis中用户Token缓存
            Long aLong = JedisUtil.delKey(username);
            return ResponseDataUtil.success("退出登录成功!");
        }
        return ResponseDataUtil.fail("令牌失效,请重新登录!");
    }

    /**
     * 查询全部用户信息
     * @return
     */
    @Override
    public ResponseData queryUserList() {
        List tUsers = userMapper.selectAll();
        if (!tUsers.isEmpty()){
            for (TUser tUser : tUsers){
                //密码解密
                tUser.setPassword(MD5Util.convertMD5(tUser.getPassword()));
            }
            log.info("获取全部用户信息:" + tUsers.toString());
            return ResponseDataUtil.success("查询成功!", tUsers);
        }
        return ResponseDataUtil.fail("暂无用户信息!");
    }

    /**
     * 根据用户查询用户信息(接口)
     * @param username
     * @return
     */
    @Override
    public ResponseData queryUserByUserName(String username) {
        TUser tUser = userMapper.selectByUserName(username);
        if (tUser != null){
            return ResponseDataUtil.success("查询成功!", tUser);
        }
        return ResponseDataUtil.fail("暂无该用户信息!");
    }
}

2、RoleServicey以及实现类

/**
 * @Author: LEAVES
 * @Date: 2020年12月30日 11时20分48秒
 * @Version 1.0
 * @Description:
 */
public interface IRoleService {

    Set getPermissionByUserName(String username);

    ResponseData queryRoleList();
}
/**
 * @Author: LEAVES
 * @Date: 2020年12月30日 11时21分02秒
 * @Version 1.0
 * @Description:
 */
@Service
@Slf4j
public class RoleService implements IRoleService {

    @Resource
    private TRoleMapper roleMapper;

    /**
     * 根据用户查询用户权限(权限认证时调用)
     * @param username
     * @return
     */
    @Override
    public Set getPermissionByUserName(String username) {
        List permissions = roleMapper.getPermissionByUserName(username);
        log.info("权限鉴别===>通过用户名获取用户权限信息:" + permissions.toString());
        return new HashSet(permissions);
    }

    /**
     * 查询全部角色信息
     * @return
     */
    @Override
    public ResponseData queryRoleList() {
        List roles = roleMapper.selectAll();
        if (!roles.isEmpty()){
            return ResponseDataUtil.success("查询成功!", roles);
        }
        return ResponseDataUtil.fail("暂无角色数据!");
    }
}

十二、控制层的实现

1、RedirectController(重定向访问)

/**
 * @Author: LEAVES
 * @Date: 2020年12月30日 17时36分46秒
 * @Version 1.0
 * @Description:
 */
@Slf4j
@RestController
@CrossOrigin
public class RedirectController {
    /**
     * 暂无token
     * @return
     */
    @GetMapping("/invalid")
    public Object redirect401(){
        return ResponseDataUtil.fail("token已失效或为空,请先登录!");
    }

    /**
     * 权限不足
     * @return
     */
    @GetMapping("/noaccess")
    public Object redirect403(){
        return ResponseDataUtil.fail("对不起,无权限访问!");
    }

}

2、LoginController

@Slf4j
@RestController
@CrossOrigin
public class LoginController {

    @Resource
    private IUserService userService;

    /**
     * 登录
     * @param username
     * @param password
     * @param response
     * @return
     */
    @PostMapping("/login")
    public Object login(@RequestParam("username")String username, @RequestParam("password")String password, HttpServletResponse response){
        //用户名和密码非空判断
        if (StrUtil.isNotBlank(username) && StrUtil.isNotBlank(password)){
            return userService.login(username, password, response);
        } else {
            log.info("******");
            return ResponseDataUtil.fail("登录失败,用户名或密码为空!");

        }
    }
}

3、UserController

/**
 * @Author: LEAVES
 * @Date: 2020年12月30日 17时41分59秒
 * @Version 1.0
 * @Description:
 */
@Slf4j
@RestController
@CrossOrigin
@RequestMapping("/api")
public class UserController {

    @Resource
    private IUserService userService;

    /**
     * 退出登录
     * @param request
     * @return
     */
    @GetMapping("/logout")
    public Object logout(HttpServletRequest request){
        return userService.logout(request);
    }

    /**
     * 查询所有用户
     * @return
     */
    @GetMapping("/user/list")
    public Object queryUserList(){
        return userService.queryUserList();
    }

    /**
     * 查询所有用户z
     * @return
     */
    @GetMapping("/user/username")
    @RequiresPermissions(value={"3","1"},logical = Logical.OR) //权限限制
    public Object queryUserByUserName(@RequestParam("username") String username){
        return userService.queryUserByUserName(username);
    }
}

4、RoleController

/**
 * @Author: LEAVES
 * @Date: 2020年12月30日 17时59分39秒
 * @Version 1.0
 * @Description:
 */
@Slf4j
@RestController
@CrossOrigin
@RequestMapping("/api")
public class RoleController {

    @Resource
    private IRoleService roleService;

    /**
     * 查询全部角色信息
     * @return
     */
    @GetMapping("/role/list")
    public Object queryAll(){
        return roleService.queryRoleList();
    }
}

十三、测试结果

1、登录生成token

基于spring boot的RBAC超详细实现_第1张图片

2、带上刚刚生成的token的访问

基于spring boot的RBAC超详细实现_第2张图片

3、不带token访问

基于spring boot的RBAC超详细实现_第3张图片

4、权限测试

基于spring boot的RBAC超详细实现_第4张图片

码云源码地址:https://gitee.com/loveleaveslove/rbac-demo.git

你可能感兴趣的:(❤️Java❤️,java)