浙江建設(shè)廳網(wǎng)站官網(wǎng)seo關(guān)鍵詞排名系統(tǒng)
文章目錄
- 1. Sa-Token 介紹
- 2. 登錄認(rèn)證
- 2.1 登錄與注銷
- 2.2 會(huì)話查詢
- 2.3 Token 查詢
- 3. 權(quán)限認(rèn)證
- 3.1 獲取當(dāng)前賬號(hào)權(quán)限碼集合
- 3.2 權(quán)限校驗(yàn)
- 3.3 角色校驗(yàn)
- 4. 前后臺(tái)分離(無Cookie模式)
- 5. Sa-Token 集成 Redis
- 6. SpringBoot 集成 Sa-Token
- 6.1 創(chuàng)建項(xiàng)目
- 6.2 添加依賴
- 6.3 設(shè)置配置文件
- 6.4 創(chuàng)建啟動(dòng)類
- 6.5 定義用戶信息類
- 6.6 自定義權(quán)限驗(yàn)證接口擴(kuò)展
- 6.7 創(chuàng)建測試Controller
- 6.8 運(yùn)行
1. Sa-Token 介紹
Sa-Token 是一個(gè)輕量級(jí) Java 權(quán)限認(rèn)證框架,主要解決:登錄認(rèn)證、權(quán)限認(rèn)證、單點(diǎn)登錄、OAuth2.0、分布式Session會(huì)話、微服務(wù)網(wǎng)關(guān)鑒權(quán) 等一系列權(quán)限相關(guān)問題。
功能結(jié)構(gòu)圖
2. 登錄認(rèn)證
對(duì)于一些登錄之后才能訪問的接口(例如:查詢我的賬號(hào)資料),我們通常的做法是增加一層接口校驗(yàn):
- 如果校驗(yàn)通過,則:正常返回?cái)?shù)據(jù)。
- 如果校驗(yàn)未通過,則:拋出異常,告知其需要先進(jìn)行登錄。
那么,判斷會(huì)話是否登錄的依據(jù)是什么?我們先來簡單分析一下登錄訪問流程:
- 用戶提交
name
+password
參數(shù),調(diào)用登錄接口。 - 登錄成功,返回這個(gè)用戶的
Token
會(huì)話憑證。 - 用戶后續(xù)的每次請(qǐng)求,都攜帶上這個(gè)
Token
。 - 服務(wù)器根據(jù)
Token
判斷此會(huì)話是否登錄成功。
所謂登錄認(rèn)證,指的就是服務(wù)器校驗(yàn)賬號(hào)密碼,為用戶頒發(fā) Token
會(huì)話憑證的過程,這個(gè)Token
也是我們后續(xù)判斷會(huì)話是否登錄的關(guān)鍵所在。
2.1 登錄與注銷
// 會(huì)話登錄:參數(shù)填寫要登錄的賬號(hào)id,建議的數(shù)據(jù)類型:long | int | String, 不可以傳入復(fù)雜類型,如:User、Admin 等等
StpUtil.login(Object id);
只此一句代碼,便可以使會(huì)話登錄成功,實(shí)際上,Sa-Token 在背后做了大量的工作,包括但不限于:
- 檢查此賬號(hào)是否之前已有登錄
- 為賬號(hào)生成
Token
憑證與Session
會(huì)話 - 通知全局偵聽器,xx 賬號(hào)登錄成功
- 將
Token
注入到請(qǐng)求上下文 - 等等其它工作……
只需要記住關(guān)鍵一點(diǎn):Sa-Token 為這個(gè)賬號(hào)創(chuàng)建了一個(gè)Token憑證,且通過 Cookie 上下文返回給了前端
。
此處僅僅做了會(huì)話登錄,但并沒有主動(dòng)向前端返回 Token
信息。嚴(yán)格來講是需要的,只不過 StpUtil.login(id)
方法利用了 Cookie
自動(dòng)注入的特性,省略了你手寫返回 Token
的代碼。
// 當(dāng)前會(huì)話注銷登錄
StpUtil.logout();// 獲取當(dāng)前會(huì)話是否已經(jīng)登錄,返回true=已登錄,false=未登錄
StpUtil.isLogin();// 檢驗(yàn)當(dāng)前會(huì)話是否已經(jīng)登錄, 如果未登錄,則拋出異常:`NotLoginException`
StpUtil.checkLogin();
2.2 會(huì)話查詢
// 獲取當(dāng)前會(huì)話賬號(hào)id, 如果未登錄,則拋出異常:`NotLoginException`
StpUtil.getLoginId();// 類似查詢API還有:
StpUtil.getLoginIdAsString(); // 獲取當(dāng)前會(huì)話賬號(hào)id, 并轉(zhuǎn)化為`String`類型
StpUtil.getLoginIdAsInt(); // 獲取當(dāng)前會(huì)話賬號(hào)id, 并轉(zhuǎn)化為`int`類型
StpUtil.getLoginIdAsLong(); // 獲取當(dāng)前會(huì)話賬號(hào)id, 并轉(zhuǎn)化為`long`類型// ---------- 指定未登錄情形下返回的默認(rèn)值 ----------// 獲取當(dāng)前會(huì)話賬號(hào)id, 如果未登錄,則返回null
StpUtil.getLoginIdDefaultNull();// 獲取當(dāng)前會(huì)話賬號(hào)id, 如果未登錄,則返回默認(rèn)值 (`defaultValue`可以為任意類型)
StpUtil.getLoginId(T defaultValue);
2.3 Token 查詢
// 獲取當(dāng)前會(huì)話的token值
StpUtil.getTokenValue();// 獲取當(dāng)前`StpLogic`的token名稱
StpUtil.getTokenName();// 獲取指定token對(duì)應(yīng)的賬號(hào)id,如果未登錄,則返回 null
StpUtil.getLoginIdByToken(String tokenValue);// 獲取當(dāng)前會(huì)話剩余有效期(單位:s,返回-1代表永久有效)
StpUtil.getTokenTimeout();// 獲取當(dāng)前會(huì)話的token信息參數(shù)
StpUtil.getTokenInfo();
3. 權(quán)限認(rèn)證
所謂權(quán)限認(rèn)證,核心邏輯就是判斷一個(gè)賬號(hào)是否擁有指定權(quán)限:
- 有,就讓你通過。
- 沒有?那么禁止訪問!
深入到底層數(shù)據(jù)中,就是每個(gè)賬號(hào)都會(huì)擁有一個(gè)權(quán)限碼集合,框架來校驗(yàn)這個(gè)集合中是否包含指定的權(quán)限碼。
例如:當(dāng)前賬號(hào)擁有權(quán)限碼集合 ["user-add", "user-delete", "user-get"]
,這時(shí)候我來校驗(yàn)權(quán)限 "user-update"
,則其結(jié)果就是:驗(yàn)證失敗,禁止訪問。
3.1 獲取當(dāng)前賬號(hào)權(quán)限碼集合
因?yàn)槊總€(gè)項(xiàng)目的需求不同,其權(quán)限設(shè)計(jì)也千變?nèi)f化,因此 [ 獲取當(dāng)前賬號(hào)權(quán)限碼集合 ] 這一操作不可能內(nèi)置到框架中, 所以 Sa-Token 將此操作以接口的方式暴露給你,以方便你根據(jù)自己的業(yè)務(wù)邏輯進(jìn)行重寫。
你需要做的就是新建一個(gè)類,實(shí)現(xiàn) StpInterface
接口,例如以下代碼:
/*** 自定義權(quán)限驗(yàn)證接口擴(kuò)展*/
@Component // 保證此類被SpringBoot掃描,完成Sa-Token的自定義權(quán)限驗(yàn)證擴(kuò)展
public class StpInterfaceImpl implements StpInterface {/*** 返回一個(gè)賬號(hào)所擁有的權(quán)限碼集合 */@Overridepublic List<String> getPermissionList(Object loginId, String loginType) {// 本list僅做模擬,實(shí)際項(xiàng)目中要根據(jù)具體業(yè)務(wù)邏輯來查詢權(quán)限List<String> list = new ArrayList<String>(); list.add("101");list.add("user.add");list.add("user.update");list.add("user.get");// list.add("user.delete");list.add("art.*");return list;}/*** 返回一個(gè)賬號(hào)所擁有的角色標(biāo)識(shí)集合 (權(quán)限與角色可分開校驗(yàn))*/@Overridepublic List<String> getRoleList(Object loginId, String loginType) {// 本list僅做模擬,實(shí)際項(xiàng)目中要根據(jù)具體業(yè)務(wù)邏輯來查詢角色List<String> list = new ArrayList<String>(); list.add("admin");list.add("super-admin");return list;}
}
參數(shù)解釋:
loginId
:賬號(hào)id,即你在調(diào)用 StpUtil.login(id)
時(shí)寫入的標(biāo)識(shí)值。
loginType
:賬號(hào)體系標(biāo)識(shí),此處暫時(shí)忽略,可以詳細(xì)了解 [ 多賬戶認(rèn)證 ]
3.2 權(quán)限校驗(yàn)
然后就可以用以下api來鑒權(quán)了
// 獲取:當(dāng)前賬號(hào)所擁有的權(quán)限集合
StpUtil.getPermissionList();// 判斷:當(dāng)前賬號(hào)是否含有指定權(quán)限, 返回 true 或 false
StpUtil.hasPermission("user.add"); // 校驗(yàn):當(dāng)前賬號(hào)是否含有指定權(quán)限, 如果驗(yàn)證未通過,則拋出異常: NotPermissionException
StpUtil.checkPermission("user.add"); // 校驗(yàn):當(dāng)前賬號(hào)是否含有指定權(quán)限 [指定多個(gè),必須全部驗(yàn)證通過]
StpUtil.checkPermissionAnd("user.add", "user.delete", "user.get"); // 校驗(yàn):當(dāng)前賬號(hào)是否含有指定權(quán)限 [指定多個(gè),只要其一驗(yàn)證通過即可]
StpUtil.checkPermissionOr("user.add", "user.delete", "user.get");
3.3 角色校驗(yàn)
在Sa-Token中,角色和權(quán)限可以獨(dú)立驗(yàn)證
// 獲取:當(dāng)前賬號(hào)所擁有的角色集合
StpUtil.getRoleList();// 判斷:當(dāng)前賬號(hào)是否擁有指定角色, 返回 true 或 false
StpUtil.hasRole("super-admin"); // 校驗(yàn):當(dāng)前賬號(hào)是否含有指定角色標(biāo)識(shí), 如果驗(yàn)證未通過,則拋出異常: NotRoleException
StpUtil.checkRole("super-admin"); // 校驗(yàn):當(dāng)前賬號(hào)是否含有指定角色標(biāo)識(shí) [指定多個(gè),必須全部驗(yàn)證通過]
StpUtil.checkRoleAnd("super-admin", "shop-admin"); // 校驗(yàn):當(dāng)前賬號(hào)是否含有指定角色標(biāo)識(shí) [指定多個(gè),只要其一驗(yàn)證通過即可]
StpUtil.checkRoleOr("super-admin", "shop-admin");
權(quán)限通配符
Sa-Token允許你根據(jù)通配符指定泛權(quán)限,例如當(dāng)一個(gè)賬號(hào)擁有art.*
的權(quán)限時(shí),art.add、art.delete、art.update
都將匹配通過
上帝權(quán)限:當(dāng)一個(gè)賬號(hào)擁有
"*"
權(quán)限時(shí),他可以驗(yàn)證通過任何權(quán)限碼 (角色認(rèn)證同理)
前端有了鑒權(quán)后端還需要鑒權(quán)嗎?
需要!
前端的鑒權(quán)只是一個(gè)輔助功能,對(duì)于專業(yè)人員這些限制都是可以輕松繞過的,為保證服務(wù)器安全,無論前端是否進(jìn)行了權(quán)限校驗(yàn),后端接口都需要對(duì)會(huì)話請(qǐng)求再次進(jìn)行權(quán)限校驗(yàn)!
4. 前后臺(tái)分離(無Cookie模式)
何為無 Cookie
模式?
無 Cookie
模式:特指不支持 Cookie
功能的終端,通俗來講就是我們常說的 —— 前后臺(tái)分離模式。
- 后端將
token
返回到前端
首先調(diào)用 StpUtil.login(id)
進(jìn)行登錄。
調(diào)用 StpUtil.getTokenInfo()
返回當(dāng)前會(huì)話的 token
詳細(xì)參數(shù)。
- 此方法返回一個(gè)對(duì)象,其有兩個(gè)關(guān)鍵屬性:
tokenName
和tokenValue
(token 的名稱和 token 的值)。 - 將此對(duì)象傳遞到前臺(tái),讓前端人員將這兩個(gè)值保存到本地。
- 前端將
token
提交到后端
- 無論是app還是小程序,其傳遞方式都大同小異。
- 那就是,將 token 塞到請(qǐng)求
header
里 ,格式為:{tokenName: tokenValue}
。
- 只要按照如此方法將
token
值傳遞到后端,Sa-Token 就能像傳統(tǒng)PC端一樣自動(dòng)讀取到 token 值,進(jìn)行鑒權(quán)。 - 你可能會(huì)有疑問,難道我每個(gè)
ajax
都要寫這么一坨?豈不是麻煩死了?
你當(dāng)然不能每個(gè) ajax 都寫這么一坨,因?yàn)檫@種重復(fù)性代碼都是要封裝在一個(gè)函數(shù)里統(tǒng)一調(diào)用的。
5. Sa-Token 集成 Redis
Sa-Token 默認(rèn)將數(shù)據(jù)保存在內(nèi)存中,此模式讀寫速度最快,且避免了序列化與反序列化帶來的性能消耗,但是此模式也有一些缺點(diǎn),比如:
- 重啟后數(shù)據(jù)會(huì)丟失。
- 無法在分布式環(huán)境中共享數(shù)據(jù)。
為此,Sa-Token 提供了擴(kuò)展接口,你可以輕松將會(huì)話數(shù)據(jù)存儲(chǔ)在 Redis
、Memcached
等專業(yè)的緩存中間件中, 做到重啟數(shù)據(jù)不丟失,而且保證分布式環(huán)境下多節(jié)點(diǎn)的會(huì)話一致性。
以下是官方提供的 Redis 集成包:
<!-- Sa-Token 整合 Redis (使用 jackson 序列化方式) -->
<dependency><groupId>cn.dev33</groupId><artifactId>sa-token-dao-redis-jackson</artifactId><version>1.34.0</version>
</dependency>
集成 Redis 請(qǐng)注意:
- 無論使用哪種序列化方式,你都必須為項(xiàng)目提供一個(gè) Redis 實(shí)例化方案,例如:
<!-- 提供Redis連接池 -->
<dependency><groupId>org.apache.commons</groupId><artifactId>commons-pool2</artifactId>
</dependency>
- 配置Redis 連接信息
spring: # redis配置 redis:# Redis數(shù)據(jù)庫索引(默認(rèn)為0)database: 0# Redis服務(wù)器地址host: 127.0.0.1# Redis服務(wù)器連接端口port: 6379# Redis服務(wù)器連接密碼(默認(rèn)為空)# password: # 連接超時(shí)時(shí)間timeout: 10slettuce:pool:# 連接池最大連接數(shù)max-active: 200# 連接池最大阻塞等待時(shí)間(使用負(fù)值表示沒有限制)max-wait: -1ms# 連接池中的最大空閑連接max-idle: 10# 連接池中的最小空閑連接min-idle: 0
-
集成
Redis
后,是我額外手動(dòng)保存數(shù)據(jù),還是框架自動(dòng)保存?
框架自動(dòng)保存。集成Redis
只需要引入對(duì)應(yīng)的pom
依賴 即可,框架所有上層 API 保持不變。 -
集成包版本問題
Sa-Token-Redis
集成包的版本盡量與Sa-Token-Starter
集成包的版本一致,否則可能出現(xiàn)兼容性問題。
6. SpringBoot 集成 Sa-Token
以最新版本 1.34.0為例
6.1 創(chuàng)建項(xiàng)目
在 IDE 中新建一個(gè) SpringBoot 項(xiàng)目,例如:sa-token-demo-springboot
6.2 添加依賴
在項(xiàng)目中添加依賴
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Sa-Token 權(quán)限認(rèn)證,在線文檔:https://sa-token.cc -->
<dependency><groupId>cn.dev33</groupId><artifactId>sa-token-spring-boot-starter</artifactId><version>1.34.0</version>
</dependency>
<!-- Sa-Token 整合 Redis (使用 jackson 序列化方式) -->
<dependency><groupId>cn.dev33</groupId><artifactId>sa-token-dao-redis-jackson</artifactId><version>1.34.0</version>
</dependency>
<!-- 提供Redis連接池 -->
<dependency><groupId>org.apache.commons</groupId><artifactId>commons-pool2</artifactId>
</dependency>
<dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId>
</dependency>
6.3 設(shè)置配置文件
server:# 端口port: 8081############## Sa-Token 配置 (文檔: https://sa-token.cc) ##############
sa-token:# token名稱 (同時(shí)也是cookie名稱)token-name: satoken# token有效期,單位s 默認(rèn)30天, -1代表永不過期timeout: 2592000# token臨時(shí)有效期 (指定時(shí)間內(nèi)無操作就視為token過期) 單位: 秒activity-timeout: -1# 是否允許同一賬號(hào)并發(fā)登錄 (為true時(shí)允許一起登錄, 為false時(shí)新登錄擠掉舊登錄)is-concurrent: true# 在多人登錄同一賬號(hào)時(shí),是否共用一個(gè)token (為true時(shí)所有登錄共用一個(gè)token, 為false時(shí)每次登錄新建一個(gè)token)is-share: true# token風(fēng)格token-style: uuid# 是否輸出操作日志is-log: falsespring:# redis配置redis:# Redis數(shù)據(jù)庫索引(默認(rèn)為0)database: 0# Redis服務(wù)器地址host: 127.0.0.1# Redis服務(wù)器連接端口port: 6379# Redis服務(wù)器連接密碼(默認(rèn)為空)password: abc123# 連接超時(shí)時(shí)間timeout: 10slettuce:pool:# 連接池最大連接數(shù)max-active: 200# 連接池最大阻塞等待時(shí)間(使用負(fù)值表示沒有限制)max-wait: -1ms# 連接池中的最大空閑連接max-idle: 10# 連接池中的最小空閑連接min-idle: 0
6.4 創(chuàng)建啟動(dòng)類
@SpringBootApplication
public class SaTokenDemoApplication {public static void main(String[] args) throws JsonProcessingException {SpringApplication.run(SaTokenDemoApplication.class, args);System.out.println("啟動(dòng)成功:Sa-Token配置如下:" + SaManager.getConfig());}
}
6.5 定義用戶信息類
@Data
@Accessors(chain = true)
@AllArgsConstructor
public class UserInfo {private Long userId;private String name;private String email;
}
6.6 自定義權(quán)限驗(yàn)證接口擴(kuò)展
@Component // 保證此類被SpringBoot掃描,完成Sa-Token的自定義權(quán)限驗(yàn)證擴(kuò)展
public class StpInterfaceImpl implements StpInterface {/*** 返回一個(gè)賬號(hào)所擁有的權(quán)限碼集合*/@Overridepublic List<String> getPermissionList(Object loginId, String loginType) {// 本list僅做模擬,實(shí)際項(xiàng)目中要根據(jù)具體業(yè)務(wù)邏輯來查詢權(quán)限
// List<String> list = new ArrayList<String>();
// list.add("user.add");
// list.add("user.get");// list.add("user.delete");
// list.add("art.*");//從redis中獲取權(quán)限List<String> authList = (List<String>) StpUtil.getSession().get("authList");return authList;}/*** 返回一個(gè)賬號(hào)所擁有的角色標(biāo)識(shí)集合 (權(quán)限與角色可分開校驗(yàn))* @param loginId 賬號(hào)id,即你在調(diào)用 StpUtil.login(id) 時(shí)寫入的標(biāo)識(shí)值* @param loginType 賬號(hào)體系標(biāo)識(shí)* @author: yh* @date: 2023/2/12*/@Overridepublic List<String> getRoleList(Object loginId, String loginType) {// 本list僅做模擬,實(shí)際項(xiàng)目中要根據(jù)具體業(yè)務(wù)邏輯來查詢角色List<String> list = new ArrayList<String>();list.add("admin");list.add("super-admin");return list;}
}
6.7 創(chuàng)建測試Controller
@RestController
@RequestMapping("/user")
public class UserController {/*** 測試登錄,瀏覽器訪問: http://localhost:8081/user/doLogin?username=zhang&password=123456*/@RequestMapping("/doLogin")public SaResult doLogin(String username, String password) {// 此處僅作模擬示例,真實(shí)項(xiàng)目需要從數(shù)據(jù)庫中查詢數(shù)據(jù)進(jìn)行比對(duì) if("zhang".equals(username) && "123456".equals(password)) {// 第1步,先登錄上StpUtil.login(10001);}//第2步,加載用戶信息和權(quán)限信息StpUtil.getSession().set("loginInfo", new UserInfo(10001L, "張三", "123123@163.com"));//加載權(quán)限,只給user.add的權(quán)限List<String> authList = new ArrayList<String>();authList.add("user.add");StpUtil.getSession().set("authList", authList);// 第3步,獲取 Token 相關(guān)參數(shù)SaTokenInfo tokenInfo = StpUtil.getTokenInfo();// 第4步,返回給前端return SaResult.data(tokenInfo);}/*** 查詢登錄狀態(tài)*/@RequestMapping("/isLogin")public String isLogin() {return "當(dāng)前會(huì)話是否登錄:" + StpUtil.isLogin();}/*** 獲取用戶信息*/@RequestMapping("/getUserInfo")public UserInfo getUserInfo() {UserInfo loginInfo = (UserInfo) StpUtil.getSession().get("loginInfo");return loginInfo;}/*** 測試方法校驗(yàn)權(quán)限*/@GetMapping(value = "/add")public String add(){StpUtil.checkPermission("user.add");return "ok";}@GetMapping(value = "/update")public String update(){StpUtil.checkPermission("user.update");return "ok";}
}
6.8 運(yùn)行
啟動(dòng)項(xiàng)目,Copy Configuration
再啟動(dòng)一個(gè)實(shí)例,這時(shí)候我們就同時(shí)啟動(dòng)了兩個(gè)實(shí)例
這樣就可以測試出在集群部署情況下登錄信息會(huì)不會(huì)有問題,然后用接口測試工具依次訪問上述測試接口
1、 登錄
http://localhost:8081/user/doLogin?username=zhang&password=123456
此時(shí)查看redis
中數(shù)據(jù),可以看到登錄信息和權(quán)限都保存在redis中了
2、 查詢登錄狀態(tài)
http://localhost:8081/user/isLogin
注意header
里沒有cookie
把登錄接口返回的tokenName
和tokenValue
加入到請(qǐng)求header
中
只有請(qǐng)求攜帶對(duì)應(yīng)的token
,登錄狀態(tài)才為:true
3、 獲取用戶信息
這里調(diào)用端口為8082
的實(shí)例
http://localhost:8082/user/getUserInfo
4、 調(diào)用添加用戶接口
測試是否有添加接口權(quán)限,登錄的時(shí)候我們賦予了添加用戶的權(quán)限
http://localhost:8082/user/add
5、 調(diào)用更新用接口
測試是否有更新用戶接口權(quán)限,登錄的時(shí)候我們沒有賦予更新用戶的權(quán)限
http://localhost:8082/user/update
結(jié)果可以看到?jīng)]有更新用戶接口的權(quán)限。
我的博客即將同步至騰訊云開發(fā)者社區(qū),邀請(qǐng)大家一同入駐:https://cloud.tencent.com/developer/support-plan?invite_code=rrez71s8jyqy