一、準備工作
①:創建一個新項目
1.事先創建好一些包
②:引入依賴
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter</artifactId></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><!--mysql驅動 --><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>8.0.11</version></dependency><!--支持使用 JDBC 訪問數據庫 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-jdbc</artifactId></dependency><!--整合mybatis plus https://baomidou.com/--><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId><version>3.4.1</version></dependency><!-- mybatis-plus-generator --><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-generator</artifactId><version>3.4.1</version></dependency><!--數據源 --><dependency><groupId>com.alibaba</groupId><artifactId>druid</artifactId><version>1.2.16</version></dependency><!--引入hutool--><dependency><groupId>cn.hutool</groupId><artifactId>hutool-all</artifactId><version>5.8.18</version></dependency><!-- springboot security --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><!-- redis--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId></dependency><!-- jwt --><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.9.1</version></dependency><!-- 圖片驗證碼生成器--><dependency><groupId>com.github.axet</groupId><artifactId>kaptcha</artifactId><version>0.0.9</version></dependency><dependency><groupId>org.apache.commons</groupId><artifactId>commons-lang3</artifactId><version>3.11</version></dependency><!-- 生成配置元數據--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-configuration-processor</artifactId><optional>true</optional></dependency><!-- 參數校驗 如:@NotBlank(message = "name為必傳參數") private String name;--><dependency><groupId>javax.validation</groupId><artifactId>validation-api</artifactId></dependency><!-- 導入 knife4j生成接口文檔--><dependency><groupId>com.github.xiaoymin</groupId><artifactId>knife4j-spring-boot-starter</artifactId><version>3.0.3</version></dependency>
③:添加一個測試接口查看效果
1.TestController
@RestController
@Api(tags = "測試專用接口")
public class TestController {@GetMapping("hello")@ApiOperation("測試接口hello")public String hello(){return "您請求了一個測試接口-hello";}
}
2.啟動查看效果
訪問http://localhost:8083/hello
- 會自動跳到Springsecurity的登錄頁面(程序已經被SpringSecurity保護)
- 沒有配置用戶名和密碼時 默認用戶user 密碼 在控制臺
3.登錄成功可以看到(引入SpringSecurity測試成功)
④:創建工具類和統一響應類
01.工具類
1.創建Redis工具了
@Component
public class RedisUtil {@Autowiredprivate RedisTemplate redisTemplate;/*** 指定緩存失效時間** @param key 鍵* @param time 時間(秒)* @return*/public boolean expire(String key, long time) {try {if (time > 0) {redisTemplate.expire(key, time, TimeUnit.SECONDS);}return true;} catch (Exception e) {e.printStackTrace();return false;}}/*** 根據key 獲取過期時間** @param key 鍵 不能為null* @return 時間(秒) 返回0代表為永久有效*/public long getExpire(String key) {return redisTemplate.getExpire(key, TimeUnit.SECONDS);}/*** 判斷key是否存在** @param key 鍵* @return true 存在 false不存在*/public boolean hasKey(String key) {try {return redisTemplate.hasKey(key);} catch (Exception e) {e.printStackTrace();return false;}}/*** 刪除緩存** @param key 可以傳一個值 或多個*/@SuppressWarnings("unchecked")public void del(String... key) {if (key != null && key.length > 0) {if (key.length == 1) {redisTemplate.delete(key[0]);} else {redisTemplate.delete(CollectionUtils.arrayToList(key));}}}//============================String============================= /*** 普通緩存獲取** @param key 鍵* @return 值*/public Object get(String key) {return key == null ? null : redisTemplate.opsForValue().get(key);}/*** 普通緩存放入** @param key 鍵* @param value 值* @return true成功 false失敗*/public boolean set(String key, Object value) {try {redisTemplate.opsForValue().set(key, value);return true;} catch (Exception e) {e.printStackTrace();return false;}}/*** 普通緩存放入并設置時間** @param key 鍵* @param value 值* @param time 時間(秒) time要大于0 如果time小于等于0 將設置無限期* @return true成功 false 失敗*/public boolean set(String key, Object value, long time) {try {if (time > 0) {redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);} else {set(key, value);}return true;} catch (Exception e) {e.printStackTrace();return false;}}/*** 遞增** @param key 鍵* @param delta 要增加幾(大于0)* @return*/public long incr(String key, long delta) {if (delta < 0) {throw new RuntimeException("遞增因子必須大于0");}return redisTemplate.opsForValue().increment(key, delta);}/*** 遞減** @param key 鍵* @param delta 要減少幾(小于0)* @return*/public long decr(String key, long delta) {if (delta < 0) {throw new RuntimeException("遞減因子必須大于0");}return redisTemplate.opsForValue().increment(key, -delta);}//================================Map================================= /*** HashGet** @param key 鍵 不能為null* @param item 項 不能為null* @return 值*/public Object hget(String key, String item) {return redisTemplate.opsForHash().get(key, item);}/*** 獲取hashKey對應的所有鍵值** @param key 鍵* @return 對應的多個鍵值*/public Map<Object, Object> hmget(String key) {return redisTemplate.opsForHash().entries(key);}/*** HashSet** @param key 鍵* @param map 對應多個鍵值* @return true 成功 false 失敗*/public boolean hmset(String key, Map<String, Object> map) {try {redisTemplate.opsForHash().putAll(key, map);return true;} catch (Exception e) {e.printStackTrace();return false;}}/*** HashSet 并設置時間** @param key 鍵* @param map 對應多個鍵值* @param time 時間(秒)* @return true成功 false失敗*/public boolean hmset(String key, Map<String, Object> map, long time) {try {redisTemplate.opsForHash().putAll(key, map);if (time > 0) {expire(key, time);}return true;} catch (Exception e) {e.printStackTrace();return false;}}/*** 向一張hash表中放入數據,如果不存在將創建** @param key 鍵* @param item 項* @param value 值* @return true 成功 false失敗*/public boolean hset(String key, String item, Object value) {try {redisTemplate.opsForHash().put(key, item, value);return true;} catch (Exception e) {e.printStackTrace();return false;}}/*** 向一張hash表中放入數據,如果不存在將創建** @param key 鍵* @param item 項* @param value 值* @param time 時間(秒) 注意:如果已存在的hash表有時間,這里將會替換原有的時間* @return true 成功 false失敗*/public boolean hset(String key, String item, Object value, long time) {try {redisTemplate.opsForHash().put(key, item, value);if (time > 0) {expire(key, time);}return true;} catch (Exception e) {e.printStackTrace();return false;}}/*** 刪除hash表中的值** @param key 鍵 不能為null* @param item 項 可以使多個 不能為null*/public void hdel(String key, Object... item) {redisTemplate.opsForHash().delete(key, item);}/*** 判斷hash表中是否有該項的值** @param key 鍵 不能為null* @param item 項 不能為null* @return true 存在 false不存在*/public boolean hHasKey(String key, String item) {return redisTemplate.opsForHash().hasKey(key, item);}/*** hash遞增 如果不存在,就會創建一個 并把新增后的值返回** @param key 鍵* @param item 項* @param by 要增加幾(大于0)* @return*/public double hincr(String key, String item, double by) {return redisTemplate.opsForHash().increment(key, item, by);}/*** hash遞減** @param key 鍵* @param item 項* @param by 要減少記(小于0)* @return*/public double hdecr(String key, String item, double by) {return redisTemplate.opsForHash().increment(key, item, -by);}//============================set============================= /*** 根據key獲取Set中的所有值** @param key 鍵* @return*/public Set<Object> sGet(String key) {try {return redisTemplate.opsForSet().members(key);} catch (Exception e) {e.printStackTrace();return null;}}/*** 根據value從一個set中查詢,是否存在** @param key 鍵* @param value 值* @return true 存在 false不存在*/public boolean sHasKey(String key, Object value) {try {return redisTemplate.opsForSet().isMember(key, value);} catch (Exception e) {e.printStackTrace();return false;}}/*** 將數據放入set緩存** @param key 鍵* @param values 值 可以是多個* @return 成功個數*/public long sSet(String key, Object... values) {try {return redisTemplate.opsForSet().add(key, values);} catch (Exception e) {e.printStackTrace();return 0;}}/*** 將set數據放入緩存** @param key 鍵* @param time 時間(秒)* @param values 值 可以是多個* @return 成功個數*/public long sSetAndTime(String key, long time, Object... values) {try {Long count = redisTemplate.opsForSet().add(key, values);if (time > 0) expire(key, time);return count;} catch (Exception e) {e.printStackTrace();return 0;}}/*** 獲取set緩存的長度** @param key 鍵* @return*/public long sGetSetSize(String key) {try {return redisTemplate.opsForSet().size(key);} catch (Exception e) {e.printStackTrace();return 0;}}/*** 移除值為value的** @param key 鍵* @param values 值 可以是多個* @return 移除的個數*/public long setRemove(String key, Object... values) {try {Long count = redisTemplate.opsForSet().remove(key, values);return count;} catch (Exception e) {e.printStackTrace();return 0;}}//===============================list================================= /*** 獲取list緩存的內容** @param key 鍵* @param start 開始* @param end 結束 0 到 -1代表所有值* @return*/public List<Object> lGet(String key, long start, long end) {try {return redisTemplate.opsForList().range(key, start, end);} catch (Exception e) {e.printStackTrace();return null;}}/*** 獲取list緩存的長度** @param key 鍵* @return*/public long lGetListSize(String key) {try {return redisTemplate.opsForList().size(key);} catch (Exception e) {e.printStackTrace();return 0;}}/*** 通過索引 獲取list中的值** @param key 鍵* @param index 索引 index>=0時, 0 表頭,1 第二個元素,依次類推;index<0時,-1,表尾,-2倒數第二個元素,依次類推* @return*/public Object lGetIndex(String key, long index) {try {return redisTemplate.opsForList().index(key, index);} catch (Exception e) {e.printStackTrace();return null;}}/*** 將list放入緩存** @param key 鍵* @param value 值* @return*/public boolean lSet(String key, Object value) {try {redisTemplate.opsForList().rightPush(key, value);return true;} catch (Exception e) {e.printStackTrace();return false;}}/*** 將list放入緩存** @param key 鍵* @param value 值* @param time 時間(秒)* @return*/public boolean lSet(String key, Object value, long time) {try {redisTemplate.opsForList().rightPush(key, value);if (time > 0) expire(key, time);return true;} catch (Exception e) {e.printStackTrace();return false;}}/*** 將list放入緩存** @param key 鍵* @param value 值* @return*/public boolean lSet(String key, List<Object> value) {try {redisTemplate.opsForList().rightPushAll(key, value);return true;} catch (Exception e) {e.printStackTrace();return false;}}/*** 將list放入緩存** @param key 鍵* @param value 值* @param time 時間(秒)* @return*/public boolean lSet(String key, List<Object> value, long time) {try {redisTemplate.opsForList().rightPushAll(key, value);if (time > 0) expire(key, time);return true;} catch (Exception e) {e.printStackTrace();return false;}}/*** 根據索引修改list中的某條數據** @param key 鍵* @param index 索引* @param value 值* @return*/public boolean lUpdateIndex(String key, long index, Object value) {try {redisTemplate.opsForList().set(key, index, value);return true;} catch (Exception e) {e.printStackTrace();return false;}}/*** 移除N個值為value** @param key 鍵* @param count 移除多少個* @param value 值* @return 移除的個數*/public long lRemove(String key, long count, Object value) {try {Long remove = redisTemplate.opsForList().remove(key, count, value);return remove;} catch (Exception e) {e.printStackTrace();return 0;}}//================有序集合 sort set===================/*** 有序set添加元素** @param key* @param value* @param score* @return*/public boolean zSet(String key, Object value, double score) {return redisTemplate.opsForZSet().add(key, value, score);}public long batchZSet(String key, Set<ZSetOperations.TypedTuple> typles) {return redisTemplate.opsForZSet().add(key, typles);}public void zIncrementScore(String key, Object value, long delta) {redisTemplate.opsForZSet().incrementScore(key, value, delta);}public void zUnionAndStore(String key, Collection otherKeys, String destKey) {redisTemplate.opsForZSet().unionAndStore(key, otherKeys, destKey);}/*** 獲取zset數量* @param key* @param value* @return*/public long getZsetScore(String key, Object value) {Double score = redisTemplate.opsForZSet().score(key, value);if(score==null){return 0;}else{return score.longValue();}}/*** 獲取有序集 key 中成員 member 的排名 。* 其中有序集成員按 score 值遞減 (從大到小) 排序。* @param key* @param start* @param end* @return*/public Set<ZSetOperations.TypedTuple> getZSetRank(String key, long start, long end) {return redisTemplate.opsForZSet().reverseRangeWithScores(key, start, end);}}
2.創建
RedisConfig
自定義key和value的序列化(避免出現亂碼)
@Configuration
public class RedisConfig {@Bean// 定義 RedisTemplate BeanRedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {// 創建 RedisTemplate 實例RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();// 設置連接工廠redisTemplate.setConnectionFactory(redisConnectionFactory);// 配置 JSON 序列化器Jackson2JsonRedisSerializer<Object> redisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);redisSerializer.setObjectMapper(new ObjectMapper());// 設置鍵的序列化器為 StringRedisSerializerredisTemplate.setKeySerializer(new StringRedisSerializer());// 設置值的序列化器為 StringRedisSerializerredisTemplate.setValueSerializer(new StringRedisSerializer());// 設置哈希鍵的序列化器為 StringRedisSerializerredisTemplate.setHashKeySerializer(new StringRedisSerializer());// 設置哈希值的序列化器為 StringRedisSerializerredisTemplate.setHashValueSerializer(new StringRedisSerializer());return redisTemplate;}
}
3.Jwt工具類 創建jwt和校驗jwt
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;import java.util.Date;@Data
@Component
@ConfigurationProperties(prefix = "coke.jwt")
public class JwtUtils {// JWT 過期時間(單位:秒)private long expire;// JWT 密鑰,用于簽名和驗證private String secret;// JWT 頭部字段,可自定義private String header;/*** 生成 JWT** @param username 用戶名* @return JWT 字符串*/public String generateToken(String username) {// 獲取當前時間Date nowDate = new Date();// 計算過期時間,當前時間 + 過期時長Date expireDate = new Date(nowDate.getTime() + expire);// 使用 JWT Builder 構建 JWTreturn Jwts.builder().setHeaderParam("typ", "JWT") // 設置頭部信息,通常為JWT.setSubject(username) // 設置主題,通常為用戶名.setIssuedAt(nowDate) // 設置簽發時間,即當前時間.setExpiration(expireDate) // 設置過期時間.signWith(SignatureAlgorithm.HS512, secret) // 使用HS512簽名算法和密鑰進行簽名.compact();}/*** 解析 JWT 獲取聲明** @param jwt JWT 字符串* @return JWT 中的聲明部分*/public Claims getClaimByToken(String jwt) {try {// 使用 JWT 解析器解析 JWT,并獲取聲明部分return Jwts.parser().setSigningKey(secret) // 設置解析時的密鑰,必須與生成時的密鑰一致.parseClaimsJws(jwt).getBody();} catch (Exception e) {// 解析失敗,返回nullreturn null;}}/*** 檢查 JWT 是否過期** @param claims JWT 中的聲明部分* @return 是否過期*/public boolean isTokenExpired(Claims claims) {// 檢查過期時間是否在當前時間之前return claims.getExpiration().before(new Date());}}
4.jwt工具類中讀取了ym配置文件中的
coke.jwt
配置如下
server:port: 8083
coke:jwt:header: Authorizationexpire: 604800 #7天,秒單位secret: ji8n3439n439n43ld9ne9343fdfer49h
02.統一響應類
1.Response
@Data
public class Response<T> {/*** 結果** @mock true*/private boolean success;/*** 狀態碼** @mock 200*/private int code;/*** 消息提示** @mock 操作成功*/private String msg;/*** 結果體** @mock null*/private T data;public Response () {}public Response (int code, Object status) {super();this.code = code;this.msg = status.toString();if (code == 1) {this.success = true;} else {this.success = false;}}public Response (int code, String status, T result) {super();this.code = code;this.msg = status;this.data = result;if (code == 1) {this.success = true;} else {this.success = false;}}public static Response<?> ok() {return new Response<>(1, "success");}public static <T> Response<T> ok(T t) {return new Response<T>(1, "success", t);}public static Response<?> error(String status) {return new Response<>(500, status);}public static Response<?> error(int code, String status) {return new Response<>(code, status);}
}
2.添加一個常量類
Const
public class Const {public final static String CAPTCHA_KEY = "captcha";public final static String Login_Key = "login";
}
⑤:數據庫 數據準備
01.yml數據庫配置
spring:datasource:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://1.11.94.14:3306/security_study?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghaiusername: rootpassword: wwwthymeleaf: # 是否使用springboot靜態文件緩存 true 當修改靜態文件需要重啟服務器 false 瀏覽器端刷新就可以了cache: falsecheck-template: trueredis:host: 1.107.94.114password: wwwport: 6379mybatis-plus:mapper-locations: classpath*:/mapper/**Mapper.xml
02.添加數據
SET FOREIGN_KEY_CHECKS=0;-- ----------------------------
-- Table structure for sys_menu
-- ----------------------------
DROP TABLE IF EXISTS `sys_menu`;
CREATE TABLE `sys_menu` (`id` bigint(20) NOT NULL AUTO_INCREMENT,`parent_id` bigint(20) DEFAULT NULL COMMENT '父菜單ID,一級菜單為0',`name` varchar(64) NOT NULL,`path` varchar(255) DEFAULT NULL COMMENT '菜單URL',`perms` varchar(255) DEFAULT NULL COMMENT '授權(多個用逗號分隔,如:user:list,user:create)',`component` varchar(255) DEFAULT NULL,`type` int(5) NOT NULL COMMENT '類型 0:目錄 1:菜單 2:按鈕',`icon` varchar(32) DEFAULT NULL COMMENT '菜單圖標',`orderNum` int(11) DEFAULT NULL COMMENT '排序',`created` datetime NOT NULL,`updated` datetime DEFAULT NULL,`statu` int(5) NOT NULL,PRIMARY KEY (`id`),UNIQUE KEY `name` (`name`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=20 DEFAULT CHARSET=utf8;-- ----------------------------
-- Records of sys_menu
-- ----------------------------
INSERT INTO `sys_menu` VALUES ('1', '0', '系統管理', '', 'sys:manage', '', '0', 'el-icon-s-operation', '1', '2021-01-15 18:58:18', '2021-01-15 18:58:20', '1');
INSERT INTO `sys_menu` VALUES ('2', '1', '用戶管理', '/sys/users', 'sys:user:list', 'sys/User', '1', 'el-icon-s-custom', '1', '2021-01-15 19:03:45', '2021-01-15 19:03:48', '1');
INSERT INTO `sys_menu` VALUES ('3', '1', '角色管理', '/sys/roles', 'sys:role:list', 'sys/Role', '1', 'el-icon-rank', '2', '2021-01-15 19:03:45', '2021-01-15 19:03:48', '1');
INSERT INTO `sys_menu` VALUES ('4', '1', '菜單管理', '/sys/menus', 'sys:menu:list', 'sys/Menu', '1', 'el-icon-menu', '3', '2021-01-15 19:03:45', '2021-01-15 19:03:48', '1');
INSERT INTO `sys_menu` VALUES ('5', '0', '系統工具', '', 'sys:tools', null, '0', 'el-icon-s-tools', '2', '2021-01-15 19:06:11', null, '1');
INSERT INTO `sys_menu` VALUES ('6', '5', '數字字典', '/sys/dicts', 'sys:dict:list', 'sys/Dict', '1', 'el-icon-s-order', '1', '2021-01-15 19:07:18', '2021-01-18 16:32:13', '1');
INSERT INTO `sys_menu` VALUES ('7', '3', '添加角色', '', 'sys:role:save', '', '2', '', '1', '2021-01-15 23:02:25', '2021-01-17 21:53:14', '0');
INSERT INTO `sys_menu` VALUES ('9', '2', '添加用戶', null, 'sys:user:save', null, '2', null, '1', '2021-01-17 21:48:32', null, '1');
INSERT INTO `sys_menu` VALUES ('10', '2', '修改用戶', null, 'sys:user:update', null, '2', null, '2', '2021-01-17 21:49:03', '2021-01-17 21:53:04', '1');
INSERT INTO `sys_menu` VALUES ('11', '2', '刪除用戶', null, 'sys:user:delete', null, '2', null, '3', '2021-01-17 21:49:21', null, '1');
INSERT INTO `sys_menu` VALUES ('12', '2', '分配角色', null, 'sys:user:role', null, '2', null, '4', '2021-01-17 21:49:58', null, '1');
INSERT INTO `sys_menu` VALUES ('13', '2', '重置密碼', null, 'sys:user:repass', null, '2', null, '5', '2021-01-17 21:50:36', null, '1');
INSERT INTO `sys_menu` VALUES ('14', '3', '修改角色', null, 'sys:role:update', null, '2', null, '2', '2021-01-17 21:51:14', null, '1');
INSERT INTO `sys_menu` VALUES ('15', '3', '刪除角色', null, 'sys:role:delete', null, '2', null, '3', '2021-01-17 21:51:39', null, '1');
INSERT INTO `sys_menu` VALUES ('16', '3', '分配權限', null, 'sys:role:perm', null, '2', null, '5', '2021-01-17 21:52:02', null, '1');
INSERT INTO `sys_menu` VALUES ('17', '4', '添加菜單', null, 'sys:menu:save', null, '2', null, '1', '2021-01-17 21:53:53', '2021-01-17 21:55:28', '1');
INSERT INTO `sys_menu` VALUES ('18', '4', '修改菜單', null, 'sys:menu:update', null, '2', null, '2', '2021-01-17 21:56:12', null, '1');
INSERT INTO `sys_menu` VALUES ('19', '4', '刪除菜單', null, 'sys:menu:delete', null, '2', null, '3', '2021-01-17 21:56:36', null, '1');-- ----------------------------
-- Table structure for sys_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_role`;
CREATE TABLE `sys_role` (`id` bigint(20) NOT NULL AUTO_INCREMENT,`name` varchar(64) NOT NULL,`code` varchar(64) NOT NULL,`remark` varchar(64) DEFAULT NULL COMMENT '備注',`created` datetime DEFAULT NULL,`updated` datetime DEFAULT NULL,`statu` int(5) NOT NULL,PRIMARY KEY (`id`),UNIQUE KEY `name` (`name`) USING BTREE,UNIQUE KEY `code` (`code`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8;-- ----------------------------
-- Records of sys_role
-- ----------------------------
INSERT INTO `sys_role` VALUES ('3', '普通用戶', 'normal', '只有基本查看功能', '2021-01-04 10:09:14', '2021-01-30 08:19:52', '1');
INSERT INTO `sys_role` VALUES ('6', '超級管理員', 'admin', '系統默認最高權限,不可以編輯和任意修改', '2021-01-16 13:29:03', '2021-01-17 15:50:45', '1');-- ----------------------------
-- Table structure for sys_role_menu
-- ----------------------------
DROP TABLE IF EXISTS `sys_role_menu`;
CREATE TABLE `sys_role_menu` (`id` bigint(20) NOT NULL AUTO_INCREMENT,`role_id` bigint(20) NOT NULL,`menu_id` bigint(20) NOT NULL,PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=102 DEFAULT CHARSET=utf8mb4;-- ----------------------------
-- Records of sys_role_menu
-- ----------------------------
INSERT INTO `sys_role_menu` VALUES ('60', '6', '1');
INSERT INTO `sys_role_menu` VALUES ('61', '6', '2');
INSERT INTO `sys_role_menu` VALUES ('62', '6', '9');
INSERT INTO `sys_role_menu` VALUES ('63', '6', '10');
INSERT INTO `sys_role_menu` VALUES ('64', '6', '11');
INSERT INTO `sys_role_menu` VALUES ('65', '6', '12');
INSERT INTO `sys_role_menu` VALUES ('66', '6', '13');
INSERT INTO `sys_role_menu` VALUES ('67', '6', '3');
INSERT INTO `sys_role_menu` VALUES ('68', '6', '7');
INSERT INTO `sys_role_menu` VALUES ('69', '6', '14');
INSERT INTO `sys_role_menu` VALUES ('70', '6', '15');
INSERT INTO `sys_role_menu` VALUES ('71', '6', '16');
INSERT INTO `sys_role_menu` VALUES ('72', '6', '4');
INSERT INTO `sys_role_menu` VALUES ('73', '6', '17');
INSERT INTO `sys_role_menu` VALUES ('74', '6', '18');
INSERT INTO `sys_role_menu` VALUES ('75', '6', '19');
INSERT INTO `sys_role_menu` VALUES ('76', '6', '5');
INSERT INTO `sys_role_menu` VALUES ('77', '6', '6');
INSERT INTO `sys_role_menu` VALUES ('96', '3', '1');
INSERT INTO `sys_role_menu` VALUES ('97', '3', '2');
INSERT INTO `sys_role_menu` VALUES ('98', '3', '3');
INSERT INTO `sys_role_menu` VALUES ('99', '3', '4');
INSERT INTO `sys_role_menu` VALUES ('100', '3', '5');
INSERT INTO `sys_role_menu` VALUES ('101', '3', '6');-- ----------------------------
-- Table structure for sys_user
-- ----------------------------
DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user` (`id` bigint(20) NOT NULL AUTO_INCREMENT,`username` varchar(64) DEFAULT NULL,`password` varchar(64) DEFAULT NULL,`avatar` varchar(255) DEFAULT NULL,`email` varchar(64) DEFAULT NULL,`city` varchar(64) DEFAULT NULL,`created` datetime DEFAULT NULL,`updated` datetime DEFAULT NULL,`last_login` datetime DEFAULT NULL,`statu` int(5) NOT NULL,PRIMARY KEY (`id`),UNIQUE KEY `UK_USERNAME` (`username`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;-- ----------------------------
-- Records of sys_user
-- ----------------------------
INSERT INTO `sys_user` VALUES ('1', 'admin', '123456', 'https://image-1300566513.cos.ap-guangzhou.myqcloud.com/upload/images/5a9f48118166308daba8b6da7e466aab.jpg', '123@qq.com', '廣州', '2021-01-12 22:13:53', '2021-01-16 16:57:32', '2020-12-30 08:38:37', '1');
INSERT INTO `sys_user` VALUES ('2', 'test', '123456', 'https://image-1300566513.cos.ap-guangzhou.myqcloud.com/upload/images/5a9f48118166308daba8b6da7e466aab.jpg', 'test@qq.com', null, '2021-01-30 08:20:22', '2021-01-30 08:55:57', null, '1');-- ----------------------------
-- Table structure for sys_user_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_user_role`;
CREATE TABLE `sys_user_role` (`id` bigint(20) NOT NULL AUTO_INCREMENT,`user_id` bigint(20) NOT NULL,`role_id` bigint(20) NOT NULL,PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=14 DEFAULT CHARSET=utf8mb4;-- ----------------------------
-- Records of sys_user_role
-- ----------------------------
INSERT INTO `sys_user_role` VALUES ('4', '1', '6');
INSERT INTO `sys_user_role` VALUES ('7', '1', '3');
INSERT INTO `sys_user_role` VALUES ('13', '2', '3');
⑥:創建根據用戶名獲取用戶接口
1.實體類
SysUser
@Data
@ApiModel(description = "用戶實體類")
public class SysUser implements Serializable{private static final long serialVersionUID = 1L;@TableId(value = "id", type = IdType.AUTO)@ApiModelProperty("用戶id,主鍵")private Long id;@NotBlank(message = "用戶名不能為空")@ApiModelProperty("用戶名")private String username;@ApiModelProperty("用戶密碼")private String password;@ApiModelProperty("頭像")private String avatar;@NotBlank(message = "郵箱不能為空")@Email(message = "郵箱格式不正確")@ApiModelProperty("郵箱")private String email;@ApiModelProperty("城市")private String city;@ApiModelProperty("最后登錄時間")private LocalDateTime lastLogin;@ApiModelProperty("創建時間")private LocalDateTime created;@ApiModelProperty("更新時間")private LocalDateTime updated;@ApiModelProperty("用戶狀態")private Integer statu;@ApiModelProperty("用戶權限")@TableField(exist = false)private List<String> auths;
}
2.創建
SysUserMapper
@Mapper
public interface SysUserMapper extends BaseMapper<SysUser> {
}
3.啟動類上加
@MapperScan("com.it.App.mapper")
4.創建
SysUserService
public interface SysUserService {Response<?> getUserByName(String username);
}
5.創建
SysUserServiceImpl
@Service
public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> implements SysUserService {@Autowiredprivate SysUserMapper sysUserMapper;@Overridepublic Response<?> getUserByName(String username) {QueryWrapper<SysUser> userQueryWrapper = new QueryWrapper<>();QueryWrapper<SysUser> wrapper = userQueryWrapper.eq("username", username);SysUser sysUser = sysUserMapper.selectOne(wrapper);// 是否查詢到用戶if (ObjectUtil.isNull(sysUser)){return Response.error("查無此人");}return Response.ok(sysUser);}
}
6.創建
SysUserController
@RestController
@RequestMapping("/sys")
@Api(tags = "用戶相關接口")
public class SysUserController {@Autowiredprivate SysUserService sysUserService;@GetMapping("/getUser")@ApiOperation("根據用戶名獲取用戶")public Response<?> getUserByName(String username){return sysUserService.getUserByName(username);}
7.測試http://localhost:8083/sys/getUser?username=admin
- 測試成功 說明我們mybatisPlus引入是沒有問題的
⑦:配置Knife4j生成api文檔在線測試
配置詳情筆記:https://blog.csdn.net/cygqtt/article/details/134544894
注意:配置完成之后是訪問不到的,因為被SpringSecurity攔截了,需要放行
如何放行:
在下文 登錄接口實現 里的 添加配置
二、實現數據庫用戶登錄
認證流程
①:自定義UserDetailService
1.首先創建一個
LoginUser
實現UserDetails
用于驗證返回的數據
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class LoginUser implements UserDetails {// 引入我們的sysUser實體類private SysUser sysUser;@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {return null;}@Overridepublic String getPassword() {// 返回密碼后將密碼置空String password = sysUser.getPassword();sysUser.setPassword(null);return password;}@Overridepublic String getUsername() {return sysUser.getUsername();}@Overridepublic boolean isAccountNonExpired() {return true;}@Overridepublic boolean isAccountNonLocked() {return true;}@Overridepublic boolean isCredentialsNonExpired() {return true;}@Overridepublic boolean isEnabled() {return true;}
}
2.創建
UserDetailServiceImpl
實現UserDetailsService
用于自定義登錄
@Service
public class UserDetailServiceImpl implements UserDetailsService {@Autowiredprivate SysUserMapper sysUserMapper;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {// 登錄驗證QueryWrapper<SysUser> userQueryWrapper = new QueryWrapper<>();QueryWrapper<SysUser> wrapper = userQueryWrapper.eq("username", username);SysUser sysUser = sysUserMapper.selectOne(wrapper);// 是否查詢到用戶, 如果沒有查詢到永固拋出異常if (ObjectUtil.isNull(sysUser)){throw new RuntimeException("用戶名或密碼錯誤");}// TODO 權限驗證// 將查詢出來的用戶封裝成UserDetails返回return LoginUser.builder().sysUser(sysUser).build();}
}
3.創建
SecurityConfig
配置類 配置密碼的加密方式
- 如果不配置直接登錄會報錯
There is no PasswordEncoder mapped for the id "null"
意思就是說密碼的加密方式為空
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {// 指定一個密碼的加密方式@BeanBCryptPasswordEncoder bCryptPasswordEncoder() {return new BCryptPasswordEncoder();}
}
4.雖然指定了加密方式但是數據庫中的密碼還是明文 所以要改成密文
- 我們可以寫一個測試類 將明文轉換為密碼 然后將密碼存到數據庫中
@SpringBootTest
@Slf4j
class ApplicationTests {@Autowiredprivate BCryptPasswordEncoder bCryptPasswordEncoder;@Testvoid getPwd() {String encode = bCryptPasswordEncoder.encode("123456");log.info("加密后的密文為: {}", encode);}}
②:測試登錄
1.登錄
2.請求測試接口 http://localhost:8083/sys/getUser?username=admin
③:登錄接口實現
01.添加配置
-
在登錄過程中 真正的認證邏輯還是交給
SpringSecurity
的,所以需要重寫authenticationManagerBean()
這個方法 -
在登錄時我們要放開登錄接口,需要重寫
configure(HttpSecurity http)
這個方法 指定放開的路徑
1.配置類
SecurityConfig
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {public static final String[] URL_WHITELIST = {"/webjars/**","/favicon.ico","/sys/captcha","/sys/login","/sys/logout","/swagger-resources/**","/v2/api-docs","/swagger-ui.html","/webjars/**", // 放行knife4j生成的接口文檔(/swagger-resources 和 /v2/api-docs 還有一些其他的資源路徑, /swagger-ui.html、/webjars/** )};// 指定一個密碼的加密方式@BeanBCryptPasswordEncoder bCryptPasswordEncoder() {return new BCryptPasswordEncoder();}@Bean@Overridepublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}// 配置HttpSecurity,定義安全策略@Overrideprotected void configure(HttpSecurity http) throws Exception {http.cors().and().csrf().disable() // 啟用跨越支持,禁用CSRF保護.formLogin().and().authorizeRequests().antMatchers(URL_WHITELIST).permitAll() // 設置白名單,允許訪問的URL.antMatchers(String.valueOf(HttpMethod.OPTIONS), "/**").permitAll() // 放行OPTIONS請求: Swagger可能會發出OPTIONS請求,確保這個請求也被放行.anyRequest().authenticated() // 其他所有請求需要身份驗證.and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); // 不會創建session}
}
02.登錄接口
1.直接在
SysUserController
中添加登錄方法即可
@PostMapping("/login")
@ApiOperation("用戶登錄")
public Response<?> login(@RequestBody SysUser sysUser){return sysUserService.login(sysUser);
}
2.
SysUserService
Response<?> login(SysUser sysUser);
3.
SysUserServiceImpl
@Autowiredprivate AuthenticationManager authenticationManager;@Autowiredprivate JwtUtils jwtUtils;@Autowiredprivate RedisUtil redisUtil;@Overridepublic Response<?> login(SysUser sysUser) {// AuthenticationManager 進行用戶認證UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(sysUser.getUsername(), sysUser.getPassword());Authentication authenticate = authenticationManager.authenticate(authenticationToken);// 如果認證沒有通過 給出對應的提示if (ObjectUtil.isNull(authenticate)){throw new RuntimeException("用戶名或密碼錯誤!");}// 如果認證通過, 使用userId生成一個Jwt jwt存入到Response中返回LoginUser loginUser = (LoginUser) authenticate.getPrincipal();// 通過userId生成tokenString userId = loginUser.getSysUser().getId().toString();String token = jwtUtils.generateToken(userId);Map<Object, Object> map = MapUtil.builder().put("token", token).build();// 把完整的用戶信息存入到redis中 統一的前綴 login 過期時間為10分鐘String jsonString = objectMapper.writeValueAsString(loginUser);redisUtil.hset(Const.Login_Key,userId,jsonString,60*10);// 返回登錄成功的結果return Response.ok(map);}
03.測試登錄
-
因為我們導入 knife4j 生成了接口文檔所以可以使用knife4j發送請求測試
-
訪問:http://localhost:8083/doc.html
1.發送登錄請求
④:token認證過濾器代碼實現
01.創建token認證過濾器
1.JWTAuthenticationFilter
@Component
@Slf4j
public class JWTAuthenticationFilter extends OncePerRequestFilter {@Autowiredprivate JwtUtils jwtUtils;@Autowiredprivate RedisUtil redisUtil;@Autowiredprivate ObjectMapper objectMapper;// 進行JWT校驗的過濾操作@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {// 日志記錄JWT校驗過濾器的執行log.info("JWT校驗過濾器執行");// 從請求頭中獲取JWTString token = request.getHeader("token");// 如果token為空,則放行,繼續處理下一個過濾器if (StrUtil.isBlankOrUndefined(token)){chain.doFilter(request,response);return;}// token不為空 使用Jwt工具類 解析獲取聲明Claims claims = jwtUtils.getClaimByToken(token);// 如果 token異常 則拋出異常if (claims == null){throw new RuntimeException("Token異常");}// 如果 token已過期 則拋出異常if (jwtUtils.isTokenExpired(claims)){throw new RuntimeException("Token已過期");}// 從token中獲取用戶idString userId = claims.getSubject();// 從redis中獲取用戶的全部信息String loginUserStr = (String) redisUtil.hget(Const.Login_Key , userId);LoginUser loginUser = objectMapper.readValue(loginUserStr, LoginUser.class);SysUser sysUser = loginUser.getSysUser();// 日志記錄正在登錄的用戶信息log.info("用戶-{},正在登錄!", sysUser.getUsername());// TODO 獲取權限信息封裝到Authentication中UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null,null);// 將認證信息設置到安全上下文中SecurityContextHolder.getContext().setAuthentication(authenticationToken);// 繼續處理請求chain.doFilter(request,response);}
}
2.將登錄驗證碼校驗過濾器加入到過濾器鏈中
- SecurityConfig
@Autowiredprivate JWTAuthenticationFilter jwtAuthenticationFilter;.....// 配置HttpSecurity,定義安全策略@Overrideprotected void configure(HttpSecurity http) throws Exception {http.cors().and().csrf().disable() // 啟用跨越支持,禁用CSRF保護.formLogin().and().authorizeRequests().antMatchers(URL_WHITELIST).permitAll() // 設置白名單,允許訪問的URL.anyRequest().authenticated() // 其他所有請求需要身份驗證.and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); // 不會創建session// 將登錄驗證碼校驗過濾器加入到過濾器鏈中http.addFilterBefore(jwtAuthenticationFilter,UsernamePasswordAuthenticationFilter.class);}
02.測試登錄
1.登錄
- redis中也存入了對象
2.攜帶token訪問其他接口
⑤:登出接口實現
思路:退出登錄時會攜帶token ==> 獲取token中的用戶id ==> 根據用戶id 刪除redis中存儲的用戶信息 ==>(如果有前臺則登出成功后刪除已緩存的token)
01. 登錄接口實現
1.SysUserController
@GetMapping("/logout")@ApiOperation("用戶登出")public Response<?> logout(){return sysUserService.logout();}
2.SysUserService
Response<?> logout();
3.SysUserServiceImpl
@Overridepublic Response<?> logout() {// 獲取當前用戶的認證信息Authentication authentication = SecurityContextHolder.getContext().getAuthentication();// 從認證信息中獲取登錄用戶對象LoginUser loginUser = (LoginUser) authentication.getPrincipal();// 如果登錄用戶為空,拋出異常,表示鑒權失敗if (ObjectUtil.isNull(loginUser)) {throw new BaseException("鑒權失敗!");}// 從Redis中刪除用戶登錄信息String userId = loginUser.getSysUser().getId().toString();redisUtil.hdel(Const.Login_Key, userId);// 返回操作成功的響應return Response.ok("操作成功!");}
02.處理全局異常
1.創建BaseException
/*** @Author: Coke* @DateTime: 2023/11/23/9:53* @注釋: 業務異常**/
public class BaseException extends RuntimeException {public BaseException() {}public BaseException(String msg) {super(msg);}}
2.創建GlobalExceptionHandler
/*** @Author: Coke* @DateTime: 2023/11/23/9:31* @注釋: 全局異常處理器,處理項目中拋出的業務異常**/
@Slf4j
@RestControllerAdvice // 用于全局處理控制器層(Controller)的異常
public class GlobalExceptionHandler {/*** 捕獲業務異常* @param e: * @return Response<?>* @author: Coke* @DateTime: 2023/11/23 9:33*/@ExceptionHandler(BaseException.class)public Response<?> exceptionHandler(BaseException e){log.error("異常信息:{}", e.getMessage());return Response.error(201,e.getMessage());}
}
3.將之前拋出的所有RuntimeException 改成BaseException
4.修改JWTAuthenticationFilter
- 在過濾器中的異常 我們自定義的全局異常捕獲只做用與Controller層以及控制層的調用鏈上 所以我們直接在filer中try catch 捕獲然后直接response響應回去就好了 當然也可以做一個AOP的切面來捕獲過濾器中的異常
@Component
@Slf4j
public class JWTAuthenticationFilter extends OncePerRequestFilter {@Autowiredprivate JwtUtils jwtUtils;@Autowiredprivate RedisUtil redisUtil;@Autowiredprivate ObjectMapper objectMapper;// 進行JWT校驗的過濾操作@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {// 日志記錄JWT校驗過濾器的執行log.info("JWT校驗過濾器執行");try {// 從請求頭中獲取JWTString token = request.getHeader("token");// 如果token為空,則放行,繼續處理下一個過濾器if (StrUtil.isBlankOrUndefined(token)) {chain.doFilter(request, response);return;}// token不為空 使用Jwt工具類 解析獲取聲明Claims claims = jwtUtils.getClaimByToken(token);// 如果 token異常 則拋出異常if (claims == null) {throw new BaseException("Token異常");}// 如果 token已過期 則拋出異常if (jwtUtils.isTokenExpired(claims)) {throw new BaseException("Token已過期");}// 從token中獲取用戶idString userId = claims.getSubject();// 從redis中獲取用戶的全部信息String loginUserStr = (String) redisUtil.hget(Const.Login_Key, userId);if (ObjectUtil.isNull(loginUserStr)) {throw new BaseException("鑒權失敗!請求重新登錄。");}LoginUser loginUser = objectMapper.readValue(loginUserStr, LoginUser.class);SysUser sysUser = loginUser.getSysUser();// 日志記錄正在登錄的用戶信息log.info("用戶-{},正在登錄!", sysUser.getUsername());// TODO 獲取權限信息封裝到Authentication中UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, null);// 將認證信息設置到安全上下文中SecurityContextHolder.getContext().setAuthentication(authenticationToken);// 繼續處理請求chain.doFilter(request, response);} catch (BaseException e) {// 捕獲并處理異常log.error("JWT校驗過濾器異常:{}", e.getMessage());response.setContentType("application/json;charset=UTF-8");response.setStatus(HttpServletResponse.SC_FORBIDDEN);ServletOutputStream outputStream = response.getOutputStream();Response<?> result = Response.error(201, e.getMessage());outputStream.write(JSONUtil.toJsonStr(result).getBytes(StandardCharsets.UTF_8));outputStream.flush();outputStream.close();}}
}
03.測試
1.登錄
- 登錄成功并且拿到了Token
- Redis中也存入了用戶信息
2.攜帶Token獲取用戶信息
- 成功
3.請求登出接口
- 登出成功 并且Redis中的數據也被刪除了
4.再次攜帶Token獲取用戶信息
三、權限
①:權限實現
01.限制訪問資源所需權限
1.
SecurityConfig
中開啟全局方法安全
@EnableGlobalMethodSecurity(prePostEnabled = true) // 啟用全局方法安全
2.在controller接口上設置 訪問接口所需要的權限
- SysUserController
@PreAuthorize("hasAuthority('sys:getUser')")
- 為了測試我們在 TestController 接口上也加一個權限(不存在的權限)
02.封裝權限信息
1.LoginUser
// 權限private List<String> auths;// 定義一個新的權限集合List<SimpleGrantedAuthority> newAuths;@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {// 如果 newAuths 為空 第一個進來需要轉換 如果不是直接返回if (ObjectUtil.isNull(newAuths)){// 將String類型的權限轉成SimpleGrantedAuthority類型newAuths = auths.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());}return newAuths;}
2.UserDetailServiceImpl
@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {// 登錄驗證QueryWrapper<SysUser> userQueryWrapper = new QueryWrapper<>();QueryWrapper<SysUser> wrapper = userQueryWrapper.eq("username", username);SysUser sysUser = sysUserMapper.selectOne(wrapper);// 是否查詢到用戶, 如果沒有查詢到永固拋出異常if (ObjectUtil.isNull(sysUser)){throw new BaseException("用戶名或密碼錯誤");}// TODO 權限驗證// 先將權限寫死ArrayList<String> auths = new ArrayList<>(Arrays.asList("sys:getUser", "sys:addUser", "sys:delUser"));// 將查詢出來的用戶封裝成UserDetails返回return LoginUser.builder().sysUser(sysUser).auths(auths).build();}
3.JWTAuthenticationFilter
// TODO 獲取權限信息封裝到Authentication中Collection<? extends GrantedAuthority> authorities = loginUser.getAuthorities();UsernamePasswordAuthenticationToken authenticationToken =new UsernamePasswordAuthenticationToken(loginUser, null, authorities);
03.測試
1.修改RedisTemplate鍵和值的序列化
這里解釋一下為什么要注銷掉
-
首先我們指定的是值的序列化器為 StringRedisSerializer 所以我們存的值要轉成String類型,這樣我們可以清楚的看懂存的是什么
-
其次我們從redis中獲取到String類型的值后還要轉成對象(問題就在這里平常對象當然沒問題,但是我們今天存了這個類型的字段
List<SimpleGrantedAuthority> newAuths;
注意:SimpleGrantedAuthority
沒有無參構造方法) -
然而字符串轉對象調用的就是無參構造(所以會報錯)
-
最后 干脆我們直接存Redis中的值為對象好了
所以我們需要改動兩個地方
-
- SysUserServiceImpl
- SysUserServiceImpl
-
- JWTAuthenticationFilter
- JWTAuthenticationFilter
2.首先登錄然后拿到Token
3.攜帶Token獲取用戶信息(有這個權限可以獲取到)
4.攜帶Token請求Hello接口(沒有hello的權限,不能訪問)
②:基于數據庫的權限實現
01.介紹
1.看一下流程就明白了
02.新增一些測試接口
1.SysUserController
- 由于測試我們直接返回即可(重點在權限驗證上)
@PostMapping("/user/save")@ApiOperation("添加用戶")@PreAuthorize("hasAuthority('sys:role:save')")public Response<?> userSave(){return Response.ok("新增用戶成功!");}@PostMapping("/user/update")@ApiOperation("修改用戶")@PreAuthorize("hasAuthority('sys:role:update')")public Response<?> updateSave(){return Response.ok("更新用戶成功!");}@GetMapping("/user/delete")@ApiOperation("刪除用戶")@PreAuthorize("hasAuthority('sys:role:delete')")public Response<?> deleteSave(){return Response.ok("刪除用戶成功!");}
03.查詢SQL實現
1.SysUserMapper
@Mapper
public interface SysUserMapper extends BaseMapper<SysUser> {@Select("select sm.perms\n" +"from sys_user su\n" +" join sys_user_role sur on sur.user_id = su.id\n" +" join sys_role sr on sur.role_id = sr.id\n" +" join sys_role_menu srm on sr.id = srm.role_id\n" +" join sys_menu sm on srm.menu_id = sm.id\n" +"where su.id = #{userId}")List<String> getMenuByUserId(Long userId);
}
2.UserDetailServiceImpl
// 根據 用戶id 從數據庫中查詢權限List<String> auths = sysUserMapper.getMenuByUserId(sysUser.getId());
③:測試
01.使用admin用戶測試
- 測試結果:有權限都可以訪問
1.登錄獲取到token
2.測試新增用戶接口
3.測試修改用戶接口
4.測試刪除用戶接口
02.使用test用戶測試
- 測試結果:沒有權限都不可以訪問
1.登錄獲取到token
- 登錄成功后redis中就有兩個用戶信息了
2.測試新增用戶接口
3.測試修改用戶接口
4.測試刪除用戶接口
四、自定義異常處理(完善)
我們還希望在認證失敗或者是授權失敗的情況下也能和我們的接口一樣返回相同結構的jso,這樣可以讓前端能對響應進行統一的處理。要實現這個功能我們需要知道SpringSecurity的異常處理機制。
在SpringSecurity中,如果我們在認證或者授權的過程中出現了異常會被ExceptionTranslationFilter捕獲到。在ExceptionTranslation Filter中會去判斷是認證失敗還是授權失敗出現的異常。
如果是認證過程中出現的異常會被封裝成AuthenticationException:然后調用AuthenticationEntryPoint)對象的方法去進行異常處
理。
如果是授權過程中出現的異常會被封裝成AccessDeniedException?然后調用*AccessDeniedHandler**對象的方法去進行異常處理。
所以如果我們需要自定義異常處理,我們只需要自定義AuthenticationEntryPoint和AccessDeniedHandler然后配置給SpringSecurity即可。
①:自定義實現類
1.授權失敗異常處理 (AccessDeniedHandlerImpl)
/*** @Author: Coke* @DateTime: 2023/11/23/16:38* @注釋: 授權失敗異常處理**/
@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {@Overridepublic void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {response.setContentType("application/json;charset=UTF-8");response.setStatus(HttpServletResponse.SC_FORBIDDEN);ServletOutputStream outputStream = response.getOutputStream();Response<?> result = Response.error(HttpStatus.FORBIDDEN.value(), "您權限不足!");outputStream.write(JSONUtil.toJsonStr(result).getBytes(StandardCharsets.UTF_8));outputStream.flush();outputStream.close();}
}
2.認證失敗異常處理 (AuthenticationEntryPointImpl)
/*** @Author: Coke* @DateTime: 2023/11/23/16:34* @注釋: 認證失敗異常處理**/
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {@Overridepublic void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {response.setContentType("application/json;charset=UTF-8");response.setStatus(HttpServletResponse.SC_FORBIDDEN);ServletOutputStream outputStream = response.getOutputStream();Response<?> result = Response.error(HttpStatus.UNAUTHORIZED.value(), "用戶認證失敗!請重新登錄");outputStream.write(JSONUtil.toJsonStr(result).getBytes(StandardCharsets.UTF_8));outputStream.flush();outputStream.close();}
}
3.修改JWTAuthenticationFilter
- 之前我們是在JWTAuthenticationFilter中使用try – catch 捕獲的異常然后處理的現在不需要了
- 刪除try – catch 處理異常的代碼
- 拋出的異常BaseException改成RuntimeException
修改后的代碼如下
@Component
@Slf4j
public class JWTAuthenticationFilter extends OncePerRequestFilter {@Autowiredprivate JwtUtils jwtUtils;@Autowiredprivate RedisUtil redisUtil;// 進行JWT校驗的過濾操作@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {// 日志記錄JWT校驗過濾器的執行log.info("JWT校驗過濾器執行");// 從請求頭中獲取JWTString token = request.getHeader("token");// 如果token為空,則放行,繼續處理下一個過濾器if (StrUtil.isBlankOrUndefined(token)) {chain.doFilter(request, response);return;}// token不為空 使用Jwt工具類 解析獲取聲明Claims claims = jwtUtils.getClaimByToken(token);// 如果 token異常 則拋出異常if (claims == null) {throw new RuntimeException("Token異常");}// 如果 token已過期 則拋出異常if (jwtUtils.isTokenExpired(claims)) {throw new RuntimeException("Token已過期");}// 從token中獲取用戶idString userId = claims.getSubject();// 從redis中獲取用戶的全部信息LoginUser loginUser = (LoginUser) redisUtil.hget(Const.Login_Key, userId);if (ObjectUtil.isNull(loginUser)) {throw new RuntimeException("鑒權失敗!請求重新登錄。");}
// LoginUser loginUser = objectMapper.readValue(loginUserStr, LoginUser.class);SysUser sysUser = loginUser.getSysUser();// 日志記錄正在登錄的用戶信息log.info("用戶-{},正在登錄!", sysUser.getUsername());// TODO 獲取權限信息封裝到Authentication中Collection<? extends GrantedAuthority> authorities = loginUser.getAuthorities();UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, authorities);// 將認證信息設置到安全上下文中SecurityContextHolder.getContext().setAuthentication(authenticationToken);// 繼續處理請求chain.doFilter(request, response);}
}
②:配置給SpringSecurity
1.SecurityConfig
@Autowiredprivate AccessDeniedHandlerImpl accessDeniedHandler;@Autowiredprivate AuthenticationEntryPointImpl authenticationEntryPoint;// 配置異常處理器(認證異常和授權異常)http.exceptionHandling()// 配置 認證異常處理器.authenticationEntryPoint(authenticationEntryPoint)// 配置授權異常處理器.accessDeniedHandler(accessDeniedHandler);
③:測試
1.登錄給出錯誤密碼
2.使用Test用戶登錄后訪問新增用戶接口(沒有這個權限)
五、跨域
瀏覽器出于安全的考慮,使用XMLHttpRequest對象發起HTP請求時必須遵守同源策略,否則就是跨域的HTP請求,默認情況下是被禁止的。同源策略要求源相同才能正常進行通信,即協議、域名、端口號都完全一致。
前后端分離項目,前端項目和后端項目一般都不是同源的,所以肯定會存在跨域請求的問題。
所以我們就要處理一下,讓前端能進行跨域請求。
①:先對SpringBoot配置,允許跨域請求
1.創建CorsConfig
@Configuration
public class CorsConfig implements WebMvcConfigurer {@Overridepublic void addCorsMappings (CorsRegistry registry) {// 設置允許跨域的路徑registry.addMapping("/**")// 設置允許跨域的域名.allowedOriginPatterns("*")// 是否允許cookie.allowCredentials(true)// 這是允許的請求方式.allowedMethods("GET","POST","DELETE","PUT")//設置允許的header屬性.allowedHeaders("*")// 跨域允許時間.maxAge(3600);}
}
②:開啟SpringSecurity的跨域訪問
六、其他權限校驗方法
我們前面都是使用@PreAuthorize注解,然后在在其中使用的是hasAuthority方法進行校驗。SpringSecurityi還為我們提供了其它方法
例如:hasAnyAuthority,hasRole,hasAnyRole,等。
這里我們先不急著去介紹這些方法,我們先去理解nasAuthority的原理,然后再去學習其他方法你就更容易理解,而不是死記硬背區別。并且我們也可以選擇定義校驗方法,實現我們自己的校驗邏輯。
hasAuthority)方法實際是執行到了SecurityExpressionRoot的nasAuthority,大家只要斷點調試既可知道它內部的校驗原理。
它內部其實是調用authenticationl的getAuthorities方法獲取用戶的權限列表。然后判斷我們存入的方法參數數據在權限列表中。
hasAnyAuthority方法可以傳入多個權限,只有用戶有其中任意一個權限都可以訪問對應資源。
@GetMapping("hello2")@ApiOperation("測試接口hello多個權限")@PreAuthorize("hasAnyAuthority('hello','sys:role:save')")public String hello2(){return "您請求了一個測試接口-hello多個權限";}
hasRole要求有對應的角色才可以訪問,但是它內部會把我們傳入的參數拼接上RoLE后再去比較。所以這種情況下要用用戶對應的權限也要有ROLE這個前綴才可以。
hasAnyRole有任意的角色就可以訪問。它內部也會把我們傳入的參數拼接上RoLE_后再去比較。所以這種情況下要用用戶對應的權限也要有ROLE_這個前綴才可以。
①:自定義權限校驗
1.com.it.App.expression.MyExpressionRoot(自己定義權限校驗)
/*** @Author: Coke* @DateTime: 2023/11/24/9:00* @注釋: 自定義權限校驗**/
@Component("MyEx") // 自定義一下容器中Bean的名字
public class MyExpressionRoot {public boolean hasAuthority(String authority){// 獲取當前用戶的權限Authentication authentication = SecurityContextHolder.getContext().getAuthentication();LoginUser loginUser = (LoginUser) authentication.getPrincipal();List<String> auths = loginUser.getAuths();// 判斷用戶權限集合中是否存在authorityreturn auths.contains(authority);}
}
2.使用自己定義的權限校驗
@GetMapping("hello3")@ApiOperation("自定義權限校驗")@PreAuthorize("@MyEx.hasAuthority('hello')") // 在SPEL表達式中使用@MyEx相當于獲取容器中bean的名字未MyEx的對象。public String hello3(){return "您請求了一個測試接口-hello自定義權限校驗";}
②:基于配置的權限校驗
.antMatchers("/user/save").hasAuthority("sys:role:save") // 訪問 /user/save接口 必須要擁有sys:role:save權限
七、CSRF
CSRF是指跨站請求偽造(Cross-site request forgery),是web常見的攻擊之一。
https://blog.csdn.net/freeking101/article/details/86537087
SpringSecurity去防l止CSRF攻擊的方式就是通過csrf_token。后端會生成一個csrf_token,前端發起請求的時候需要攜帶這個csrf_token,后端會有過濾器進行校驗,如果沒有攜帶或者是偽造的就不允許訪問。
我們可以發現cSRF攻擊依靠的是cookie中所攜帶的認證信息。但是在前后端分離的項目中我們的認證信息其實是token,而token并不是存儲中cookie中,并且需要前端代碼去把token設置到請求頭中才可以,所以CSRF攻擊也就不用擔心了。