# spring boot + mybatis + jwt + shiro + redis **Repository Path**: l1-git/rbac-demo ## Basic Information - **Project Name**: spring boot + mybatis + jwt + shiro + redis - **Description**: 一个简单的RBAC实现(基于spring boot + mybatis + jwt + shiro + redis + postgresql) - **Primary Language**: Java - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 6 - **Created**: 2022-07-25 - **Last Updated**: 2022-07-25 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README ## **基于spring boot + mybatis + jwt + shiro + redis + postgresql的RBAC实现* ##### 一、搭建数据库 1、用户表 ```plsql -- 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、角色表(由于是简单实现其功能,所以此处简单代替) ```plsql 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中导入依赖 ```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 ```yaml #端口号 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 ```java /** * @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 ```java /** * @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 ```java /** * @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 ```java /** * @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 ```java /** * 封装常见的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 ```java /** * @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 ```java /** * @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 ```java /** * @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 ```java /** * @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 ```java /** * @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 ```java /** * @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 ```java * @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 ```java /** * @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时使其变成空字符串 ```java /** * 处理返回值中的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; } } ``` ```java /** * 该类控制将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); } } ``` ```java /** * 处理数组类型的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(); } } } ``` ```java /** * 处理字符串等类型的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(""); } } ``` ```java /** * 处理对象类型的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 ```java /** * @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 ```java /** * @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 ```java /** * 重写鉴权 * @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 ```java /** * @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 ```java /** * @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; } } ``` ##### 十一、重定向Controller ```java /** * @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("对不起,无权限访问!"); } } ``` ##### 十二、业务实现 (整个过程中省略了mapper与xml的实现,以及实体类的。mapper、xml、entity、service、controller都可根据实际情况进行修改。) ###### 1、UserService以及实现类 ```java /** * @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); } ``` ```java /** * @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以及实现类 ```java /** * @Author: LEAVES * @Date: 2020年12月30日 11时20分48秒 * @Version 1.0 * @Description: */ public interface IRoleService { Set getPermissionByUserName(String username); ResponseData queryRoleList(); } ``` ```java /** * @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(重定向访问) ```java /** * @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 ```java @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 ```java /** * @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 ```java /** * @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(); } } ```