700字范文,内容丰富有趣,生活中的好帮手!
700字范文 > springboot+shiro+redis+jwt实现多端登录:PC端和移动端同时(不同终端可同时)

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

时间:2020-11-27 11:46:06

相关推荐

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

前言

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

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

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

正文

一、pom.xml和数据库表

1、pom.xml

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId></dependency><!-- AOP依赖,一定要加,否则权限拦截验证不生效 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><!-- mysql 驱动 --><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>8.0.22</version><scope>runtime</scope></dependency><!-- mybatis_plus --><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.1.2</version></dependency><!-- json --><dependency><groupId>net.sf.json-lib</groupId><artifactId>json-lib</artifactId><version>2.4</version><classifier>jdk15</classifier><!-- 就是这句 --></dependency><dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.7</version></dependency><!--redis--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><!-- Shiro 核心依赖 --><dependency><groupId>org.apache.shiro</groupId><artifactId>shiro-spring</artifactId><version>1.4.0</version></dependency><!-- jwt token --><dependency><groupId>com.auth0</groupId><artifactId>java-jwt</artifactId><version>3.2.0</version></dependency><dependency><groupId>mons</groupId><artifactId>commons-pool2</artifactId><version>2.0</version></dependency><!-- StringUtilS工具 --><dependency><groupId>mons</groupId><artifactId>commons-lang3</artifactId><version>3.5</version></dependency>

2、数据库

