SpringBoot整合SpringSecurity+jwt+knife4生成api接口(从零开始简单易懂)

一、准备工作

①:创建一个新项目

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中的值为对象好了

所以我们需要改动两个地方

    1. SysUserServiceImpl加粗样式
    1. 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攻击也就不用担心了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/166042.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

linux -系统通用命令查询

有时候内网环境下&#xff0c;系统有些命令没有安装因此掌握一些通用的linux 命令也可以帮助我们解决一些问题查看 1.查看系统内核版本 uname -r2.查看系统版本 cat /etc/os-release3. 查看cpu 配置 lscpu4.查看内存信息 free [参数] 中各个数值的解释如下表 数值解释t…

提高工作效率的宝藏网站和宝藏工具(高级版)

一、参考资料 亲测&#xff1a;你这些网站都不知道&#xff0c;哪来时间去摸鱼&#xff1f; 提高工作效率的宝藏网站和宝藏工具&#xff08;基础版&#xff09; 二、好用的网站 HelloGitHub - 开源项目平台 HelloGitHub 是一个分享有趣、 入门级开源项目的平台。 希望大家能…

MySQL-02-InnoDB存储引擎

实际的业务系统开发中&#xff0c;使用MySQL数据库&#xff0c;我们使用最多的当然是支持事务并发的InnoDB存储引擎的这种表结构&#xff0c;下面我们介绍下InnoDB存储引擎相关的知识点。 1-Innodb体系架构 InnoDB存储引擎有多个内存块&#xff0c;可以认为这些内存块组成了一…

qgis添加arcgis的mapserver

左侧浏览器-ArcGIS地图服务器-右键-新建连接 Folder: / 展开-双击图层即可

物联网AI MicroPython学习之语法 I2S音频总线接口

学物联网&#xff0c;来万物简单IoT物联网&#xff01;&#xff01; I2S 介绍 模块功能: I2S音频总线驱动模块 接口说明 I2S - 构建I2S对象 函数原型&#xff1a;I2S(id, sck, ws, sd, mode, bits, format, rate, ibuf)参数说明&#xff1a; 参数类型必选参数&#xff1f…

关于接口测试自动化的总结与思考!

序 近期看到阿里云性能测试 PTS 接口测试开启免费公测&#xff0c;本着以和大家交流如何实现高效的接口测试为出发点&#xff0c;本文包含了我在接口测试领域的一些方法和心得&#xff0c;希望大家一起讨论和分享&#xff0c;内容包括但不仅限于&#xff1a; 服务端接口测试介…

Vatee万腾的科技冒险:vatee创新力量的前沿发现

在当今飞速发展的科技潮流中&#xff0c;Vatee万腾以其独特的创新力量成为前沿的引领者。这场科技冒险不仅仅是技术的迭代&#xff0c;更是一次前所未有的前沿发现之旅&#xff0c;让我们一同深入探索Vatee万腾的科技冒险&#xff0c;感受vatee创新力量的前沿奇迹。 Vatee万腾将…

机器学习---最大似然估计和贝叶斯参数估计

1. 估计 贝叶斯框架下的数据收集&#xff0c;在以下条件下我们可以设计一个可选择的分类器 : P(wi) (先验)&#xff1b;P(x | wi) (类条件密度) 但是。我们很少能够完整的得到这些信息! 从一个传统的样本中设计一个分类器&#xff1a; ①先验估计不成问题 ②对类条件密度…

git本地账户如何从一台电脑迁移到另外一台

为了表述方便&#xff0c;我们此处用旧电脑、新电脑指代。 在新电脑上安装git 例如&#xff0c;我旧电脑上安装的git版本是2.33.1版本&#xff0c;新电脑安装git的版本是2.43.0&#xff0c;这不妨碍迁移。 将git的全局配置文件从旧电脑拷贝到新电脑 Git的全局配置文件&…

“关爱零距离.情暖老人心”主题活动

为提高社区老年人的生活质量&#xff0c;促进邻里间的互动与友谊&#xff0c;以及弘扬尊老爱幼的社区精神&#xff0c;11月21日山东省潍坊市金阳公益服务中心、重庆市潼南区同悦社会工作服务中心在潼南区桂林街道东风社区共同在潼南区桂林街道东风社区举办了“关爱零距离.情暖老…

