一、准备工作
①:创建一个新项目
1.事先创建好一些包
②:引入依赖
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><!--mysql驱动 --><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>8.0.11</version></dependency><!--支持使用 JDBC 访问数据库 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-jdbc</artifactId></dependency><!--整合mybatis plus https://baomidou.com/--><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.4.1</version></dependency><!-- mybatis-plus-generator --><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-generator</artifactId><version>3.4.1</version></dependency><!--数据源 --><dependency><groupId>com.alibaba</groupId><artifactId>druid</artifactId><version>1.2.16</version></dependency><!--引入hutool--><dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.8.18</version></dependency><!-- springboot security --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><!-- redis--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><!-- jwt --><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.9.1</version></dependency><!-- 图片验证码生成器--><dependency><groupId>com.github.axet</groupId><artifactId>kaptcha</artifactId><version>0.0.9</version></dependency><dependency><groupId>org.apache.commons</groupId><artifactId>commons-lang3</artifactId><version>3.11</version></dependency><!-- 生成配置元数据--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-configuration-processor</artifactId><optional>true</optional></dependency><!-- 参数校验 如:@NotBlank(message = "name为必传参数") private String name;--><dependency><groupId>javax.validation</groupId><artifactId>validation-api</artifactId></dependency><!-- 导入 knife4j生成接口文档--><dependency><groupId>com.github.xiaoymin</groupId><artifactId>knife4j-spring-boot-starter</artifactId><version>3.0.3</version></dependency>
③:添加一个测试接口查看效果
1.TestController
@RestController
@Api(tags = "测试专用接口")
public class TestController {@GetMapping("hello")@ApiOperation("测试接口hello")public String hello(){return "您请求了一个测试接口-hello";}
}
2.启动查看效果
访问http://localhost:8083/hello
- 会自动跳到Springsecurity的登录页面(程序已经被SpringSecurity保护)
- 没有配置用户名和密码时 默认用户user 密码 在控制台
3.登录成功可以看到(引入SpringSecurity测试成功)
④:创建工具类和统一响应类
01.工具类
1.创建Redis工具了
@Component
public class RedisUtil {@Autowiredprivate RedisTemplate redisTemplate;/*** 指定缓存失效时间** @param key 键* @param time 时间(秒)* @return*/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 可以传一个值 或多个*/@SuppressWarnings("unchecked")public void del(String... key) {if (key != null && key.length > 0) {if (key.length == 1) {redisTemplate.delete(key[0]);} else {redisTemplate.delete(CollectionUtils.arrayToList(key));}}}//============================String============================= /*** 普通缓存获取** @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)* @return*/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)* @return*/public long decr(String key, long delta) {if (delta < 0) {throw new RuntimeException("递减因子必须大于0");}return redisTemplate.opsForValue().increment(key, -delta);}//================================Map================================= /*** HashGet** @param key 键 不能为null* @param item 项 不能为null* @return 值*/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 对应多个键值* @return true 成功 false 失败*/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)* @return*/public double hincr(String key, String item, double by) {return redisTemplate.opsForHash().increment(key, item, by);}/*** hash递减** @param key 键* @param item 项* @param by 要减少记(小于0)* @return*/public double hdecr(String key, String item, double by) {return redisTemplate.opsForHash().increment(key, item, -by);}//============================set============================= /*** 根据key获取Set中的所有值** @param key 键* @return*/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 键* @return*/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================================= /*** 获取list缓存的内容** @param key 键* @param start 开始* @param end 结束 0 到 -1代表所有值* @return*/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 键* @return*/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倒数第二个元素,依次类推* @return*/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 值* @return*/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 时间(秒)* @return*/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;}}//================有序集合 sort set===================/*** 有序set添加元素** @param key* @param value* @param score* @return*/public boolean zSet(String key, Object value, double score) {return redisTemplate.opsForZSet().add(key, value, score);}public long batchZSet(String key, Set<ZSetOperations.TypedTuple> typles) {return redisTemplate.opsForZSet().add(key, typles);}public void zIncrementScore(String key, Object value, long delta) {redisTemplate.opsForZSet().incrementScore(key, value, delta);}public void zUnionAndStore(String key, Collection otherKeys, String destKey) {redisTemplate.opsForZSet().unionAndStore(key, otherKeys, destKey);}/*** 获取zset数量* @param key* @param value* @return*/public long getZsetScore(String key, Object value) {Double score = redisTemplate.opsForZSet().score(key, value);if(score==null){return 0;}else{return score.longValue();}}/*** 获取有序集 key 中成员 member 的排名 。* 其中有序集成员按 score 值递减 (从大到小) 排序。* @param key* @param start* @param end* @return*/public Set<ZSetOperations.TypedTuple> getZSetRank(String key, long start, long end) {return redisTemplate.opsForZSet().reverseRangeWithScores(key, start, end);}}
2.创建
RedisConfig
自定义key和value的序列化(避免出现乱码)
@Configuration
public class RedisConfig {@Bean// 定义 RedisTemplate BeanRedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {// 创建 RedisTemplate 实例RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();// 设置连接工厂redisTemplate.setConnectionFactory(redisConnectionFactory);// 配置 JSON 序列化器Jackson2JsonRedisSerializer<Object> redisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);redisSerializer.setObjectMapper(new ObjectMapper());// 设置键的序列化器为 StringRedisSerializerredisTemplate.setKeySerializer(new StringRedisSerializer());// 设置值的序列化器为 StringRedisSerializerredisTemplate.setValueSerializer(new StringRedisSerializer());// 设置哈希键的序列化器为 StringRedisSerializerredisTemplate.setHashKeySerializer(new StringRedisSerializer());// 设置哈希值的序列化器为 StringRedisSerializerredisTemplate.setHashValueSerializer(new StringRedisSerializer());return redisTemplate;}
}
3.Jwt工具类 创建jwt和校验jwt
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;import java.util.Date;@Data
@Component
@ConfigurationProperties(prefix = "coke.jwt")
public class JwtUtils {// JWT 过期时间(单位:秒)private long expire;// JWT 密钥,用于签名和验证private String secret;// JWT 头部字段,可自定义private String header;/*** 生成 JWT** @param username 用户名* @return JWT 字符串*/public String generateToken(String username) {// 获取当前时间Date nowDate = new Date();// 计算过期时间,当前时间 + 过期时长Date expireDate = new Date(nowDate.getTime() + expire);// 使用 JWT Builder 构建 JWTreturn Jwts.builder().setHeaderParam("typ", "JWT") // 设置头部信息,通常为JWT.setSubject(username) // 设置主题,通常为用户名.setIssuedAt(nowDate) // 设置签发时间,即当前时间.setExpiration(expireDate) // 设置过期时间.signWith(SignatureAlgorithm.HS512, secret) // 使用HS512签名算法和密钥进行签名.compact();}/*** 解析 JWT 获取声明** @param jwt JWT 字符串* @return JWT 中的声明部分*/public Claims getClaimByToken(String jwt) {try {// 使用 JWT 解析器解析 JWT,并获取声明部分return Jwts.parser().setSigningKey(secret) // 设置解析时的密钥,必须与生成时的密钥一致.parseClaimsJws(jwt).getBody();} catch (Exception e) {// 解析失败,返回nullreturn null;}}/*** 检查 JWT 是否过期** @param claims JWT 中的声明部分* @return 是否过期*/public boolean isTokenExpired(Claims claims) {// 检查过期时间是否在当前时间之前return claims.getExpiration().before(new Date());}}
4.jwt工具类中读取了ym配置文件中的
coke.jwt
配置如下
server:port: 8083
coke:jwt:header: Authorizationexpire: 604800 #7天,秒单位secret: ji8n3439n439n43ld9ne9343fdfer49h
02.统一响应类
1.Response
@Data
public class Response<T> {/*** 结果** @mock true*/private boolean success;/*** 状态码** @mock 200*/private int code;/*** 消息提示** @mock 操作成功*/private String msg;/*** 结果体** @mock null*/private T data;public Response () {}public Response (int code, Object status) {super();this.code = code;this.msg = status.toString();if (code == 1) {this.success = true;} else {this.success = false;}}public Response (int code, String status, T result) {super();this.code = code;this.msg = status;this.data = result;if (code == 1) {this.success = true;} else {this.success = false;}}public static Response<?> ok() {return new Response<>(1, "success");}public static <T> Response<T> ok(T t) {return new Response<T>(1, "success", t);}public static Response<?> error(String status) {return new Response<>(500, status);}public static Response<?> error(int code, String status) {return new Response<>(code, status);}
}
2.添加一个常量类
Const
public class Const {public final static String CAPTCHA_KEY = "captcha";public final static String Login_Key = "login";
}
⑤:数据库 数据准备
01.yml数据库配置
spring:datasource:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://1.11.94.14:3306/security_study?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghaiusername: rootpassword: wwwthymeleaf: # 是否使用springboot静态文件缓存 true 当修改静态文件需要重启服务器 false 浏览器端刷新就可以了cache: falsecheck-template: trueredis:host: 1.107.94.114password: wwwport: 6379mybatis-plus:mapper-locations: classpath*:/mapper/**Mapper.xml
02.添加数据
SET FOREIGN_KEY_CHECKS=0;-- ----------------------------
-- Table structure for sys_menu
-- ----------------------------
DROP TABLE IF EXISTS `sys_menu`;
CREATE TABLE `sys_menu` (`id` bigint(20) NOT NULL AUTO_INCREMENT,`parent_id` bigint(20) DEFAULT NULL COMMENT '父菜单ID,一级菜单为0',`name` varchar(64) NOT NULL,`path` varchar(255) DEFAULT NULL COMMENT '菜单URL',`perms` varchar(255) DEFAULT NULL COMMENT '授权(多个用逗号分隔,如:user:list,user:create)',`component` varchar(255) DEFAULT NULL,`type` int(5) NOT NULL COMMENT '类型 0:目录 1:菜单 2:按钮',`icon` varchar(32) DEFAULT NULL COMMENT '菜单图标',`orderNum` int(11) DEFAULT NULL COMMENT '排序',`created` datetime NOT NULL,`updated` datetime DEFAULT NULL,`statu` int(5) NOT NULL,PRIMARY KEY (`id`),UNIQUE KEY `name` (`name`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=20 DEFAULT CHARSET=utf8;-- ----------------------------
-- Records of sys_menu
-- ----------------------------
INSERT INTO `sys_menu` VALUES ('1', '0', '系统管理', '', 'sys:manage', '', '0', 'el-icon-s-operation', '1', '2021-01-15 18:58:18', '2021-01-15 18:58:20', '1');
INSERT INTO `sys_menu` VALUES ('2', '1', '用户管理', '/sys/users', 'sys:user:list', 'sys/User', '1', 'el-icon-s-custom', '1', '2021-01-15 19:03:45', '2021-01-15 19:03:48', '1');
INSERT INTO `sys_menu` VALUES ('3', '1', '角色管理', '/sys/roles', 'sys:role:list', 'sys/Role', '1', 'el-icon-rank', '2', '2021-01-15 19:03:45', '2021-01-15 19:03:48', '1');
INSERT INTO `sys_menu` VALUES ('4', '1', '菜单管理', '/sys/menus', 'sys:menu:list', 'sys/Menu', '1', 'el-icon-menu', '3', '2021-01-15 19:03:45', '2021-01-15 19:03:48', '1');
INSERT INTO `sys_menu` VALUES ('5', '0', '系统工具', '', 'sys:tools', null, '0', 'el-icon-s-tools', '2', '2021-01-15 19:06:11', null, '1');
INSERT INTO `sys_menu` VALUES ('6', '5', '数字字典', '/sys/dicts', 'sys:dict:list', 'sys/Dict', '1', 'el-icon-s-order', '1', '2021-01-15 19:07:18', '2021-01-18 16:32:13', '1');
INSERT INTO `sys_menu` VALUES ('7', '3', '添加角色', '', 'sys:role:save', '', '2', '', '1', '2021-01-15 23:02:25', '2021-01-17 21:53:14', '0');
INSERT INTO `sys_menu` VALUES ('9', '2', '添加用户', null, 'sys:user:save', null, '2', null, '1', '2021-01-17 21:48:32', null, '1');
INSERT INTO `sys_menu` VALUES ('10', '2', '修改用户', null, 'sys:user:update', null, '2', null, '2', '2021-01-17 21:49:03', '2021-01-17 21:53:04', '1');
INSERT INTO `sys_menu` VALUES ('11', '2', '删除用户', null, 'sys:user:delete', null, '2', null, '3', '2021-01-17 21:49:21', null, '1');
INSERT INTO `sys_menu` VALUES ('12', '2', '分配角色', null, 'sys:user:role', null, '2', null, '4', '2021-01-17 21:49:58', null, '1');
INSERT INTO `sys_menu` VALUES ('13', '2', '重置密码', null, 'sys:user:repass', null, '2', null, '5', '2021-01-17 21:50:36', null, '1');
INSERT INTO `sys_menu` VALUES ('14', '3', '修改角色', null, 'sys:role:update', null, '2', null, '2', '2021-01-17 21:51:14', null, '1');
INSERT INTO `sys_menu` VALUES ('15', '3', '删除角色', null, 'sys:role:delete', null, '2', null, '3', '2021-01-17 21:51:39', null, '1');
INSERT INTO `sys_menu` VALUES ('16', '3', '分配权限', null, 'sys:role:perm', null, '2', null, '5', '2021-01-17 21:52:02', null, '1');
INSERT INTO `sys_menu` VALUES ('17', '4', '添加菜单', null, 'sys:menu:save', null, '2', null, '1', '2021-01-17 21:53:53', '2021-01-17 21:55:28', '1');
INSERT INTO `sys_menu` VALUES ('18', '4', '修改菜单', null, 'sys:menu:update', null, '2', null, '2', '2021-01-17 21:56:12', null, '1');
INSERT INTO `sys_menu` VALUES ('19', '4', '删除菜单', null, 'sys:menu:delete', null, '2', null, '3', '2021-01-17 21:56:36', null, '1');-- ----------------------------
-- Table structure for sys_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_role`;
CREATE TABLE `sys_role` (`id` bigint(20) NOT NULL AUTO_INCREMENT,`name` varchar(64) NOT NULL,`code` varchar(64) NOT NULL,`remark` varchar(64) DEFAULT NULL COMMENT '备注',`created` datetime DEFAULT NULL,`updated` datetime DEFAULT NULL,`statu` int(5) NOT NULL,PRIMARY KEY (`id`),UNIQUE KEY `name` (`name`) USING BTREE,UNIQUE KEY `code` (`code`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8;-- ----------------------------
-- Records of sys_role
-- ----------------------------
INSERT INTO `sys_role` VALUES ('3', '普通用户', 'normal', '只有基本查看功能', '2021-01-04 10:09:14', '2021-01-30 08:19:52', '1');
INSERT INTO `sys_role` VALUES ('6', '超级管理员', 'admin', '系统默认最高权限,不可以编辑和任意修改', '2021-01-16 13:29:03', '2021-01-17 15:50:45', '1');-- ----------------------------
-- Table structure for sys_role_menu
-- ----------------------------
DROP TABLE IF EXISTS `sys_role_menu`;
CREATE TABLE `sys_role_menu` (`id` bigint(20) NOT NULL AUTO_INCREMENT,`role_id` bigint(20) NOT NULL,`menu_id` bigint(20) NOT NULL,PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=102 DEFAULT CHARSET=utf8mb4;-- ----------------------------
-- Records of sys_role_menu
-- ----------------------------
INSERT INTO `sys_role_menu` VALUES ('60', '6', '1');
INSERT INTO `sys_role_menu` VALUES ('61', '6', '2');
INSERT INTO `sys_role_menu` VALUES ('62', '6', '9');
INSERT INTO `sys_role_menu` VALUES ('63', '6', '10');
INSERT INTO `sys_role_menu` VALUES ('64', '6', '11');
INSERT INTO `sys_role_menu` VALUES ('65', '6', '12');
INSERT INTO `sys_role_menu` VALUES ('66', '6', '13');
INSERT INTO `sys_role_menu` VALUES ('67', '6', '3');
INSERT INTO `sys_role_menu` VALUES ('68', '6', '7');
INSERT INTO `sys_role_menu` VALUES ('69', '6', '14');
INSERT INTO `sys_role_menu` VALUES ('70', '6', '15');
INSERT INTO `sys_role_menu` VALUES ('71', '6', '16');
INSERT INTO `sys_role_menu` VALUES ('72', '6', '4');
INSERT INTO `sys_role_menu` VALUES ('73', '6', '17');
INSERT INTO `sys_role_menu` VALUES ('74', '6', '18');
INSERT INTO `sys_role_menu` VALUES ('75', '6', '19');
INSERT INTO `sys_role_menu` VALUES ('76', '6', '5');
INSERT INTO `sys_role_menu` VALUES ('77', '6', '6');
INSERT INTO `sys_role_menu` VALUES ('96', '3', '1');
INSERT INTO `sys_role_menu` VALUES ('97', '3', '2');
INSERT INTO `sys_role_menu` VALUES ('98', '3', '3');
INSERT INTO `sys_role_menu` VALUES ('99', '3', '4');
INSERT INTO `sys_role_menu` VALUES ('100', '3', '5');
INSERT INTO `sys_role_menu` VALUES ('101', '3', '6');-- ----------------------------
-- Table structure for sys_user
-- ----------------------------
DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user` (`id` bigint(20) NOT NULL AUTO_INCREMENT,`username` varchar(64) DEFAULT NULL,`password` varchar(64) DEFAULT NULL,`avatar` varchar(255) DEFAULT NULL,`email` varchar(64) DEFAULT NULL,`city` varchar(64) DEFAULT NULL,`created` datetime DEFAULT NULL,`updated` datetime DEFAULT NULL,`last_login` datetime DEFAULT NULL,`statu` int(5) NOT NULL,PRIMARY KEY (`id`),UNIQUE KEY `UK_USERNAME` (`username`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;-- ----------------------------
-- Records of sys_user
-- ----------------------------
INSERT INTO `sys_user` VALUES ('1', 'admin', '123456', 'https://image-1300566513.cos.ap-guangzhou.myqcloud.com/upload/images/5a9f48118166308daba8b6da7e466aab.jpg', '123@qq.com', '广州', '2021-01-12 22:13:53', '2021-01-16 16:57:32', '2020-12-30 08:38:37', '1');
INSERT INTO `sys_user` VALUES ('2', 'test', '123456', 'https://image-1300566513.cos.ap-guangzhou.myqcloud.com/upload/images/5a9f48118166308daba8b6da7e466aab.jpg', 'test@qq.com', null, '2021-01-30 08:20:22', '2021-01-30 08:55:57', null, '1');-- ----------------------------
-- Table structure for sys_user_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_user_role`;
CREATE TABLE `sys_user_role` (`id` bigint(20) NOT NULL AUTO_INCREMENT,`user_id` bigint(20) NOT NULL,`role_id` bigint(20) NOT NULL,PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=14 DEFAULT CHARSET=utf8mb4;-- ----------------------------
-- Records of sys_user_role
-- ----------------------------
INSERT INTO `sys_user_role` VALUES ('4', '1', '6');
INSERT INTO `sys_user_role` VALUES ('7', '1', '3');
INSERT INTO `sys_user_role` VALUES ('13', '2', '3');
⑥:创建根据用户名获取用户接口
1.实体类
SysUser
@Data
@ApiModel(description = "用户实体类")
public class SysUser implements Serializable{private static final long serialVersionUID = 1L;@TableId(value = "id", type = IdType.AUTO)@ApiModelProperty("用户id,主键")private Long id;@NotBlank(message = "用户名不能为空")@ApiModelProperty("用户名")private String username;@ApiModelProperty("用户密码")private String password;@ApiModelProperty("头像")private String avatar;@NotBlank(message = "邮箱不能为空")@Email(message = "邮箱格式不正确")@ApiModelProperty("邮箱")private String email;@ApiModelProperty("城市")private String city;@ApiModelProperty("最后登录时间")private LocalDateTime lastLogin;@ApiModelProperty("创建时间")private LocalDateTime created;@ApiModelProperty("更新时间")private LocalDateTime updated;@ApiModelProperty("用户状态")private Integer statu;@ApiModelProperty("用户权限")@TableField(exist = false)private List<String> auths;
}
2.创建
SysUserMapper
@Mapper
public interface SysUserMapper extends BaseMapper<SysUser> {
}
3.启动类上加
@MapperScan("com.it.App.mapper")
4.创建
SysUserService
public interface SysUserService {Response<?> getUserByName(String username);
}
5.创建
SysUserServiceImpl
@Service
public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> implements SysUserService {@Autowiredprivate SysUserMapper sysUserMapper;@Overridepublic Response<?> getUserByName(String username) {QueryWrapper<SysUser> userQueryWrapper = new QueryWrapper<>();QueryWrapper<SysUser> wrapper = userQueryWrapper.eq("username", username);SysUser sysUser = sysUserMapper.selectOne(wrapper);// 是否查询到用户if (ObjectUtil.isNull(sysUser)){return Response.error("查无此人");}return Response.ok(sysUser);}
}
6.创建
SysUserController
@RestController
@RequestMapping("/sys")
@Api(tags = "用户相关接口")
public class SysUserController {@Autowiredprivate SysUserService sysUserService;@GetMapping("/getUser")@ApiOperation("根据用户名获取用户")public Response<?> getUserByName(String username){return sysUserService.getUserByName(username);}
7.测试http://localhost:8083/sys/getUser?username=admin
- 测试成功 说明我们mybatisPlus引入是没有问题的
⑦:配置Knife4j生成api文档在线测试
配置详情笔记:https://blog.csdn.net/cygqtt/article/details/134544894
注意:配置完成之后是访问不到的,因为被SpringSecurity拦截了,需要放行
如何放行:
在下文 登录接口实现 里的 添加配置
二、实现数据库用户登录
认证流程
①:自定义UserDetailService
1.首先创建一个
LoginUser
实现UserDetails
用于验证返回的数据
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class LoginUser implements UserDetails {// 引入我们的sysUser实体类private SysUser sysUser;@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {return null;}@Overridepublic String getPassword() {// 返回密码后将密码置空String password = sysUser.getPassword();sysUser.setPassword(null);return password;}@Overridepublic String getUsername() {return sysUser.getUsername();}@Overridepublic boolean isAccountNonExpired() {return true;}@Overridepublic boolean isAccountNonLocked() {return true;}@Overridepublic boolean isCredentialsNonExpired() {return true;}@Overridepublic boolean isEnabled() {return true;}
}
2.创建
UserDetailServiceImpl
实现UserDetailsService
用于自定义登录
@Service
public class UserDetailServiceImpl implements UserDetailsService {@Autowiredprivate SysUserMapper sysUserMapper;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {// 登录验证QueryWrapper<SysUser> userQueryWrapper = new QueryWrapper<>();QueryWrapper<SysUser> wrapper = userQueryWrapper.eq("username", username);SysUser sysUser = sysUserMapper.selectOne(wrapper);// 是否查询到用户, 如果没有查询到永固抛出异常if (ObjectUtil.isNull(sysUser)){throw new RuntimeException("用户名或密码错误");}// TODO 权限验证// 将查询出来的用户封装成UserDetails返回return LoginUser.builder().sysUser(sysUser).build();}
}
3.创建
SecurityConfig
配置类 配置密码的加密方式
- 如果不配置直接登录会报错
There is no PasswordEncoder mapped for the id "null"
意思就是说密码的加密方式为空
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {// 指定一个密码的加密方式@BeanBCryptPasswordEncoder bCryptPasswordEncoder() {return new BCryptPasswordEncoder();}
}
4.虽然指定了加密方式但是数据库中的密码还是明文 所以要改成密文
- 我们可以写一个测试类 将明文转换为密码 然后将密码存到数据库中
@SpringBootTest
@Slf4j
class ApplicationTests {@Autowiredprivate BCryptPasswordEncoder bCryptPasswordEncoder;@Testvoid getPwd() {String encode = bCryptPasswordEncoder.encode("123456");log.info("加密后的密文为: {}", encode);}}
②:测试登录
1.登录
2.请求测试接口 http://localhost:8083/sys/getUser?username=admin
③:登录接口实现
01.添加配置
-
在登录过程中 真正的认证逻辑还是交给
SpringSecurity
的,所以需要重写authenticationManagerBean()
这个方法 -
在登录时我们要放开登录接口,需要重写
configure(HttpSecurity http)
这个方法 指定放开的路径
1.配置类
SecurityConfig
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {public static final String[] URL_WHITELIST = {"/webjars/**","/favicon.ico","/sys/captcha","/sys/login","/sys/logout","/swagger-resources/**","/v2/api-docs","/swagger-ui.html","/webjars/**", // 放行knife4j生成的接口文档(/swagger-resources 和 /v2/api-docs 还有一些其他的资源路径, /swagger-ui.html、/webjars/** )};// 指定一个密码的加密方式@BeanBCryptPasswordEncoder bCryptPasswordEncoder() {return new BCryptPasswordEncoder();}@Bean@Overridepublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}// 配置HttpSecurity,定义安全策略@Overrideprotected void configure(HttpSecurity http) throws Exception {http.cors().and().csrf().disable() // 启用跨越支持,禁用CSRF保护.formLogin().and().authorizeRequests().antMatchers(URL_WHITELIST).permitAll() // 设置白名单,允许访问的URL.antMatchers(String.valueOf(HttpMethod.OPTIONS), "/**").permitAll() // 放行OPTIONS请求: Swagger可能会发出OPTIONS请求,确保这个请求也被放行.anyRequest().authenticated() // 其他所有请求需要身份验证.and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); // 不会创建session}
}
02.登录接口
1.直接在
SysUserController
中添加登录方法即可
@PostMapping("/login")
@ApiOperation("用户登录")
public Response<?> login(@RequestBody SysUser sysUser){return sysUserService.login(sysUser);
}
2.
SysUserService
Response<?> login(SysUser sysUser);
3.
SysUserServiceImpl
@Autowiredprivate AuthenticationManager authenticationManager;@Autowiredprivate JwtUtils jwtUtils;@Autowiredprivate RedisUtil redisUtil;@Overridepublic Response<?> login(SysUser sysUser) {// AuthenticationManager 进行用户认证UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(sysUser.getUsername(), sysUser.getPassword());Authentication authenticate = authenticationManager.authenticate(authenticationToken);// 如果认证没有通过 给出对应的提示if (ObjectUtil.isNull(authenticate)){throw new RuntimeException("用户名或密码错误!");}// 如果认证通过, 使用userId生成一个Jwt jwt存入到Response中返回LoginUser loginUser = (LoginUser) authenticate.getPrincipal();// 通过userId生成tokenString userId = loginUser.getSysUser().getId().toString();String token = jwtUtils.generateToken(userId);Map<Object, Object> map = MapUtil.builder().put("token", token).build();// 把完整的用户信息存入到redis中 统一的前缀 login 过期时间为10分钟String jsonString = objectMapper.writeValueAsString(loginUser);redisUtil.hset(Const.Login_Key,userId,jsonString,60*10);// 返回登录成功的结果return Response.ok(map);}
03.测试登录
-
因为我们导入 knife4j 生成了接口文档所以可以使用knife4j发送请求测试
-
访问:http://localhost:8083/doc.html
1.发送登录请求
④:token认证过滤器代码实现
01.创建token认证过滤器
1.JWTAuthenticationFilter
@Component
@Slf4j
public class JWTAuthenticationFilter extends OncePerRequestFilter {@Autowiredprivate JwtUtils jwtUtils;@Autowiredprivate RedisUtil redisUtil;@Autowiredprivate ObjectMapper objectMapper;// 进行JWT校验的过滤操作@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {// 日志记录JWT校验过滤器的执行log.info("JWT校验过滤器执行");// 从请求头中获取JWTString token = request.getHeader("token");// 如果token为空,则放行,继续处理下一个过滤器if (StrUtil.isBlankOrUndefined(token)){chain.doFilter(request,response);return;}// token不为空 使用Jwt工具类 解析获取声明Claims claims = jwtUtils.getClaimByToken(token);// 如果 token异常 则抛出异常if (claims == null){throw new RuntimeException("Token异常");}// 如果 token已过期 则抛出异常if (jwtUtils.isTokenExpired(claims)){throw new RuntimeException("Token已过期");}// 从token中获取用户idString userId = claims.getSubject();// 从redis中获取用户的全部信息String loginUserStr = (String) redisUtil.hget(Const.Login_Key , userId);LoginUser loginUser = objectMapper.readValue(loginUserStr, LoginUser.class);SysUser sysUser = loginUser.getSysUser();// 日志记录正在登录的用户信息log.info("用户-{},正在登录!", sysUser.getUsername());// TODO 获取权限信息封装到Authentication中UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null,null);// 将认证信息设置到安全上下文中SecurityContextHolder.getContext().setAuthentication(authenticationToken);// 继续处理请求chain.doFilter(request,response);}
}
2.将登录验证码校验过滤器加入到过滤器链中
- SecurityConfig
@Autowiredprivate JWTAuthenticationFilter jwtAuthenticationFilter;.....// 配置HttpSecurity,定义安全策略@Overrideprotected void configure(HttpSecurity http) throws Exception {http.cors().and().csrf().disable() // 启用跨越支持,禁用CSRF保护.formLogin().and().authorizeRequests().antMatchers(URL_WHITELIST).permitAll() // 设置白名单,允许访问的URL.anyRequest().authenticated() // 其他所有请求需要身份验证.and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); // 不会创建session// 将登录验证码校验过滤器加入到过滤器链中http.addFilterBefore(jwtAuthenticationFilter,UsernamePasswordAuthenticationFilter.class);}
02.测试登录
1.登录
- redis中也存入了对象
2.携带token访问其他接口
⑤:登出接口实现
思路:退出登录时会携带token ==> 获取token中的用户id ==> 根据用户id 删除redis中存储的用户信息 ==>(如果有前台则登出成功后删除已缓存的token)
01. 登录接口实现
1.SysUserController
@GetMapping("/logout")@ApiOperation("用户登出")public Response<?> logout(){return sysUserService.logout();}
2.SysUserService
Response<?> logout();
3.SysUserServiceImpl
@Overridepublic Response<?> logout() {// 获取当前用户的认证信息Authentication authentication = SecurityContextHolder.getContext().getAuthentication();// 从认证信息中获取登录用户对象LoginUser loginUser = (LoginUser) authentication.getPrincipal();// 如果登录用户为空,抛出异常,表示鉴权失败if (ObjectUtil.isNull(loginUser)) {throw new BaseException("鉴权失败!");}// 从Redis中删除用户登录信息String userId = loginUser.getSysUser().getId().toString();redisUtil.hdel(Const.Login_Key, userId);// 返回操作成功的响应return Response.ok("操作成功!");}
02.处理全局异常
1.创建BaseException
/*** @Author: Coke* @DateTime: 2023/11/23/9:53* @注释: 业务异常**/
public class BaseException extends RuntimeException {public BaseException() {}public BaseException(String msg) {super(msg);}}
2.创建GlobalExceptionHandler
/*** @Author: Coke* @DateTime: 2023/11/23/9:31* @注释: 全局异常处理器,处理项目中抛出的业务异常**/
@Slf4j
@RestControllerAdvice // 用于全局处理控制器层(Controller)的异常
public class GlobalExceptionHandler {/*** 捕获业务异常* @param e: * @return Response<?>* @author: Coke* @DateTime: 2023/11/23 9:33*/@ExceptionHandler(BaseException.class)public Response<?> exceptionHandler(BaseException e){log.error("异常信息:{}", e.getMessage());return Response.error(201,e.getMessage());}
}
3.将之前抛出的所有RuntimeException 改成BaseException
4.修改JWTAuthenticationFilter
- 在过滤器中的异常 我们自定义的全局异常捕获只做用与Controller层以及控制层的调用链上 所以我们直接在filer中try catch 捕获然后直接response响应回去就好了 当然也可以做一个AOP的切面来捕获过滤器中的异常
@Component
@Slf4j
public class JWTAuthenticationFilter extends OncePerRequestFilter {@Autowiredprivate JwtUtils jwtUtils;@Autowiredprivate RedisUtil redisUtil;@Autowiredprivate ObjectMapper objectMapper;// 进行JWT校验的过滤操作@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {// 日志记录JWT校验过滤器的执行log.info("JWT校验过滤器执行");try {// 从请求头中获取JWTString token = request.getHeader("token");// 如果token为空,则放行,继续处理下一个过滤器if (StrUtil.isBlankOrUndefined(token)) {chain.doFilter(request, response);return;}// token不为空 使用Jwt工具类 解析获取声明Claims claims = jwtUtils.getClaimByToken(token);// 如果 token异常 则抛出异常if (claims == null) {throw new BaseException("Token异常");}// 如果 token已过期 则抛出异常if (jwtUtils.isTokenExpired(claims)) {throw new BaseException("Token已过期");}// 从token中获取用户idString userId = claims.getSubject();// 从redis中获取用户的全部信息String loginUserStr = (String) redisUtil.hget(Const.Login_Key, userId);if (ObjectUtil.isNull(loginUserStr)) {throw new BaseException("鉴权失败!请求重新登录。");}LoginUser loginUser = objectMapper.readValue(loginUserStr, LoginUser.class);SysUser sysUser = loginUser.getSysUser();// 日志记录正在登录的用户信息log.info("用户-{},正在登录!", sysUser.getUsername());// TODO 获取权限信息封装到Authentication中UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, null);// 将认证信息设置到安全上下文中SecurityContextHolder.getContext().setAuthentication(authenticationToken);// 继续处理请求chain.doFilter(request, response);} catch (BaseException e) {// 捕获并处理异常log.error("JWT校验过滤器异常:{}", e.getMessage());response.setContentType("application/json;charset=UTF-8");response.setStatus(HttpServletResponse.SC_FORBIDDEN);ServletOutputStream outputStream = response.getOutputStream();Response<?> result = Response.error(201, e.getMessage());outputStream.write(JSONUtil.toJsonStr(result).getBytes(StandardCharsets.UTF_8));outputStream.flush();outputStream.close();}}
}
03.测试
1.登录
- 登录成功并且拿到了Token
- Redis中也存入了用户信息
2.携带Token获取用户信息
- 成功
3.请求登出接口
- 登出成功 并且Redis中的数据也被删除了
4.再次携带Token获取用户信息
三、权限
①:权限实现
01.限制访问资源所需权限
1.
SecurityConfig
中开启全局方法安全
@EnableGlobalMethodSecurity(prePostEnabled = true) // 启用全局方法安全
2.在controller接口上设置 访问接口所需要的权限
- SysUserController
@PreAuthorize("hasAuthority('sys:getUser')")
- 为了测试我们在 TestController 接口上也加一个权限(不存在的权限)
02.封装权限信息
1.LoginUser
// 权限private List<String> auths;// 定义一个新的权限集合List<SimpleGrantedAuthority> newAuths;@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {// 如果 newAuths 为空 第一个进来需要转换 如果不是直接返回if (ObjectUtil.isNull(newAuths)){// 将String类型的权限转成SimpleGrantedAuthority类型newAuths = auths.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());}return newAuths;}
2.UserDetailServiceImpl
@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {// 登录验证QueryWrapper<SysUser> userQueryWrapper = new QueryWrapper<>();QueryWrapper<SysUser> wrapper = userQueryWrapper.eq("username", username);SysUser sysUser = sysUserMapper.selectOne(wrapper);// 是否查询到用户, 如果没有查询到永固抛出异常if (ObjectUtil.isNull(sysUser)){throw new BaseException("用户名或密码错误");}// TODO 权限验证// 先将权限写死ArrayList<String> auths = new ArrayList<>(Arrays.asList("sys:getUser", "sys:addUser", "sys:delUser"));// 将查询出来的用户封装成UserDetails返回return LoginUser.builder().sysUser(sysUser).auths(auths).build();}
3.JWTAuthenticationFilter
// TODO 获取权限信息封装到Authentication中Collection<? extends GrantedAuthority> authorities = loginUser.getAuthorities();UsernamePasswordAuthenticationToken authenticationToken =new UsernamePasswordAuthenticationToken(loginUser, null, authorities);
03.测试
1.修改RedisTemplate键和值的序列化
这里解释一下为什么要注销掉
-
首先我们指定的是值的序列化器为 StringRedisSerializer 所以我们存的值要转成String类型,这样我们可以清楚的看懂存的是什么
-
其次我们从redis中获取到String类型的值后还要转成对象(问题就在这里平常对象当然没问题,但是我们今天存了这个类型的字段
List<SimpleGrantedAuthority> newAuths;
注意:SimpleGrantedAuthority
没有无参构造方法) -
然而字符串转对象调用的就是无参构造(所以会报错)
-
最后 干脆我们直接存Redis中的值为对象好了
所以我们需要改动两个地方
-
- SysUserServiceImpl
-
- JWTAuthenticationFilter
- JWTAuthenticationFilter
2.首先登录然后拿到Token
3.携带Token获取用户信息(有这个权限可以获取到)
4.携带Token请求Hello接口(没有hello的权限,不能访问)
②:基于数据库的权限实现
01.介绍
1.看一下流程就明白了
02.新增一些测试接口
1.SysUserController
- 由于测试我们直接返回即可(重点在权限验证上)
@PostMapping("/user/save")@ApiOperation("添加用户")@PreAuthorize("hasAuthority('sys:role:save')")public Response<?> userSave(){return Response.ok("新增用户成功!");}@PostMapping("/user/update")@ApiOperation("修改用户")@PreAuthorize("hasAuthority('sys:role:update')")public Response<?> updateSave(){return Response.ok("更新用户成功!");}@GetMapping("/user/delete")@ApiOperation("删除用户")@PreAuthorize("hasAuthority('sys:role:delete')")public Response<?> deleteSave(){return Response.ok("删除用户成功!");}
03.查询SQL实现
1.SysUserMapper
@Mapper
public interface SysUserMapper extends BaseMapper<SysUser> {@Select("select sm.perms\n" +"from sys_user su\n" +" join sys_user_role sur on sur.user_id = su.id\n" +" join sys_role sr on sur.role_id = sr.id\n" +" join sys_role_menu srm on sr.id = srm.role_id\n" +" join sys_menu sm on srm.menu_id = sm.id\n" +"where su.id = #{userId}")List<String> getMenuByUserId(Long userId);
}
2.UserDetailServiceImpl
// 根据 用户id 从数据库中查询权限List<String> auths = sysUserMapper.getMenuByUserId(sysUser.getId());
③:测试
01.使用admin用户测试
- 测试结果:有权限都可以访问
1.登录获取到token
2.测试新增用户接口
3.测试修改用户接口
4.测试删除用户接口
02.使用test用户测试
- 测试结果:没有权限都不可以访问
1.登录获取到token
- 登录成功后redis中就有两个用户信息了
2.测试新增用户接口
3.测试修改用户接口
4.测试删除用户接口
四、自定义异常处理(完善)
我们还希望在认证失败或者是授权失败的情况下也能和我们的接口一样返回相同结构的jso,这样可以让前端能对响应进行统一的处理。要实现这个功能我们需要知道SpringSecurity的异常处理机制。
在SpringSecurity中,如果我们在认证或者授权的过程中出现了异常会被ExceptionTranslationFilter捕获到。在ExceptionTranslation Filter中会去判断是认证失败还是授权失败出现的异常。
如果是认证过程中出现的异常会被封装成AuthenticationException:然后调用AuthenticationEntryPoint)对象的方法去进行异常处
理。
如果是授权过程中出现的异常会被封装成AccessDeniedException?然后调用*AccessDeniedHandler**对象的方法去进行异常处理。
所以如果我们需要自定义异常处理,我们只需要自定义AuthenticationEntryPoint和AccessDeniedHandler然后配置给SpringSecurity即可。
①:自定义实现类
1.授权失败异常处理 (AccessDeniedHandlerImpl)
/*** @Author: Coke* @DateTime: 2023/11/23/16:38* @注释: 授权失败异常处理**/
@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {@Overridepublic void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {response.setContentType("application/json;charset=UTF-8");response.setStatus(HttpServletResponse.SC_FORBIDDEN);ServletOutputStream outputStream = response.getOutputStream();Response<?> result = Response.error(HttpStatus.FORBIDDEN.value(), "您权限不足!");outputStream.write(JSONUtil.toJsonStr(result).getBytes(StandardCharsets.UTF_8));outputStream.flush();outputStream.close();}
}
2.认证失败异常处理 (AuthenticationEntryPointImpl)
/*** @Author: Coke* @DateTime: 2023/11/23/16:34* @注释: 认证失败异常处理**/
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {@Overridepublic void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {response.setContentType("application/json;charset=UTF-8");response.setStatus(HttpServletResponse.SC_FORBIDDEN);ServletOutputStream outputStream = response.getOutputStream();Response<?> result = Response.error(HttpStatus.UNAUTHORIZED.value(), "用户认证失败!请重新登录");outputStream.write(JSONUtil.toJsonStr(result).getBytes(StandardCharsets.UTF_8));outputStream.flush();outputStream.close();}
}
3.修改JWTAuthenticationFilter
- 之前我们是在JWTAuthenticationFilter中使用try – catch 捕获的异常然后处理的现在不需要了
- 删除try – catch 处理异常的代码
- 抛出的异常BaseException改成RuntimeException
修改后的代码如下
@Component
@Slf4j
public class JWTAuthenticationFilter extends OncePerRequestFilter {@Autowiredprivate JwtUtils jwtUtils;@Autowiredprivate RedisUtil redisUtil;// 进行JWT校验的过滤操作@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {// 日志记录JWT校验过滤器的执行log.info("JWT校验过滤器执行");// 从请求头中获取JWTString token = request.getHeader("token");// 如果token为空,则放行,继续处理下一个过滤器if (StrUtil.isBlankOrUndefined(token)) {chain.doFilter(request, response);return;}// token不为空 使用Jwt工具类 解析获取声明Claims claims = jwtUtils.getClaimByToken(token);// 如果 token异常 则抛出异常if (claims == null) {throw new RuntimeException("Token异常");}// 如果 token已过期 则抛出异常if (jwtUtils.isTokenExpired(claims)) {throw new RuntimeException("Token已过期");}// 从token中获取用户idString userId = claims.getSubject();// 从redis中获取用户的全部信息LoginUser loginUser = (LoginUser) redisUtil.hget(Const.Login_Key, userId);if (ObjectUtil.isNull(loginUser)) {throw new RuntimeException("鉴权失败!请求重新登录。");}
// LoginUser loginUser = objectMapper.readValue(loginUserStr, LoginUser.class);SysUser sysUser = loginUser.getSysUser();// 日志记录正在登录的用户信息log.info("用户-{},正在登录!", sysUser.getUsername());// TODO 获取权限信息封装到Authentication中Collection<? extends GrantedAuthority> authorities = loginUser.getAuthorities();UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, authorities);// 将认证信息设置到安全上下文中SecurityContextHolder.getContext().setAuthentication(authenticationToken);// 继续处理请求chain.doFilter(request, response);}
}
②:配置给SpringSecurity
1.SecurityConfig
@Autowiredprivate AccessDeniedHandlerImpl accessDeniedHandler;@Autowiredprivate AuthenticationEntryPointImpl authenticationEntryPoint;// 配置异常处理器(认证异常和授权异常)http.exceptionHandling()// 配置 认证异常处理器.authenticationEntryPoint(authenticationEntryPoint)// 配置授权异常处理器.accessDeniedHandler(accessDeniedHandler);
③:测试
1.登录给出错误密码
2.使用Test用户登录后访问新增用户接口(没有这个权限)
五、跨域
浏览器出于安全的考虑,使用XMLHttpRequest对象发起HTP请求时必须遵守同源策略,否则就是跨域的HTP请求,默认情况下是被禁止的。同源策略要求源相同才能正常进行通信,即协议、域名、端口号都完全一致。
前后端分离项目,前端项目和后端项目一般都不是同源的,所以肯定会存在跨域请求的问题。
所以我们就要处理一下,让前端能进行跨域请求。
①:先对SpringBoot配置,允许跨域请求
1.创建CorsConfig
@Configuration
public class CorsConfig implements WebMvcConfigurer {@Overridepublic void addCorsMappings (CorsRegistry registry) {// 设置允许跨域的路径registry.addMapping("/**")// 设置允许跨域的域名.allowedOriginPatterns("*")// 是否允许cookie.allowCredentials(true)// 这是允许的请求方式.allowedMethods("GET","POST","DELETE","PUT")//设置允许的header属性.allowedHeaders("*")// 跨域允许时间.maxAge(3600);}
}
②:开启SpringSecurity的跨域访问
六、其他权限校验方法
我们前面都是使用@PreAuthorize注解,然后在在其中使用的是hasAuthority方法进行校验。SpringSecurityi还为我们提供了其它方法
例如:hasAnyAuthority,hasRole,hasAnyRole,等。
这里我们先不急着去介绍这些方法,我们先去理解nasAuthority的原理,然后再去学习其他方法你就更容易理解,而不是死记硬背区别。并且我们也可以选择定义校验方法,实现我们自己的校验逻辑。
hasAuthority)方法实际是执行到了SecurityExpressionRoot的nasAuthority,大家只要断点调试既可知道它内部的校验原理。
它内部其实是调用authenticationl的getAuthorities方法获取用户的权限列表。然后判断我们存入的方法参数数据在权限列表中。
hasAnyAuthority方法可以传入多个权限,只有用户有其中任意一个权限都可以访问对应资源。
@GetMapping("hello2")@ApiOperation("测试接口hello多个权限")@PreAuthorize("hasAnyAuthority('hello','sys:role:save')")public String hello2(){return "您请求了一个测试接口-hello多个权限";}
hasRole要求有对应的角色才可以访问,但是它内部会把我们传入的参数拼接上RoLE后再去比较。所以这种情况下要用用户对应的权限也要有ROLE这个前缀才可以。
hasAnyRole有任意的角色就可以访问。它内部也会把我们传入的参数拼接上RoLE_后再去比较。所以这种情况下要用用户对应的权限也要有ROLE_这个前缀才可以。
①:自定义权限校验
1.com.it.App.expression.MyExpressionRoot(自己定义权限校验)
/*** @Author: Coke* @DateTime: 2023/11/24/9:00* @注释: 自定义权限校验**/
@Component("MyEx") // 自定义一下容器中Bean的名字
public class MyExpressionRoot {public boolean hasAuthority(String authority){// 获取当前用户的权限Authentication authentication = SecurityContextHolder.getContext().getAuthentication();LoginUser loginUser = (LoginUser) authentication.getPrincipal();List<String> auths = loginUser.getAuths();// 判断用户权限集合中是否存在authorityreturn auths.contains(authority);}
}
2.使用自己定义的权限校验
@GetMapping("hello3")@ApiOperation("自定义权限校验")@PreAuthorize("@MyEx.hasAuthority('hello')") // 在SPEL表达式中使用@MyEx相当于获取容器中bean的名字未MyEx的对象。public String hello3(){return "您请求了一个测试接口-hello自定义权限校验";}
②:基于配置的权限校验
.antMatchers("/user/save").hasAuthority("sys:role:save") // 访问 /user/save接口 必须要拥有sys:role:save权限
七、CSRF
CSRF是指跨站请求伪造(Cross-site request forgery),是web常见的攻击之一。
https://blog.csdn.net/freeking101/article/details/86537087
SpringSecurity去防l止CSRF攻击的方式就是通过csrf_token。后端会生成一个csrf_token,前端发起请求的时候需要携带这个csrf_token,后端会有过滤器进行校验,如果没有携带或者是伪造的就不允许访问。
我们可以发现cSRF攻击依靠的是cookie中所携带的认证信息。但是在前后端分离的项目中我们的认证信息其实是token,而token并不是存储中cookie中,并且需要前端代码去把token设置到请求头中才可以,所以CSRF攻击也就不用担心了。