SET NAMES utf8mb4;SET FOREIGN_KEY_CHECKS = 0;-- ------------------------------ Table structure for sys_dept-- ----------------------------DROP TABLE IF EXISTS `sys_dept`;CREATE TABLE `sys_dept` (`id` bigint NOT NULL AUTO_INCREMENT COMMENT '部门id',`create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',`create_by` bigint NULL DEFAULT NULL COMMENT '创建用户Id',`update_time` datetime NULL DEFAULT NULL COMMENT '修改时间',`update_by` bigint NULL DEFAULT NULL COMMENT '修改用户Id',`parent_id` bigint NULL DEFAULT 0 COMMENT '父部门id',`ancestors` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '祖级列表',`dept_name` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '部门名称',`dept_type` tinyint(1) NULL DEFAULT 1 COMMENT '类型 1 公司 2 部门',`status` tinyint(1) NULL DEFAULT 0 COMMENT '部门状态(0正常 1停用)',`sort` int NULL DEFAULT 0 COMMENT '显示顺序',PRIMARY KEY (`id`) USING BTREE) ENGINE = InnoDB AUTO_INCREMENT = 216 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '部门表' ROW_FORMAT = DYNAMIC;-- ------------------------------ Table structure for sys_dict-- ----------------------------DROP TABLE IF EXISTS `sys_dict`;CREATE TABLE `sys_dict` (`id` bigint NOT NULL AUTO_INCREMENT COMMENT '编号',`create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',`create_by` bigint NULL DEFAULT NULL COMMENT '创建用户Id',`update_time` datetime NULL DEFAULT NULL COMMENT '修改时间',`update_by` bigint NULL DEFAULT NULL COMMENT '修改用户Id',`parent_id` bigint NULL DEFAULT NULL COMMENT '父级id',`dict_code` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '字典代码',`dict_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '字典名称',`dict_value` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '字典值',`sort` int NULL DEFAULT NULL COMMENT '排序',`remark` text CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL COMMENT '备注',PRIMARY KEY (`id`) USING BTREE) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = COMPACT;-- ------------------------------ Table structure for sys_log-- ----------------------------DROP TABLE IF EXISTS `sys_log`;CREATE TABLE `sys_log` (`id` bigint NOT NULL AUTO_INCREMENT COMMENT '日志编号',`log_time` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '操作日期',`log_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '操作账号',`log_method` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '操作',`log_ip` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '主机地址',`log_url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '请求URL',`status` tinyint(1) NULL DEFAULT 0 COMMENT '操作状态(0成功 1失败)',PRIMARY KEY (`id`) USING BTREE) ENGINE = InnoDB AUTO_INCREMENT = 4240 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '操作日志记录' ROW_FORMAT = DYNAMIC;-- ------------------------------ Table structure for sys_menu-- ----------------------------DROP TABLE IF EXISTS `sys_menu`;CREATE TABLE `sys_menu` (`id` bigint NOT NULL AUTO_INCREMENT COMMENT '菜单ID',`name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '菜单名称',`parent_id` bigint NULL DEFAULT 0 COMMENT '父菜单ID',`sort` int NULL DEFAULT 0 COMMENT '显示顺序',`url` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '地址',`type` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '菜单类型(M目录 C菜单 F按钮)',`perms` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '权限标识',`icon` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '#' COMMENT '菜单图标',`create_by` bigint NULL DEFAULT NULL COMMENT '创建者',`create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',`update_by` bigint NULL DEFAULT NULL COMMENT '更新者',`update_time` datetime NULL DEFAULT NULL COMMENT '更新时间',`remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '备注',PRIMARY KEY (`id`) USING BTREE) ENGINE = InnoDB AUTO_INCREMENT = 2044 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '菜单权限表' ROW_FORMAT = DYNAMIC;-- ------------------------------ Table structure for sys_role-- ----------------------------DROP TABLE IF EXISTS `sys_role`;CREATE TABLE `sys_role` (`id` bigint NOT NULL AUTO_INCREMENT COMMENT '角色ID',`role_name` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '角色名称',`role_key` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '角色代码',`create_by` bigint NULL DEFAULT NULL COMMENT '创建者',`create_time` datetime NULL DEFAULT NULL COMMENT '创建时间',`update_by` bigint NULL DEFAULT NULL COMMENT '更新者',`update_time` datetime NULL DEFAULT NULL COMMENT '更新时间',`remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '备注',PRIMARY KEY (`id`) USING BTREE) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = '角色信息表' ROW_FORMAT = DYNAMIC;-- ------------------------------ Table structure for sys_token-- ----------------------------DROP TABLE IF EXISTS `sys_token`;CREATE TABLE `sys_token` (`id` bigint NOT NULL COMMENT '主键',`user_id` bigint NULL DEFAULT NULL COMMENT '用户Id',`token` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '用户token',`type` tinyint(1) NULL DEFAULT NULL COMMENT '终端类型(1 web端 2 app端)',`status` tinyint(1) NULL DEFAULT NULL COMMENT '登录状态 (1 已登录 2 已注销)',`login_time` datetime NULL DEFAULT NULL COMMENT '登录时间',`logout_time` datetime NULL DEFAULT NULL COMMENT '退出时间',`last_request_time` datetime NULL DEFAULT NULL COMMENT '最后一次登录时间(最后一次请求时间)',PRIMARY KEY (`id`) USING BTREE) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;-- ------------------------------ Table structure for sys_user-- ----------------------------DROP TABLE IF EXISTS `sys_user`;CREATE TABLE `sys_user` (`id` bigint NOT NULL,`dept_id` bigint NOT NULL AUTO_INCREMENT COMMENT '部门id',`user_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '用户名称',`real_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '用户姓名',`password` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '密码',`salt` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '密码加密盐值',`roles` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL,`login_date` datetime NULL DEFAULT NULL COMMENT '登录时间',`error_num` int NOT NULL DEFAULT 0 AUTO_INCREMENT COMMENT '密码错误次数',`update_pwd_time` datetime NULL DEFAULT NULL COMMENT '密码更改时间'PRIMARY KEY (`id`) USING BTREE) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;SET FOREIGN_KEY_CHECKS = 1;

二、JWTToken,继承 AuthenticationToken

import org.apache.shiro.authc.AuthenticationToken;public class JWTToken implements AuthenticationToken {private String token;//登录类型,区分PC端和移动端private String loginType;public JWTToken(String token,String loginType) {this.token = token;this.loginType=loginType;}public String getLoginType() {return loginType;}public void setLoginType(String loginType) {this.loginType = loginType;}@Overridepublic Object getPrincipal() {return token;}@Overridepublic Object getCredentials() {return token;}}

三、工具类

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

import com.auth0.jwt.JWT;import com.auth0.jwt.JWTVerifier;import com.auth0.jwt.algorithms.Algorithm;import com.auth0.jwt.exceptions.JWTDecodeException;import com.auth0.jwt.interfaces.DecodedJWT;import com.entity.sys.SysUser;import org.apache.shiro.SecurityUtils;import java.io.UnsupportedEncodingException;import java.util.Date;import java.util.HashMap;import java.util.Map;/*** jwt工具类*/public class JWTUtil {//token有效时长(30分钟)private static final long EXPIRE=30*60*1000L;//token的密钥private static final String SECRET="jwt+shiro";/*** 生成token* @param userName 用户名* @param current 当前时间截点* @param loginType 登录类型* @return*/public static String createToken(String userName,Long current,String loginType) {//token过期时间Date date=new Date(current+EXPIRE);//jwt的header部分Map<String ,Object>map=new HashMap<>();map.put("alg","HS256");map.put("typ","JWT");//使用jwt的api生成tokenString token= null;//签名try {token = JWT.create().withHeader(map).withClaim("userName", userName+"_"+loginType)//私有声明.withClaim("current",current)//当前时间截点.withExpiresAt(date)//过期时间.withIssuedAt(new Date())//签发时间.sign(Algorithm.HMAC256(SECRET));} catch (UnsupportedEncodingException e) {e.printStackTrace();}return token;}//校验token的有效性,1、token的header和payload是否没改过;2、没有过期public static boolean verify(String token){try {//解密JWTVerifier verifier=JWT.require(Algorithm.HMAC256(SECRET)).build();verifier.verify(token);return true;}catch (Exception e){return false;}}//根据token获取用户名(无需解密也可以获取token的信息)public static String getUserName(String token){try {DecodedJWT jwt = JWT.decode(token);String userName = jwt.getClaim("userName").asString();userName = userName.substring(0,userName.lastIndexOf("_"));return userName;} catch (JWTDecodeException e) {return null;}}/*** 获取当前用户信息*/public static SysUser getUserInfo(){SysUser user =(SysUser) SecurityUtils.getSubject().getPrincipal();return user;}public static Long getUserId(){return getUserInfo().getId();}//获取过期时间public static Long getExpire(String token){try {DecodedJWT jwt = JWT.decode(token);return jwt.getClaim("current").asLong();}catch (Exception e){return null;}}}

2、RedisUtil

import org.springframework.beans.factory.annotation.Autowired;import org.springframework.data.redis.core.RedisTemplate;import org.ponent;import org.springframework.util.CollectionUtils;import java.util.Collection;import java.util.List;import java.util.Map;import java.util.Set;import java.util.concurrent.TimeUnit;/*** redis工具类*/@Componentpublic class RedisUtil {@Autowiredprivate RedisTemplate<String, Object> redisTemplate;/*** 指定缓存失效时间* @param key 键* @param time 时间(秒)*/public boolean expire(String key, long time) {try {if (time > 0) {redisTemplate.expire(key, time, TimeUnit.SECONDS);}return true;} catch (Exception e) {e.printStackTrace();return false;}}/*** 根据key 获取过期时间* @param key 键 不能为null* @return 时间(秒) 返回0代表为永久有效*/public long getExpire(String key) {return redisTemplate.getExpire(key, TimeUnit.SECONDS);}/*** 判断key是否存在* @param key 键* @return true 存在 false不存在*/public boolean hasKey(String key) {try {return redisTemplate.hasKey(key);} catch (Exception e) {e.printStackTrace();return false;}}/*** 删除缓存* @param key 可以传一个值 或多个*/public void del(String... key) {if (key != null && key.length > 0) {if (key.length == 1) {redisTemplate.delete(key[0]);} else {redisTemplate.delete((Collection<String>) CollectionUtils.arrayToList(key));}}}/*** 普通缓存获取* @param key 键* @return 值*/public Object get(String key) {return key == null ? null : redisTemplate.opsForValue().get(key);}/*** 普通缓存放入* @param key 键* @param value 值* @return true成功 false失败*/public boolean set(String key, Object value) {try {redisTemplate.opsForValue().set(key, value);return true;} catch (Exception e) {e.printStackTrace();return false;}}/*** 普通缓存放入并设置时间* @param key 键* @param value 值* @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期* @return true成功 false 失败*/public boolean set(String key, Object value, long time) {try {if (time > 0) {redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);} else {set(key, value);}return true;} catch (Exception e) {e.printStackTrace();return false;}}/*** 递增* @param key 键* @param delta 要增加几(大于0)*/public long incr(String key, long delta) {if (delta < 0) {throw new RuntimeException("递增因子必须大于0");}return redisTemplate.opsForValue().increment(key, delta);}/*** 递减* @param key 键* @param delta 要减少几(小于0)*/public long decr(String key, long delta) {if (delta < 0) {throw new RuntimeException("递减因子必须大于0");}return redisTemplate.opsForValue().decrement(key,delta);// return redisTemplate.opsForValue().increment(key, -delta);}public long strLen(String key){return redisTemplate.opsForValue().get(key).toString().length();}/** 追加字符* @param key 键* @param str 要追加的字符* */public boolean append(String key,String str){try {redisTemplate.opsForValue().append(key,str);return true;}catch (Exception e){return false;}}/*** HashGet* @param key 键 不能为null* @param item 项 不能为null*/public Object hget(String key, String item) {return redisTemplate.opsForHash().get(key, item);}/*** 获取hashKey对应的所有键值* @param key 键* @return 对应的多个键值*/public Map<Object, Object> hmget(String key) {return redisTemplate.opsForHash().entries(key);}/*** HashSet* @param key 键* @param map 对应多个键值*/public boolean hmset(String key, Map<String, Object> map) {try {redisTemplate.opsForHash().putAll(key, map);return true;} catch (Exception e) {e.printStackTrace();return false;}}/*** HashSet 并设置时间* @param key 键* @param map 对应多个键值* @param time 时间(秒)* @return true成功 false失败*/public boolean hmset(String key, Map<String, Object> map, long time) {try {redisTemplate.opsForHash().putAll(key, map);if (time > 0) {expire(key, time);}return true;} catch (Exception e) {e.printStackTrace();return false;}}/*** 向一张hash表中放入数据,如果不存在将创建* @param key 键* @param item 项* @param value 值* @return true 成功 false失败*/public boolean hset(String key, String item, Object value) {try {redisTemplate.opsForHash().put(key, item, value);return true;} catch (Exception e) {e.printStackTrace();return false;}}/*** 向一张hash表中放入数据,如果不存在将创建* @param key 键* @param item 项* @param value 值* @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间* @return true 成功 false失败*/public boolean hset(String key, String item, Object value, long time) {try {redisTemplate.opsForHash().put(key, item, value);if (time > 0) {expire(key, time);}return true;} catch (Exception e) {e.printStackTrace();return false;}}/*** 删除hash表中的值* @param key 键 不能为null* @param item 项 可以使多个 不能为null*/public void hdel(String key, Object... item) {redisTemplate.opsForHash().delete(key, item);}/*** 判断hash表中是否有该项的值* @param key 键 不能为null* @param item 项 不能为null* @return true 存在 false不存在*/public boolean hHasKey(String key, String item) {return redisTemplate.opsForHash().hasKey(key, item);}/*** hash递增 如果不存在,就会创建一个 并把新增后的值返回* @param key 键* @param item 项* @param by 要增加几(大于0)*/public double hincr(String key, String item, double by) {return redisTemplate.opsForHash().increment(key, item, by);}/*** hash递减* @param key 键* @param item 项* @param by 要减少记(小于0)*/public double hdecr(String key, String item, double by) {return redisTemplate.opsForHash().increment(key, item, -by);}/*** 根据key获取Set中的所有值* @param key 键*/public Set<Object> sGet(String key) {try {return redisTemplate.opsForSet().members(key);} catch (Exception e) {e.printStackTrace();return null;}}/*** 根据value从一个set中查询,是否存在* @param key 键* @param value 值* @return true 存在 false不存在*/public boolean sHasKey(String key, Object value) {try {return redisTemplate.opsForSet().isMember(key, value);} catch (Exception e) {e.printStackTrace();return false;}}/*** 将数据放入set缓存* @param key 键* @param values 值 可以是多个* @return 成功个数*/public long sSet(String key, Object... values) {try {return redisTemplate.opsForSet().add(key, values);} catch (Exception e) {e.printStackTrace();return 0;}}/*** 将set数据放入缓存* @param key 键* @param time 时间(秒)* @param values 值 可以是多个* @return 成功个数*/public long sSetAndTime(String key, long time, Object... values) {try {Long count = redisTemplate.opsForSet().add(key, values);if (time > 0)expire(key, time);return count;} catch (Exception e) {e.printStackTrace();return 0;}}/*** 获取set缓存的长度* @param key 键*/public long sGetSetSize(String key) {try {return redisTemplate.opsForSet().size(key);} catch (Exception e) {e.printStackTrace();return 0;}}/*** 移除值为value的* @param key 键* @param values 值 可以是多个* @return 移除的个数*/public long setRemove(String key, Object... values) {try {Long count = redisTemplate.opsForSet().remove(key, values);return count;} catch (Exception e) {e.printStackTrace();return 0;}}/*** 获取list缓存的内容* @param key 键* @param start 开始* @param end 结束 0 到 -1代表所有值*/public List<Object> lGet(String key, long start, long end) {try {return redisTemplate.opsForList().range(key, start, end);} catch (Exception e) {e.printStackTrace();return null;}}/*** 获取list缓存的长度* @param key 键*/public long lGetListSize(String key) {try {return redisTemplate.opsForList().size(key);} catch (Exception e) {e.printStackTrace();return 0;}}/*** 通过索引 获取list中的值* @param key 键* @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推*/public Object lGetIndex(String key, long index) {try {return redisTemplate.opsForList().index(key, index);} catch (Exception e) {e.printStackTrace();return null;}}/*** 将list放入缓存* @param key 键* @param value 值*/public boolean lSet(String key, Object value) {try {redisTemplate.opsForList().rightPush(key, value);return true;} catch (Exception e) {e.printStackTrace();return false;}}/*** 将list放入缓存* @param key 键* @param value 值* @param time 时间(秒)*/public boolean lSet(String key, Object value, long time) {try {redisTemplate.opsForList().rightPush(key, value);if (time > 0)expire(key, time);return true;} catch (Exception e) {e.printStackTrace();return false;}}/*** 将list放入缓存* @param key 键* @param value 值* @return*/public boolean lSet(String key, List<Object> value) {try {redisTemplate.opsForList().rightPushAll(key, value);return true;} catch (Exception e) {e.printStackTrace();return false;}}/*** 将list放入缓存* @param key 键* @param value 值* @param time 时间(秒)* @return*/public boolean lSet(String key, List<Object> value, long time) {try {redisTemplate.opsForList().rightPushAll(key, value);if (time > 0)expire(key, time);return true;} catch (Exception e) {e.printStackTrace();return false;}}/*** 根据索引修改list中的某条数据* @param key 键* @param index 索引* @param value 值* @return*/public boolean lUpdateIndex(String key, long index, Object value) {try {redisTemplate.opsForList().set(key, index, value);return true;} catch (Exception e) {e.printStackTrace();return false;}}/*** 移除N个值为value* @param key 键* @param count 移除多少个* @param value 值* @return 移除的个数*/public long lRemove(String key, long count, Object value) {try {Long remove = redisTemplate.opsForList().remove(key, count, value);return remove;} catch (Exception e) {e.printStackTrace();return 0;}}}

3、SpringUtil

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

四、realm

1、ModularRealm 多realm管理器

import mon.vo.JWTToken;import org.apache.shiro.authc.AuthenticationException;import org.apache.shiro.authc.AuthenticationInfo;import org.apache.shiro.authc.AuthenticationToken;import org.apache.shiro.authc.pam.ModularRealmAuthenticator;import org.apache.shiro.realm.Realm;import org.slf4j.Logger;import org.slf4j.LoggerFactory;import java.util.Collection;import java.util.HashMap;/*** 自定义的Realm管理,主要针对多realm*/public class ModularRealm extends ModularRealmAuthenticator {private static final Logger log = LoggerFactory.getLogger(ModularRealm.class);@Overrideprotected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {// 判断getRealms()是否返回为空assertRealmsConfigured();// 所有RealmCollection<Realm> realms = getRealms();// 登录类型对应的所有RealmHashMap<String, Realm> realmHashMap = new HashMap<>(realms.size());for (Realm realm : realms) {realmHashMap.put(realm.getName(), realm);}JWTToken token = (JWTToken) authenticationToken;// 登录类型String type = token.getLoginType();//根据登录类型,走对应的realmif (realmHashMap.get(type) != null) {return doSingleRealmAuthentication(realmHashMap.get(type), token);} else {return doMultiRealmAuthentication(realms, token);}}}

2、MobileRealm 移动端的realm

import com.auth0.jwt.exceptions.TokenExpiredException;import mon.constant.UserConstant;import mon.enums.ResultEnum;import mon.util.JWTUtil;import mon.util.RedisUtil;import mon.vo.CustomException;import mon.vo.JWTToken;import com.entity.sys.SysUser;import com.service.sys.SysUserService;import org.apache.shiro.authc.AuthenticationException;import org.apache.shiro.authc.AuthenticationInfo;import org.apache.shiro.authc.AuthenticationToken;import org.apache.shiro.authc.SimpleAuthenticationInfo;import org.apache.shiro.authz.AuthorizationInfo;import org.apache.shiro.realm.AuthorizingRealm;import org.apache.shiro.subject.PrincipalCollection;import org.springframework.beans.factory.annotation.Autowired;/*** app端登录的Realm管理*/public class MobileRealm extends AuthorizingRealm {@Autowiredprivate SysUserService userService;@Autowiredprivate RedisUtil redisUtil;/*** 使用JWT替代原生Token* @param token* @return*/@Overridepublic boolean supports(AuthenticationToken token) {return token instanceof JWTToken;}private static final String ADMIN_LOGIN_TYPE = UserConstant.APP;{super.setName("mobile"); //设置realm的名字,非常重要}@Overrideprotected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {//这里根据自己的需求进行授权和处理return null;}@Overrideprotected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {String jwt= (String) authenticationToken.getCredentials();String userName= JWTUtil.getUserName(jwt);SysUser user = userService.getUserByName(userName);//判断账号是否存在if (user == null ) {throw new CustomException(ResultEnum.USER_NOT_ERROR,"");}String userNameType = userName+"_"+UserConstant.APP;if (redisUtil.hasKey(userNameType)){//判断AccessToken有无过期if (!JWTUtil.verify(jwt)){throw new TokenExpiredException("token认证失效,token过期,重新登陆");}else {//判断AccessToken和refreshToken的时间节点是否一致long current = (long) redisUtil.hget(userNameType, "current");if (current==JWTUtil.getExpire(jwt)){return new SimpleAuthenticationInfo(user,jwt,getName());}}}return null;}}

3、WebRealm PC端的realm

import com.auth0.jwt.exceptions.TokenExpiredException;import mon.constant.UserConstant;import mon.enums.ResultEnum;import mon.util.JWTUtil;import mon.util.RedisUtil;import mon.vo.CustomException;import mon.vo.JWTToken;import com.entity.sys.SysMenu;import com.entity.sys.SysRole;import com.entity.sys.SysUser;import com.service.sys.SysMenuService;import com.service.sys.SysRoleService;import com.service.sys.SysUserService;import org.apache.shiro.authc.AuthenticationException;import org.apache.shiro.authc.AuthenticationInfo;import org.apache.shiro.authc.AuthenticationToken;import org.apache.shiro.authc.SimpleAuthenticationInfo;import org.apache.shiro.authz.AuthorizationInfo;import org.apache.shiro.authz.SimpleAuthorizationInfo;import org.apache.shiro.realm.AuthorizingRealm;import org.apache.shiro.subject.PrincipalCollection;import org.springframework.beans.factory.annotation.Autowired;import java.util.HashSet;import java.util.List;import java.util.Set;/*** web端登录的Realm管理*/public class WebRealm extends AuthorizingRealm {@Autowiredprivate SysUserService userService;@Autowiredprivate SysRoleService roleService;@Autowiredprivate SysMenuService menuService;@Autowiredprivate RedisUtil redisUtil;/*** 使用JWT替代原生Token* @param token* @return*/@Overridepublic boolean supports(AuthenticationToken token) {return token instanceof JWTToken;}private static final String ADMIN_LOGIN_TYPE = UserConstant.WEB;{super.setName("web"); //设置realm的名字,非常重要}@Overrideprotected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();SysUser user = (SysUser)principalCollection.getPrimaryPrincipal();//这里可以进行授权和处理Set<String> rolesSet = new HashSet<>();Set<String> permsSet = new HashSet<>();//查询角色和权限(这里根据业务自行查询)List<SysRole> roleList = roleService.selectRoleByUserId(user);for (SysRole role:roleList) {rolesSet.add(role.getRoleName());List<SysMenu> menuList = menuService.selectMenuByRoleId(role.getRoleId());for (SysMenu menu :menuList) {permsSet.add(menu.getPerms());}}//将查到的权限和角色分别传入authorizationInfo中authorizationInfo.setStringPermissions(permsSet);authorizationInfo.setRoles(rolesSet);return authorizationInfo;}@Overrideprotected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {String jwt= (String) authenticationToken.getCredentials();String userName= JWTUtil.getUserName(jwt);SysUser user = userService.getUserByName(userName);//判断账号是否存在if (user == null ) {throw new CustomException(ResultEnum.USER_NOT_ERROR,"");}String userNameType = userName+"_"+UserConstant.WEB;if (redisUtil.hasKey(userNameType)){if (!JWTUtil.verify(jwt)){throw new TokenExpiredException("token认证失效,token过期,重新登陆");}else {//判断AccessToken和refreshToken的时间节点是否一致long current = (long) redisUtil.hget(userNameType, "current");if (current==JWTUtil.getExpire(jwt)){return new SimpleAuthenticationInfo(user,jwt,getName());}}}return null;}}

五、JWTFilter 过滤器

import com.alibaba.fastjson.JSONObject;import com.auth0.jwt.exceptions.TokenExpiredException;import mon.constant.UserConstant;import mon.util.JWTUtil;import mon.vo.JWTToken;import com.entity.sys.SysToken;import com.entity.sys.SysUser;import mon.util.RedisUtil;import mon.util.SpringUtil;import mon.vo.ResultVo;import com.service.sys.SysTokenService;import com.service.sys.SysUserService;import org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter;import org.springframework.http.HttpStatus;import org.springframework.web.bind.annotation.RequestMethod;import javax.servlet.ServletRequest;import javax.servlet.ServletResponse;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.IOException;import java.util.Date;import java.util.HashMap;import java.util.Map;public class JWTFilter extends BasicHttpAuthenticationFilter {//是否允许访问,如果带有 token,则对 token 进行检查@Overrideprotected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {HttpServletRequest req= (HttpServletRequest) request;//判断请求的请求头是否带上 "Token"if (isLoginAttempt(request, response)){String loginType=req.getHeader("loginType");try {//如果存在,则进入 executeLogin 方法执行登入,检查 token 是否正确executeLogin(request, response);return true;}catch (Exception e){if (UserConstant.APP.equals(loginType)){return refreshTokenApp(req,request,response);}else {/** 注意这里捕获的异常其实是在Realm抛出的,但是由于executeLogin()方法抛出的异常是从login()来的,* login抛出的异常类型是AuthenticationException,所以要去获取它的子类异常才能获取到我们在Realm抛出的异常类型。* */Throwable cause = e.getCause();if (cause!=null&&cause instanceof TokenExpiredException){//AccessToken过期,尝试去刷新tokenreturn refreshToken(request, response);}}}}return false;}@Overrideprotected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {HttpServletRequest req= (HttpServletRequest) request;String token=req.getHeader("Authorization");return token !=null;}/** executeLogin实际上就是先调用createToken来获取token,这里我们重写了这个方法,就不会自动去调用createToken来获取token* 然后调用getSubject方法来获取当前用户再调用login方法来实现登录* 这也解释了我们为什么要自定义jwtToken,因为我们不再使用Shiro默认的UsernamePasswordToken了。* */@Overrideprotected boolean executeLogin(ServletRequest request, ServletResponse response){HttpServletRequest req= (HttpServletRequest) request;String token=req.getHeader("Authorization");String loginType=req.getHeader("loginType");JWTToken jwt=new JWTToken(token,loginType);//交给自定义的realm对象去登录,如果错误他会抛出异常并被捕获getSubject(request, response).login(jwt);return true;}@Overrideprotected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {HttpServletRequest req= (HttpServletRequest) request;HttpServletResponse res= (HttpServletResponse) response;res.setHeader("Access-control-Allow-Origin",req.getHeader("Origin"));res.setHeader("Access-control-Allow-Methods","GET,POST,OPTIONS,PUT,DELETE");res.setHeader("Access-control-Allow-Headers",req.getHeader("Access-Control-Request-Headers"));// 跨域时会首先发送一个option请求,这里我们给option请求直接返回正常状态if (req.getMethod().equals(RequestMethod.OPTIONS.name())) {res.setStatus(HttpStatus.OK.value());return false;}return super.preHandle(request, response);}/*** isAccessAllowed返回false时,执行该方法* 在访问controller前判断是否登录,返回json,不进行重定向。* @return true-继续往下执行,false-该filter过滤器已经处理,不继续执行其他过滤器*/@Overrideprotected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws IOException {HttpServletResponse httpServletResponse = (HttpServletResponse) response;//这里是个坑,如果不设置的接受的访问源,那么前端都会报跨域错误,因为这里还没到corsConfig里面httpServletResponse.setHeader("Access-Control-Allow-Origin", ((HttpServletRequest) request).getHeader("Origin"));httpServletResponse.setHeader("Access-Control-Allow-Credentials", "true");httpServletResponse.setCharacterEncoding("UTF-8");httpServletResponse.setContentType("application/json");ResultVo resultVo = new ResultVo();resultVo.setCode(1003);resultVo.setMessage("用户未登录,请进行登录");httpServletResponse.getWriter().write(JSONObject.toJSON(resultVo).toString());return false;}//刷新tokenprivate boolean refreshToken(ServletRequest request,ServletResponse response) {HttpServletRequest req= (HttpServletRequest) request;RedisUtil redisUtil= SpringUtil.getBean(RedisUtil.class);//获取传递过来的accessTokenString accessToken=req.getHeader("Authorization");String loginType=req.getHeader("loginType");//获取token里面的用户名String userName = JWTUtil.getUserName(accessToken);String userNameType = userName+"_"+loginType;//判断refreshToken是否过期了,过期了那么所含的username的键不存在if (redisUtil.hasKey(userNameType)){//判断refresh的时间节点和传递过来的accessToken的时间节点是否一致,不一致校验失败long current= (long) redisUtil.hget(userNameType,"current");if (current==JWTUtil.getExpire(accessToken)){//获取当前时间节点long currentTimeMillis = System.currentTimeMillis();//生成刷新的tokenString token=JWTUtil.createToken(userName,currentTimeMillis,loginType);//刷新redis里面的refreshToken,过期时间是30minMap<String,Object> setMap = new HashMap<>();setMap.put("current",currentTimeMillis);setMap.put("userInfo",redisUtil.hget(userNameType,"userInfo"));redisUtil.hmset(userNameType,setMap,30*60);//再次交给shiro进行认证JWTToken jwtToken=new JWTToken(token,loginType);try {getSubject(request, response).login(jwtToken);// 最后将刷新的AccessToken存放在Response的Header中的Authorization字段返回HttpServletResponse httpServletResponse = (HttpServletResponse) response;httpServletResponse.setHeader("Authorization", token);httpServletResponse.setHeader("loginType", loginType);httpServletResponse.setHeader("Access-Control-Expose-Headers", "Authorization");return true;}catch (Exception e){return false;}}}return false;}/*** app刷新token*/private Boolean refreshTokenApp(ServletRequest request,ServletResponse response) {HttpServletRequest req= (HttpServletRequest) request;RedisUtil redisUtil=SpringUtil.getBean(RedisUtil.class);// 如果是app端登录,则根据token去数据库查询,有数据则刷新token。// 并将新的token保存到数据库中,没有数据则提示用户重新登录。SysTokenService tokenService = SpringUtil.getBean(SysTokenService.class);SysUserService userService = SpringUtil.getBean(SysUserService.class);String token = req.getHeader("Authorization");String loginType=req.getHeader("loginType");SysToken sysToken = tokenService.getByToken(token);if (sysToken != null) {SysUser user = userService.getById(sysToken.getUserId());long currentTimeMillis = System.currentTimeMillis();String newToken = JWTUtil.createToken(user.getUserName(), currentTimeMillis,UserConstant.APP);sysToken.setLoginTime(new Date());sysToken.setToken(newToken);tokenService.updateById(sysToken);Map<String,Object> setMap = new HashMap<>();setMap.put("current",currentTimeMillis);setMap.put("userInfo",user);redisUtil.hmset(user.getUserName()+"_"+UserConstant.APP,setMap,30*60);JWTToken jwtToken = new JWTToken(newToken,loginType);try {getSubject(request, response).login(jwtToken);// 最后将刷新的AccessToken存放在Response的Header中的Authorization字段返回HttpServletResponse httpServletResponse = (HttpServletResponse) response;httpServletResponse.setHeader("Authorization", newToken);httpServletResponse.setHeader("loginType", loginType);httpServletResponse.setHeader("Access-Control-Expose-Headers", "Authorization");return true;}catch (Exception e){e.getMessage();}}return false;}}

六、config

1、RedisConfig

import com.fasterxml.jackson.annotation.JsonAutoDetect;import com.fasterxml.jackson.annotation.PropertyAccessor;import com.fasterxml.jackson.databind.ObjectMapper;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.data.redis.connection.RedisConnectionFactory;import org.springframework.data.redis.core.RedisTemplate;import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;import org.springframework.data.redis.serializer.StringRedisSerializer;import .UnknownHostException;/*** redis序列化* @author fuhua*/@Configurationpublic class RedisConfig {//编写我们自己的redisTemplate@Beanpublic RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {//我们为了自己开发使用方便,一般使用<String, Object>类型RedisTemplate<String, Object> template = new RedisTemplate();template.setConnectionFactory(redisConnectionFactory);//序列化配置//json序列化Jackson2JsonRedisSerializer jackson2JsonRedisSerializer=new Jackson2JsonRedisSerializer(Object.class);ObjectMapper om=new ObjectMapper();om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);jackson2JsonRedisSerializer.setObjectMapper(om);//String序列化StringRedisSerializer stringRedisSerializer=new StringRedisSerializer();//key使用String的序列化方式template.setKeySerializer(stringRedisSerializer);//hash的key也使用String序列化template.setHashKeySerializer(stringRedisSerializer);//value使用json序列化template.setValueSerializer(jackson2JsonRedisSerializer);//hash的value使用json序列化template.setHashValueSerializer(jackson2JsonRedisSerializer);template.afterPropertiesSet();return template;}}

2、ShiroConfig

import mon.constant.UserConstant;import mon.filter.JWTFilter;import mon.realm.MobileRealm;import mon.realm.ModularRealm;import mon.realm.WebRealm;import org.apache.shiro.authc.Authenticator;import org.apache.shiro.authc.pam.AtLeastOneSuccessfulStrategy;import org.apache.shiro.mgt.DefaultSessionStorageEvaluator;import org.apache.shiro.mgt.DefaultSubjectDAO;import org.apache.shiro.mgt.SecurityManager;import org.apache.shiro.realm.Realm;import org.apache.shiro.spring.LifecycleBeanPostProcessor;import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;import org.apache.shiro.spring.web.ShiroFilterFactoryBean;import org.apache.shiro.web.mgt.DefaultWebSecurityManager;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import javax.servlet.Filter;import java.util.*;/*** @Description Shiro配置类*/@Configurationpublic class ShiroConfig {@Beanpublic LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {return new LifecycleBeanPostProcessor();}/*** 开启Shiro-aop注解支持* @Attention 使用代理方式所以需要开启代码支持*/@Beanpublic AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);return authorizationAttributeSourceAdvisor;}/*** Shiro基础配置*/@Beanpublic ShiroFilterFactoryBean shiroFilterFactory(SecurityManager securityManager){ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();shiroFilterFactoryBean.setSecurityManager(securityManager);Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();// 注意过滤器配置顺序不能颠倒// 配置过滤:不会被拦截的链接filterChainDefinitionMap.put("/swagger-ui.html", "anon");filterChainDefinitionMap.put("/swagger/**", "anon");filterChainDefinitionMap.put("/swagger-resources/**", "anon");filterChainDefinitionMap.put("/v2/**", "anon");filterChainDefinitionMap.put("/webjars/**", "anon");// filterChainDefinitionMap.put("/static/**", "anon");filterChainDefinitionMap.put("/uploads/**", "anon");filterChainDefinitionMap.put("/api/user/getCode", "anon");filterChainDefinitionMap.put("/api/user/login/web", "anon");filterChainDefinitionMap.put("/api/user/login/app", "anon");//将所有请求指向我们自己定义的jwt过滤器filterChainDefinitionMap.put("/**", "jwt");//获取filtersMap<String, Filter> filters = shiroFilterFactoryBean.getFilters();//设置我们自定义的JWT过滤器,并且取名为jwtfilters.put("jwt",new JWTFilter());shiroFilterFactoryBean.setFilters(filters);shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);return shiroFilterFactoryBean;}@Beanpublic Authenticator authenticator() {ModularRealm modularRealm = new ModularRealm();modularRealm.setRealms(Arrays.asList(webRealm(), mobileRealm()));modularRealm.setAuthenticationStrategy(new AtLeastOneSuccessfulStrategy());return modularRealm;}/*** 安全管理器*/@Beanpublic SecurityManager securityManager() {DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();//多realmSet<Realm> realms = new HashSet<Realm>();realms.add(mobileRealm());realms.add(webRealm());securityManager.setRealms(realms);//关闭sessionDefaultSubjectDAO subjectDAO=new DefaultSubjectDAO();DefaultSessionStorageEvaluator sessionStorageEvaluator=new DefaultSessionStorageEvaluator();sessionStorageEvaluator.setSessionStorageEnabled(false);subjectDAO.setSessionStorageEvaluator(sessionStorageEvaluator);securityManager.setSubjectDAO(subjectDAO);securityManager.setAuthenticator(authenticator());//解决多realm的异常问题重点在此return securityManager;}/*** app端的身份验证器*/@Beanpublic MobileRealm mobileRealm() {MobileRealm mobileRealm = new MobileRealm();mobileRealm.setName(UserConstant.APP);return mobileRealm;}/*** web端的身份验证器*/@Beanpublic WebRealm webRealm() {WebRealm webRealm = new WebRealm();webRealm.setName(UserConstant.WEB);return webRealm;}}

七、SysLoginController 登录的controller

import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;import mon.constant.UserConstant;import mon.enums.ResultEnum;import mon.util.*;import mon.vo.CustomException;import mon.vo.ResultVo;import com.entity.sys.SysToken;import com.entity.sys.SysUser;import com.service.sys.SysTokenService;import com.service.sys.SysUserService;import lombok.AllArgsConstructor;import org.apache.shiro.authc.AuthenticationException;import org.apache.shiro.authc.IncorrectCredentialsException;import org.apache.shiro.authc.LockedAccountException;import org.apache.shiro.authz.annotation.RequiresAuthentication;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.web.bind.annotation.*;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;import java.io.OutputStream;import java.text.ParseException;import java.text.SimpleDateFormat;import java.util.Calendar;import java.util.Date;import java.util.HashMap;import java.util.Map;@AllArgsConstructor@RestController@RequestMapping("/${api.url.prefix}/login")public class SysLoginController {@Autowiredprivate SysUserService userService;@Autowiredprivate RedisUtil redisUtil;@Autowiredprivate SysTokenService tokenService;//密码最大错误次数private static int ERROR_COUNT = 3;/*** web端登录*/@PostMapping("/web")public ResultVo web(String userName, String password,String code){try {Object verCode = redisUtil.get("verCode");if (null == verCode)return ResultUtil.error("验证码已失效,请重新输入");String verCodeStr = verCode.toString();if (verCodeStr == null || StringUtils.isEmpty(code) || !verCodeStr.equalsIgnoreCase(code))return ResultUtil.error("验证码错误");else if (!redisUtil.hasKey("verCode"))return ResultUtil.error("验证码已过期,请重新输入");elseredisUtil.del("verCode");String salt = userService.getSalt(userName);password = SHA256Util.sha256(password, salt);//验证用户名和密码SysUser user = passwordErrorNum(userName,password);long currentTimeMillis = System.currentTimeMillis();String token= JWTUtil.createToken(user.getUserName(),currentTimeMillis,UserConstant.WEB);Map<String, Object> map = new HashMap<>();map.put("current",currentTimeMillis);map.put("userInfo",user);redisUtil.hmset(userName+"_"+UserConstant.WEB,map,60*30);return ResultUtil.success(token);}catch (IncorrectCredentialsException e) {return ResultUtil.error(1000,e.getMessage());} catch (LockedAccountException e) {return ResultUtil.error(1004,e.getMessage());} catch (AuthenticationException e) {return ResultUtil.error(ResultEnum.USER_NOT_ERROR);} catch (Exception e) {return ResultUtil.error(ResultEnum.UNKNOWN_EXCEPTION);}}/*** web登录获取验证码*/@RequestMapping(value = "/getCode", method = RequestMethod.GET)public void getCode(HttpServletRequest request, HttpServletResponse response) {try {response.setHeader("Pragma", "No-cache");response.setHeader("Cache-Control", "no-cache");response.setDateHeader("Expires", 0);response.setContentType("image/jpeg");// 生成随机字串String verifyCode = VerifyCodeUtils.generateVerifyCode(4);//将验证码存入redis中,设置有效期为一分钟redisUtil.set("verCode",verifyCode,60);// 生成图片int w = 200, h = 50;OutputStream out = response.getOutputStream();VerifyCodeUtils.outputImage(w, h, out, verifyCode);} catch (Exception e) {e.printStackTrace();}}/*** app端登录*/@PostMapping("/app")public ResultVo app(String userName, String password){try {String salt = userService.getSalt(userName);password = SHA256Util.sha256(password, salt);//验证用户名和密码SysUser user = passwordErrorNum(userName,password);long currentTimeMillis = System.currentTimeMillis();String token= JWTUtil.createToken(user.getUserName(),currentTimeMillis,UserConstant.APP);SysToken sysToken = new SysToken();sysToken.setUserId(user.getId());sysToken.setToken(token);sysToken.setLoginTime(new Date());tokenService.save(sysToken);Map<String, Object> map = new HashMap<>();map.put("current",currentTimeMillis);map.put("userInfo",user);redisUtil.hmset(userName+"_"+UserConstant.APP,map,60*30);return ResultUtil.success(token);}catch (IncorrectCredentialsException e) {return ResultUtil.error(1000,e.getMessage());} catch (LockedAccountException e) {return ResultUtil.error(1004,e.getMessage());} catch (AuthenticationException e) {return ResultUtil.error(ResultEnum.USER_NOT_ERROR);} catch (Exception e) {return ResultUtil.error(ResultEnum.UNKNOWN_EXCEPTION);}}/*** 退出登录*/@DeleteMapping("/logout")@RequiresAuthenticationpublic ResultVo logout(HttpServletRequest request){String token = request.getHeader("Authorization");String loginType = request.getHeader("loginType");if (UserConstant.APP.equals(loginType)){SysToken sysToken = tokenService.getByToken(token);if (sysToken != null){tokenService.removeById(sysToken.getId());}}String username=JWTUtil.getUserName(token);redisUtil.del(username+"_"+loginType);return ResultUtil.success("退出登录成功");}/*** 密码错误次数验证* @param userName* @param password* @return*/private SysUser passwordErrorNum(String userName,String password){//查询用户SysUser user = userService.getUserByName(userName);if (null == user){throw new AuthenticationException();}/*Safe securitySet = securitySetService.getById(1);//密码登录限制(0:连续错3次,锁定账号15分钟。1:连续错5次,锁定账号30分钟)if (securitySet.getPwdLoginLimit()==1){ERROR_COUNT = 5;}*///登录时间Date allowTime = user.getLoginDate() == null ? new Date() : user.getLoginDate();//当前时间Date currentTime = new Date();try {SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");allowTime = sdf.parse(sdf.format(allowTime));}catch (ParseException e){throw new CustomException(-1,"日期转换异常","");}UpdateWrapper<SysUser> updateWrapper = new UpdateWrapper<>();//如果当前登录时间大于允许登录时间if (allowTime == null || currentTime.getTime() > allowTime.getTime()) {// 判断用户账号和密码是否正确user = userService.getUserByPass(userName, password);if (user != null) {//正确密码错误次数清零updateWrapper.set("error_num",0);updateWrapper.set("login_date",new Date());updateWrapper.eq("id",user.getId());userService.update(updateWrapper);} else {//登录错误次数int errorNum = user.getErrorNum();//最后登录的时间long allowTimes = user.getLoginDate() == null ? 0 : user.getLoginDate().getTime();//错误的次数if (errorNum < ERROR_COUNT-1) {int surplusCount = ERROR_COUNT - errorNum-1;boolean result;//每次输入错误密码间隔时间在2分钟内 (如果上次登录错误时间距离相差小于定义的时间(毫秒))if ((currentTime.getTime() - allowTimes) <= 120000) {updateWrapper.set("error_num",errorNum + 1);updateWrapper.set("login_date",new Date());updateWrapper.eq("id",user.getId());result = userService.update(updateWrapper);} else {updateWrapper.set("error_num",1);updateWrapper.set("login_date",new Date());updateWrapper.eq("id",user.getId());result = userService.update(updateWrapper);}if (result) {//抛出密码错误异常throw new IncorrectCredentialsException("密码错误,总登录次数"+ERROR_COUNT+"次,剩余次数: " + surplusCount);}} else {//错误3次,锁定15分钟后才可登陆 允许时间加上定义的登陆时间(毫秒)Date dateAfterAllowTime = new Date(currentTime.getTime() + 900000);String str = "15";if (ERROR_COUNT == 5){//错误5次,锁定30分钟后才可登陆 允许时间加上定义的登陆时间(毫秒)dateAfterAllowTime = new Date(currentTime.getTime() + 1800000);str = "30";}updateWrapper.set("error_num",0);updateWrapper.set("login_date",dateAfterAllowTime);updateWrapper.eq("id",user.getId());if (userService.update(updateWrapper)) {throw new LockedAccountException("您的密码已错误"+ERROR_COUNT+"次,现已被锁定,请"+str+"分钟后再尝试");}}}}else {Calendar calendar = Calendar.getInstance();calendar.setTime(allowTime);long time1 = calendar.get(Calendar.MINUTE);calendar.setTime(currentTime);long time2 = calendar.get(Calendar.MINUTE);long between_minute=(time1-time2);throw new LockedAccountException("账号锁定,还没到允许登录的时间,请"+between_minute+"分钟后再尝试");}return user;}}

八、yml

server:# 服务器端口号port: 8081spring:# 配置数据库连接池datasource:url: jdbc:mysql://127.0.0.1:3306/my_shiro?useUnicode=true&characterEncoding=utf8&allowMultiQueries=true&useSSL=false&serverTimezone=UTCusername: rootpassword: roottype: com.zaxxer.hikari.HikariDataSourcedriver-class-name: com.mysql.cj.jdbc.Driverhikari:# 最小连接minimum-idle: 5# 最大连接maximum-pool-size: 15# 自动提交auto-commit: true# 最大空闲时间idle-timeout: 30000# 连接池名称pool-name: DatebookHikariCP# 最大生命周期max-lifetime: 900000# 连接超时时间connection-timeout: 15000# 心跳检测connection-test-query: select 1# 配置Redisredis:host: localhostport: 6379timeout: 6000 #以秒为单位password: 123456database: 0lettuce:pool:max-active: -1max-wait: -1max-idle: 16min-idle: 8main:allow-bean-definition-overriding: trueservlet:multipart:max-file-size: -1max-request-size: -1# mybatis_plus#mybatis-plus:# xml路径# mapper-locations: classpath:mapping/*Mapper.xml# mybatis-plus相关配置mybatis-plus:# xml扫描,多个目录用逗号或者分号分隔(告诉 Mapper 所对应的 XML 文件位置)mapper-locations: classpath:mapper/*/*.xml# 注意:对应实体类的路径type-aliases-package: com.entity.sys,;mon.basic.entity# 以下配置均有默认值,可以不设置global-config:db-config:#主键类型 AUTO:"数据库ID自增" INPUT:"用户输入ID",ID_WORKER:"全局唯一ID (数字类型唯一ID)", UUID:"全局唯一ID UUID";id-type: auto#字段策略 IGNORED:"忽略判断" NOT_NULL:"非 NULL 判断") NOT_EMPTY:"非空判断"field-strategy: NOT_EMPTY#数据库类型db-type: MYSQLconfiguration:# 是否开启自动驼峰命名规则映射:从数据库列名到Java属性驼峰命名的类似映射map-underscore-to-camel-case: true# 返回map时true:当查询数据为空时字段返回为null,false:不加这个查询数据为空时,字段将被隐藏call-setters-on-nulls: true# 这个配置会将执行的sql打印出来,在开发或测试的时候可以用log-impl: org.apache.ibatis.logging.stdout.StdOutImplapi.url.prefix: /api

九、其他

1、GlobalExceptionConfig 全局异常处理

import mon.enums.ResultEnum;import mon.util.ResultUtil;import mon.vo.CustomException;import mon.vo.ResultVo;import lombok.extern.slf4j.Slf4j;import org.apache.shiro.authz.UnauthorizedException;import org.springframework.http.HttpStatus;import org.springframework.http.converter.HttpMessageNotReadableException;import org.springframework.validation.BindException;import org.springframework.validation.BindingResult;import org.springframework.web.HttpRequestMethodNotSupportedException;import org.springframework.web.bind.MethodArgumentNotValidException;import org.springframework.web.bind.annotation.ExceptionHandler;import org.springframework.web.bind.annotation.ResponseStatus;import org.springframework.web.bind.annotation.RestControllerAdvice;import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;import java.util.Objects;/*** 全局异常处理*/@Slf4j@RestControllerAdvicepublic class GlobalExceptionConfig {/*** 自定义异常*/@ExceptionHandler(value = CustomException.class)public ResultVo processException(CustomException e) {log.error("位置:{} -> 错误信息:{}", e.getMethod() ,e.getLocalizedMessage());return ResultUtil.error(Objects.requireNonNull(ResultEnum.getByCode(e.getCode())));}/*** 拦截表单参数校验*/@ResponseStatus(HttpStatus.OK)@ExceptionHandler({BindException.class})public ResultVo bindException(BindException e) {BindingResult bindingResult = e.getBindingResult();return ResultUtil.error(Objects.requireNonNull(bindingResult.getFieldError()).getDefaultMessage());}/*** 拦截JSON参数校验*/@ResponseStatus(HttpStatus.OK)@ExceptionHandler(MethodArgumentNotValidException.class)public ResultVo bindException(MethodArgumentNotValidException e) {BindingResult bindingResult = e.getBindingResult();return ResultUtil.error(Objects.requireNonNull(bindingResult.getFieldError()).getDefaultMessage());}/*** 参数格式错误*/@ExceptionHandler(MethodArgumentTypeMismatchException.class)public ResultVo methodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e) {log.error("错误信息{}", e.getLocalizedMessage());return ResultUtil.error(ResultEnum.ARGUMENT_TYPE_MISMATCH);}/*** 参数格式错误*/@ExceptionHandler(HttpMessageNotReadableException.class)public ResultVo httpMessageNotReadable(HttpMessageNotReadableException e) {log.error("错误信息:{}", e.getLocalizedMessage());return ResultUtil.error(ResultEnum.FORMAT_ERROR);}/*** 请求方式不支持*/@ExceptionHandler(HttpRequestMethodNotSupportedException.class)public ResultVo httpReqMethodNotSupported(HttpRequestMethodNotSupportedException e) {log.error("错误信息:{}", e.getLocalizedMessage());return ResultUtil.error(ResultEnum.REQ_METHOD_NOT_SUPPORT);}/*** 通用异常*/@ResponseStatus(HttpStatus.OK)@ExceptionHandler(Exception.class)public ResultVo exception(Exception e) {//权限不足异常if (e instanceof UnauthorizedException) {return ResultUtil.error(ResultEnum.SHIRO_ERROR);}e.printStackTrace();return ResultUtil.error(ResultEnum.UNKNOWN_EXCEPTION);}}

2、UserConstant

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

3、ResultEnum 返回状态枚举类

import lombok.Getter;/*** 返回状态枚举类*/@Getterpublic enum ResultEnum {/*** 未知异常*/UNKNOWN_EXCEPTION(100, "未知异常"),/*** 请求方式不支持*/REQ_METHOD_NOT_SUPPORT(101,"请求方式不支持"),/*** 格式错误*/FORMAT_ERROR(102, "参数格式错误"),/*** 文件格式错误*/FILE_FORMAT_ERROR(103,"文件格式错误"),FILE_PATH_ERROR(105,"文件上传路径错误"),FILE_NAME_NOT_NULL(106,"文件名不可为空"),/*** 参数类型不匹配*/ARGUMENT_TYPE_MISMATCH(104, "参数类型不匹配"),/*** 添加失败*/ADD_ERROR(2000, "添加失败"),/*** 更新失败*/UPDATE_ERROR(2001, "更新失败"),/*** 删除失败*/DELETE_ERROR(2002, "删除失败"),/*** 查找失败*/GET_ERROR(, "查询失败,数据可能不存在"),/*** 导入失败*/IMPORT_ERROR(,"导入失败"),/*** 用户名或密码错误* */USER_PWD_ERROR(1000, "用户名或密码错误"),/*** 用户不存在* */USER_NOT_ERROR(1001, "用户不存在"),/** 登录超时,请重新登录 */LOGIN_TIME_OUT(1002,"登录超时,请重新登录"),/** 用户未登录,请进行登录 */USER_NOT_LOGIN(1003,"用户未登录,请进行登录"),/** 账号锁定 */USER_LOCK(1004,"账号锁定中"),/*** 非法令牌*/ILLEGAL_TOKEN(5000,"非法令牌"),/*** 其他客户端登录*/OTHER_CLIENT_LOGIN(5001,"其他客户端登录"),/*** 令牌过期*/TOKEN_EXPIRED(5002,"令牌过期"),/*** 权限不足*/SHIRO_ERROR(403,"权限不足");;private Integer code;private String msg;ResultEnum(Integer code, String msg) {this.code = code;this.msg = msg;}/*** 通过状态码获取枚举对象* @param code 状态码* @return 枚举对象*/public static ResultEnum getByCode(int code){for (ResultEnum resultEnum : ResultEnum.values()) {if(code == resultEnum.getCode()){return resultEnum;}}return null;}}

4、ResultUtil 返回数据工具类

import mon.enums.ResultEnum;import mon.vo.ResultVo;import java.util.List;import java.util.Map;/*** 返回数据工具类*/public class ResultUtil {/*** 私有化工具类 防止被实例化*/private ResultUtil() {}/*** 成功* @param object 需要返回的数据* @return data*/public static ResultVo success(Object object) {ResultVo result = new ResultVo();result.setCode(0);result.setMessage("ok");result.setData(object);return result;}/*** 成功* @param map 需要返回的数据* @return data*/public static ResultVo success(Map<String, List> map) {ResultVo result = new ResultVo();result.setCode(0);result.setMessage("ok");result.setData(map);return result;}/*** 成功*/public static ResultVo success(Integer code,String msg) {ResultVo result = new ResultVo();result.setCode(code);result.setMessage(msg);return result;}/*** 成功* @return 返回空*/public static ResultVo success() {return success(null);}/*** 错误* @param resultEnum 错误枚举类* @return 错误信息*/public static ResultVo error(ResultEnum resultEnum) {ResultVo result = new ResultVo();result.setCode(resultEnum.getCode());result.setMessage(resultEnum.getMsg());return result;}/*** 错误* @param code 状态码* @param msg 消息* @return ResultBean*/public static ResultVo error(Integer code, String msg) {ResultVo result = new ResultVo();result.setCode(code);result.setMessage(msg);return result;}/*** 错误* @param msg 错误信息* @return ResultBean*/public static ResultVo error(String msg) {return error(-1, msg);}}

5、SHA256Util 密码加密工具类

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

6、VerifyCodeUtils 验证码生成工具类

import javax.imageio.ImageIO;import java.awt.*;import java.awt.geom.AffineTransform;import java.awt.image.BufferedImage;import java.io.File;import java.io.FileOutputStream;import java.io.IOException;import java.io.OutputStream;import java.util.Arrays;import java.util.Random;/*** 生成验证码*/public class VerifyCodeUtils {//使用到Algerian字体,系统里没有的话需要安装字体,字体只显示大写,去掉了1,0,i,o几个容易混淆的字符public static final String VERIFY_CODES = "23456789ABCDEFGHJKLMNPQRSTUVWXYZ";private static Random random = new Random();/*** 使用系统默认字符源生成验证码** @param verifySize 验证码长度* @return*/public static String generateVerifyCode(int verifySize) {return generateVerifyCode(verifySize, VERIFY_CODES);}/*** 使用指定源生成验证码** @param verifySize 验证码长度* @param sources 验证码字符源* @return*/public static String generateVerifyCode(int verifySize, String sources) {if (sources == null || sources.length() == 0) {sources = VERIFY_CODES;}int codesLen = sources.length();Random rand = new Random(System.currentTimeMillis());StringBuilder verifyCode = new StringBuilder(verifySize);for (int i = 0; i < verifySize; i++) {verifyCode.append(sources.charAt(rand.nextInt(codesLen - 1)));}return verifyCode.toString();}/*** 生成随机验证码文件,并返回验证码值** @param w* @param h* @param outputFile* @param verifySize* @return* @throws IOException*/public static String outputVerifyImage(int w, int h, File outputFile, int verifySize) throws IOException {String verifyCode = generateVerifyCode(verifySize);outputImage(w, h, outputFile, verifyCode);return verifyCode;}/*** 输出随机验证码图片流,并返回验证码值** @param w* @param h* @param os* @param verifySize* @return* @throws IOException*/public static String outputVerifyImage(int w, int h, OutputStream os, int verifySize) throws IOException {String verifyCode = generateVerifyCode(verifySize);outputImage(w, h, os, verifyCode);return verifyCode;}/*** 生成指定验证码图像文件** @param w* @param h* @param outputFile* @param code* @throws IOException*/public static void outputImage(int w, int h, File outputFile, String code) throws IOException {if (outputFile == null) {return;}File dir = outputFile.getParentFile();if (!dir.exists()) {dir.mkdirs();}try {outputFile.createNewFile();FileOutputStream fos = new FileOutputStream(outputFile);outputImage(w, h, fos, code);fos.close();} catch (IOException e) {throw e;}}/*** 输出指定验证码图片流** @param w* @param h* @param os* @param code* @throws IOException*/public static void outputImage(int w, int h, OutputStream os, String code) throws IOException {int verifySize = code.length();BufferedImage image = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);Random rand = new Random();Graphics2D g2 = image.createGraphics();g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);Color[] colors = new Color[5];Color[] colorSpaces = new Color[]{Color.WHITE, Color.CYAN,Color.GRAY, Color.LIGHT_GRAY, Color.MAGENTA, Color.ORANGE,Color.PINK, Color.YELLOW};float[] fractions = new float[colors.length];for (int i = 0; i < colors.length; i++) {colors[i] = colorSpaces[rand.nextInt(colorSpaces.length)];fractions[i] = rand.nextFloat();}Arrays.sort(fractions);g2.setColor(Color.GRAY);// 设置边框色g2.fillRect(0, 0, w, h);Color c = getRandColor(200, 250);g2.setColor(c);// 设置背景色g2.fillRect(0, 2, w, h - 4);//绘制干扰线Random random = new Random();g2.setColor(getRandColor(160, 200));// 设置线条的颜色for (int i = 0; i < 20; i++) {int x = random.nextInt(w - 1);int y = random.nextInt(h - 1);int xl = random.nextInt(6) + 1;int yl = random.nextInt(12) + 1;g2.drawLine(x, y, x + xl + 40, y + yl + 20);}// 添加噪点float yawpRate = 0.05f;// 噪声率int area = (int) (yawpRate * w * h);for (int i = 0; i < area; i++) {int x = random.nextInt(w);int y = random.nextInt(h);int rgb = getRandomIntColor();image.setRGB(x, y, rgb);}shear(g2, w, h, c);// 使图片扭曲g2.setColor(getRandColor(100, 160));int fontSize = h - 4;Font font = new Font("Algerian", Font.ITALIC, fontSize);g2.setFont(font);char[] chars = code.toCharArray();for (int i = 0; i < verifySize; i++) {AffineTransform affine = new AffineTransform();affine.setToRotation(Math.PI / 4 * rand.nextDouble() * (rand.nextBoolean() ? 1 : -1), (w / verifySize) * i + fontSize / 2, h / 2);g2.setTransform(affine);g2.drawChars(chars, i, 1, ((w - 10) / verifySize) * i + 5, h / 2 + fontSize / 2 - 10);}g2.dispose();ImageIO.write(image, "jpg", os);}private static Color getRandColor(int fc, int bc) {if (fc > 255)fc = 255;if (bc > 255)bc = 255;int r = fc + random.nextInt(bc - fc);int g = fc + random.nextInt(bc - fc);int b = fc + random.nextInt(bc - fc);return new Color(r, g, b);}private static int getRandomIntColor() {int[] rgb = getRandomRgb();int color = 0;for (int c : rgb) {color = color << 8;color = color | c;}return color;}private static int[] getRandomRgb() {int[] rgb = new int[3];for (int i = 0; i < 3; i++) {rgb[i] = random.nextInt(255);}return rgb;}private static void shear(Graphics g, int w1, int h1, Color color) {shearX(g, w1, h1, color);shearY(g, w1, h1, color);}private static void shearX(Graphics g, int w1, int h1, Color color) {int period = random.nextInt(2);boolean borderGap = true;int frames = 1;int phase = random.nextInt(2);for (int i = 0; i < h1; i++) {double d = (double) (period >> 1)* Math.sin((double) i / (double) period+ (6.2831853071795862D * (double) phase)/ (double) frames);g.copyArea(0, i, w1, 1, (int) d, 0);if (borderGap) {g.setColor(color);g.drawLine((int) d, i, 0, i);g.drawLine((int) d + w1, i, w1, i);}}}private static void shearY(Graphics g, int w1, int h1, Color color) {int period = random.nextInt(40) + 10; // 50;boolean borderGap = true;int frames = 20;int phase = 7;for (int i = 0; i < w1; i++) {double d = (double) (period >> 1)* Math.sin((double) i / (double) period+ (6.2831853071795862D * (double) phase)/ (double) frames);g.copyArea(i, 0, 1, h1, 0, (int) d);if (borderGap) {g.setColor(color);g.drawLine(i, (int) d, i, 0);g.drawLine(i, (int) d + h1, i, h1);}}}}

7、CustomException 自定义异常处理类

import mon.enums.ResultEnum;import lombok.Data;import lombok.EqualsAndHashCode;/*** 自定义异常*/@Data@EqualsAndHashCode(callSuper = false)public class CustomException extends RuntimeException {/*** 状态码*/private final Integer code;/*** 方法名称*/private final String method;/*** 自定义异常** @param resultEnum 返回枚举对象* @param method方法*/public CustomException(ResultEnum resultEnum, String method) {super(resultEnum.getMsg());this.code = resultEnum.getCode();this.method = method;}/*** @param code 状态码* @param message 错误信息* @param method 方法*/public CustomException(Integer code, String message, String method) {super(message);this.code = code;this.method = method;}}

8、ResultVo 固定返回格式

import lombok.Data;/*** 固定返回格式*/@Datapublic class ResultVo {/*** 错误码*/private Integer code;/*** 提示信息*/private String message;/*** 具体的内容*/private Object data;}

最后

源码

-05-13 更新

1、

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

2、JWTFilterisAccessAllowed方法:

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

3、JWTFilter 中 refreshTokenApp方法:

//将这两句:// JWTToken jwtToken = new JWTToken(token,loginType);// httpServletResponse.setHeader("Authorization", token);//改为:(粗心了,参数不小心写的和 refreshToken 中的一样。。其实是因为下面这一段代码是直接复制的 refreshToken 中的。。。)JWTToken jwtToken = new JWTToken(newToken,loginType);httpServletResponse.setHeader("Authorization", newToken);

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。