22款奔驰S400L升级原厂360全景影像 高清环绕 无死角

360全景影像影像系统提升行车时的便利&#xff0c;不管是新手或是老司机都将是一个不错的配置&#xff0c;无论是在倒车&#xff0c;挪车以及拐弯转角的时候都能及时关注车辆所处的环境状况&#xff0c;避免盲区事故发生&#xff0c;提升行车出入安全性。 360全景影像包含&…

自学编程,用好这几个网站就够了!

如果你要自学编程&#xff0c;一定要收藏好这7个网站&#xff0c;上面免费的优质教程很多&#xff0c;完全可以省去你上万块钱的学费&#xff01; 话不多说&#xff0c;直接上干货&#xff01; 第一个&#xff0c;W3school 一个主打图文教程的网站&#xff0c;不管是前端开发…

怎样将带表格的图片批量合并转换成word表格?

注&#xff1a;本功能适用于V3.66以上版本的金鸣表格文字识别大师 在日常的办公场景中&#xff0c;我们常常会遇到需要将带有表格类的图片识别成excel的需求。我们知道&#xff0c;普通的OCR软件并不具备识别中文表格的功能&#xff0c;即使有&#xff0c;效果也强差人意&…

JSP:MVC

Web应用 一个好的Web应用&#xff1a; 功能完善 易于实现和维护 易于扩展等 的体系结构 一个Web应用通常分为两个部分&#xff1a; m 1. 由界面设计人员完成的 表示层 &#xff08;主要做网页界面设计&#xff09; m 2. 由程序设计人员实现的 行为层 &#xff08;主要完成本…

2015年7月8日 Go生态洞察:Go、开源与社区

&#x1f337;&#x1f341; 博主猫头虎&#xff08;&#x1f405;&#x1f43e;&#xff09;带您 Go to New World✨&#x1f341; &#x1f984; 博客首页——&#x1f405;&#x1f43e;猫头虎的博客&#x1f390; &#x1f433; 《面试题大全专栏》 &#x1f995; 文章图文…

基于springboot实现大学生就业服务平台系统项目【项目源码】计算机毕业设计

基于springboot实现大学生就业服务平台系统演示 Java技术 Java是由SUN公司推出&#xff0c;该公司于2010年被oracle公司收购。Java本是印度尼西亚的一个叫做爪洼岛的英文名称&#xff0c;也因此得来java是一杯正冒着热气咖啡的标识。Java语言在移动互联网的大背景下具备了显著…

企业必看的大数据安全极速传输解决方案

在这个大数据时代&#xff0c;企业在享受大数据带来的便利同时&#xff0c;也面临着巨大的挑战&#xff0c;其中最主要的问题就是数据安全方面和传输方面&#xff0c;为了更好地满足企业大数据传输的需求&#xff0c;小编将深入分析企业对于大数据传输面临的挑战和风险以及大数…

【elementui】el-popover在列表里循环使用,取消的doClose无效解决办法

目录 一、需求效果二、代码详情html方法接口 一、需求效果 在使用elementui的Popover 弹出框时&#xff0c;需求是在table列表里使用&#xff0c;循环出来&#xff0c;无法取消。 二、代码详情 html <el-table-column v-if"checkPermission([admin,user:resetPass…

【C++】标准模板库STL作业(其二)

&#x1f383;个人专栏&#xff1a; &#x1f42c; 算法设计与分析&#xff1a;算法设计与分析_IT闫的博客-CSDN博客 &#x1f433;Java基础&#xff1a;Java基础_IT闫的博客-CSDN博客 &#x1f40b;c语言&#xff1a;c语言_IT闫的博客-CSDN博客 &#x1f41f;MySQL&#xff1a…

Py之wikipedia-api:wikipedia-api的简介、安装、使用方法之详细攻略

Py之wikipedia-api&#xff1a;wikipedia-api的简介、安装、使用方法之详细攻略 目录 wikipedia-api的简介 wikipedia-api的安装 wikipedia-api的使用方法 1、 创建 Wikipedia并进行查询 wikipedia-api的简介 Wikipedia-API是一个易于使用的Python封装&#xff0c;用于访…