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

前言

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

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

2、数据库

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

二、JWTToken,继承 AuthenticationToken

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

三、工具类

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

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

2、RedisUtil

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

3、SpringUtil

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

四、realm

1、ModularRealm 多realm管理器

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

2、MobileRealm 移动端的realm

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

3、WebRealm PC端的realm

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

五、JWTFilter 过滤器

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

六、config

1、RedisConfig

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

2、ShiroConfig

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

七、SysLoginController 登录的controller

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

八、yml

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

九、其他

1、GlobalExceptionConfig 全局异常处理

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

2、UserConstant

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

3、ResultEnum 返回状态枚举类

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

4、ResultUtil 返回数据工具类

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

5、SHA256Util 密码加密工具类

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

6、VerifyCodeUtils 验证码生成工具类

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

7、CustomException 自定义异常处理类

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

8、ResultVo 固定返回格式

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

最后

源码