之前写了篇 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的可操作性更大一些。
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starterartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-aopartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<optional>trueoptional>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>8.0.22version>
<scope>runtimescope>
dependency>
<dependency>
<groupId>com.baomidougroupId>
<artifactId>mybatis-plus-boot-starterartifactId>
<version>3.1.2version>
dependency>
<dependency>
<groupId>net.sf.json-libgroupId>
<artifactId>json-libartifactId>
<version>2.4version>
<classifier>jdk15classifier>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>fastjsonartifactId>
<version>1.2.7version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
<dependency>
<groupId>org.apache.shirogroupId>
<artifactId>shiro-springartifactId>
<version>1.4.0version>
dependency>
<dependency>
<groupId>com.auth0groupId>
<artifactId>java-jwtartifactId>
<version>3.2.0version>
dependency>
<dependency>
<groupId>org.apache.commonsgroupId>
<artifactId>commons-pool2artifactId>
<version>2.0version>
dependency>
<dependency>
<groupId>org.apache.commonsgroupId>
<artifactId>commons-lang3artifactId>
<version>3.5version>
dependency>
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;
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;
}
}
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;
}
}
}
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;
}
}
}
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);
}
}
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);
}
}
}
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;
}
}
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;
}
}
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;
}
}
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 {
//我们为了自己开发使用方便,一般使用类型
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;
}
}
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;
}
}
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;
}
}
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
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);
}
}
public interface UserConstant {
String APP = "app";
String WEB = "web";
}
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;
}
}
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);
}
}
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();
}
}
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);
}
}
}
}
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;
}
}
import lombok.Data;
/**
* 固定返回格式
*/
@Data
public class ResultVo {
/**
* 错误码
*/
private Integer code;
/**
* 提示信息
*/
private String message;
/**
* 具体的内容
*/
private Object data;
}