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.pswp.cn/news/166042.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/166042.shtml
英文地址,請注明出處:http://en.pswp.cn/news/166042.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

可以遠程控制電腦桌面的軟件有哪些?

隨著電腦辦公的普及&#xff0c;人們對于遠程控制電腦的需求也越來越大。遠程控制電腦技術能夠讓用戶在不同地點的電腦之間進行操作和訪問&#xff0c;能夠提高工作效率。可以遠程控制電腦桌面的軟件有哪些&#xff1f; 1. 遠程監控電腦軟件 需要安裝在被控制端電腦&#xff…

【cppcheck 靜態代碼分析工具使用教程】

cppcheck 是一個流行的靜態代碼分析工具,用于 C 和 C++ 程序。它可以幫助檢測代碼中的錯誤、未定義的行為、內存泄漏等。在 Ubuntu 系統上使用 cppcheck 的基本步驟和示例如下: 安裝 cppcheck 打開終端。使用以下命令安裝 cppcheck:sudo apt-get update sudo apt-get insta…

linux -系統通用命令查詢

有時候內網環境下&#xff0c;系統有些命令沒有安裝因此掌握一些通用的linux 命令也可以幫助我們解決一些問題查看 1.查看系統內核版本 uname -r2.查看系統版本 cat /etc/os-release3. 查看cpu 配置 lscpu4.查看內存信息 free [參數] 中各個數值的解釋如下表 數值解釋t…

4.并發中的各種鎖概念

目錄 概述鎖分類按上鎖方式劃分按特性劃分悲觀鎖/樂觀鎖重入鎖/不可重入鎖公平鎖/非公平鎖獨享鎖/共享鎖 其它自旋鎖分段鎖無鎖/偏向鎖/輕量級鎖/重量級鎖 結束 概述 java 鎖分類&#xff0c;雖是概念&#xff0c;很常見。 鎖分類 按上鎖方式劃分 鎖關鍵字解釋隱式鎖synchr…

提高工作效率的寶藏網站和寶藏工具(高級版)

一、參考資料 親測&#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: / 展開-雙擊圖層即可

oracle 表樹形結構查詢遞歸查詢

簡介&#xff1a; WITH RECURSIVE 是一種在關系型數據庫中處理遞歸查詢的語法。 舉例&#xff1a; 假設我們有一個樹形結構數據表 tree_table&#xff0c; 包含節點的 ID、父節點的 ID 和節點名稱等字段。 示例表數據&#xff1a; --------------- | id | pid | name | ----…

物聯網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萬騰將…

【Thumbnailator】圖片壓縮、水印、格式修改一網打盡

前言&#xff1a; 對于javaweb服務端開發人員&#xff0c;圖片資源的管理總是繞不開的一環。很多網站上都會提供上傳圖片這個功能&#xff0c;而現代數碼設備拍攝出來的都是高清圖片&#xff0c;分辨率很高&#xff0c;占用的空間也很大。物理存儲的問題還算容易解決&#xff0…

機器學習---最大似然估計和貝葉斯參數估計

1. 估計 貝葉斯框架下的數據收集&#xff0c;在以下條件下我們可以設計一個可選擇的分類器 : P(wi) (先驗)&#xff1b;P(x | wi) (類條件密度) 但是。我們很少能夠完整的得到這些信息! 從一個傳統的樣本中設計一個分類器&#xff1a; ①先驗估計不成問題 ②對類條件密度…

蘋果企業簽名失敗常見的問題

蘋果企業簽名失敗的常見問題主要有以下幾種&#xff1a; 證書過期或無效&#xff1a;蘋果開發者需要定期更新他們的簽名證書&#xff0c;以確保其有效性。一旦證書過期&#xff0c;相關應用將無法正常工作。證書不匹配&#xff1a;如果使用的證書與應用程序的Bundle ID不匹配&…

WT588F02B-8S語音芯片支持PWM音頻輸出的特征優勢及應用前景

隨著科技的飛速發展&#xff0c;語音芯片作為人機交互的核心組件&#xff0c;在各個領域的應用越來越廣泛。而在這些語音芯片中&#xff0c;支持PWM音頻輸出的特性日益受到關注。本文將探討語音芯片支持PWM音頻輸出的特征優勢以及其在各個領域的應用前景。 一、特征優勢 1、高…

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;效果也強差人意&…