Sa-Token 是一個輕量級 Java 權限認證框架,主要解決:登錄認證、權限認證、單點登錄、OAuth2.0、分布式Session會話、微服務網關鑒權 等一系列權限相關問題。
Sa-Token 旨在以簡單、優雅的方式完成系統的權限認證部分,以登錄認證為例,你只需要:
// 會話登錄,參數填登錄人的賬號id StpUtil.login(10001);
無需實現任何接口,無需創建任何配置文件,只需要這一句靜態代碼的調用,便可以完成會話登錄認證。
如果一個接口需要登錄后才能訪問,我們只需調用以下代碼:
// 校驗當前客戶端是否已經登錄,如果未登錄則拋出 `NotLoginException` 異常StpUtil.checkLogin();
在 Sa-Token 中,大多數功能都可以一行代碼解決:
踢人下線:
// 將賬號id為 10077 的會話踢下線 StpUtil.kickout(10077);
權限認證:
// 注解鑒權:只有具備 `user:add` 權限的會話才可以進入方法@SaCheckPermission("user:add") public String insert(SysUser user) { // ... return "用戶增加";}
路由攔截鑒權:
// 根據路由劃分模塊,不同模塊不同鑒權 registry.addInterceptor(new SaInterceptor(handler -> { SaRouter.match("/user/**", r -> StpUtil.checkPermission("user")); SaRouter.match("/admin/**", r -> StpUtil.checkPermission("admin")); SaRouter.match("/goods/**", r -> StpUtil.checkPermission("goods")); SaRouter.match("/orders/**", r -> StpUtil.checkPermission("orders")); SaRouter.match("/notice/**", r -> StpUtil.checkPermission("notice")); // 更多模塊... })).addPathPatterns("/**");
當你受夠 Shiro、SpringSecurity 等框架的三拜九叩之后,你就會明白,相對于這些傳統老牌框架,Sa-Token 的 API 設計是多么的簡單、優雅!
Sa-Token 目前主要五大功能模塊:登錄認證、權限認證、單點登錄、OAuth2.0、微服務鑒權。
<!-- Sa-Token 權限認證,在線文檔:https://sa-token.cc --><dependency> <groupId>cn.dev33</groupId> <artifactId>sa-token-spring-boot-starter</artifactId> <version>1.34.0</version></dependency>
注:如果你使用的 SpringBoot 3.x,只需要將 sa-token-spring-boot-starter 修改為 sa-token-spring-boot3-starter 即可。
你可以零配置啟動項目 ,但同時你也可以在 application.yml 中增加如下配置,定制性使用框架:
server: # 端口 port: 8081############## Sa-Token 配置 (文檔: https://sa-token.cc) ##############sa-token: # token名稱 (同時也是cookie名稱) token-name: satoken # token有效期,單位s 默認30天, -1代表永不過期 timeout: 2592000 # token臨時有效期 (指定時間內無操作就視為token過期) 單位: 秒 activity-timeout: -1 # 是否允許同一賬號并發登錄 (為true時允許一起登錄, 為false時新登錄擠掉舊登錄) is-concurrent: true # 在多人登錄同一賬號時,是否共用一個token (為true時所有登錄共用一個token, 為false時每次登錄新建一個token) is-share: true # token風格 token-style: uuid # 是否輸出操作日志 is-log: false
@RestController @RequestMapping("/user/") public class UserController { // 測試登錄,瀏覽器訪問:http://localhost:8081/user/doLogin?username=zhang&password=123456 @RequestMapping("doLogin") public String doLogin(String username, String password) { // 此處僅作模擬示例,真實項目需要從數據庫中查詢數據進行比對 if("iron".equals(username) && "123456".equals(password)) { StpUtil.login(10001); return "登錄成功"; } return "登錄失敗"; } // 查詢登錄狀態,瀏覽器訪問:http://localhost:8081/user/isLogin @RequestMapping("isLogin") public String isLogin() { return "當前會話是否登錄:" + StpUtil.isLogin(); } }
對于一些登錄之后才能訪問的接口(例如:查詢我的賬號資料),我們通常的做法是增加一層接口校驗:
那么,判斷會話是否登錄的依據是什么?我們先來簡單分析一下登錄訪問流程:
所謂登錄認證,指的就是服務器校驗賬號密碼,為用戶頒發 Token 會話憑證的過程,這個 Token 也是我們后續判斷會話是否登錄的關鍵所在。
根據以上思路,我們需要一個會話登錄的函數:
// 會話登錄:參數填寫要登錄的賬號id,建議的數據類型:long | int | String, 不可以傳入復雜類型,如:User、Admin 等等StpUtil.login(Object id);
只此一句代碼,便可以使會話登錄成功,實際上,Sa-Token 在背后做了大量的工作,包括但不限于:
你暫時不需要完整的了解整個登錄過程,你只需要記住關鍵一點:Sa-Token 為這個賬號創建了一個Token憑證,且通過 Cookie 上下文返回給了前端。
所以一般情況下,我們的登錄接口代碼,會大致類似如下:
// 會話登錄接口 @RequestMapping("doLogin") public SaResult doLogin(String name, String pwd) { // 第一步:比對前端提交的賬號名稱、密碼 if("iron".equals(name) && "123456".equals(pwd)) { // 第二步:根據賬號id,進行登錄 StpUtil.login(10001); return SaResult.ok("登錄成功"); } return SaResult.error("登錄失敗");}
如果你對以上代碼閱讀沒有壓力,你可能會注意到略顯奇怪的一點:此處僅僅做了會話登錄,但并沒有主動向前端返回 Token 信息。是因為不需要嗎?嚴格來講是需要的,只不過 StpUtil.login(id) 方法利用了 Cookie 自動注入的特性,省略了你手寫返回 Token 的代碼。
如果你對 Cookie 功能還不太了解,也不用擔心,我們會在之后的 [ 前后端分離 ] 章節中詳細的闡述 Cookie 功能,現在你只需要了解最基本的兩點:
因此,在 Cookie 功能的加持下,我們可以僅靠 StpUtil.login(id) 一句代碼就完成登錄認證。
除了登錄方法,我們還需要:
// 當前會話注銷登錄StpUtil.logout();// 獲取當前會話是否已經登錄,返回true=已登錄,false=未登錄StpUtil.isLogin();// 檢驗當前會話是否已經登錄, 如果未登錄,則拋出異常:`NotLoginException`StpUtil.checkLogin();
異常 NotLoginException 代表當前會話暫未登錄,可能的原因有很多:前端沒有提交 Token、前端提交的 Token 是無效的、前端提交的 Token 已經過期 …… 等等,可參照此篇:未登錄場景值,了解如何獲取未登錄的場景值。
// 獲取當前會話賬號id, 如果未登錄,則拋出異常:`NotLoginException`StpUtil.getLoginId();// 類似查詢API還有:StpUtil.getLoginIdAsString(); // 獲取當前會話賬號id, 并轉化為`String`類型StpUtil.getLoginIdAsInt(); // 獲取當前會話賬號id, 并轉化為`int`類型StpUtil.getLoginIdAsLong(); // 獲取當前會話賬號id, 并轉化為`long`類型// ---------- 指定未登錄情形下返回的默認值 ----------// 獲取當前會話賬號id, 如果未登錄,則返回null StpUtil.getLoginIdDefaultNull();// 獲取當前會話賬號id, 如果未登錄,則返回默認值 (`defaultValue`可以為任意類型)StpUtil.getLoginId(T defaultValue);
// 獲取當前會話的token值StpUtil.getTokenValue();// 獲取當前`StpLogic`的token名稱StpUtil.getTokenName();// 獲取指定token對應的賬號id,如果未登錄,則返回 nullStpUtil.getLoginIdByToken(String tokenValue);// 獲取當前會話剩余有效期(單位:s,返回-1代表永久有效)StpUtil.getTokenTimeout();// 獲取當前會話的token信息參數StpUtil.getTokenInfo();
有關TokenInfo參數詳解,請參考:TokenInfo參數詳解
新建 LoginController,復制以下代碼
/** * 登錄測試 */@RestController@RequestMapping("/acc/")public class LoginController { // 測試登錄 ---- http://localhost:8081/acc/doLogin?name=zhang&pwd=123456 @RequestMapping("doLogin") public SaResult doLogin(String name, String pwd) { // 此處僅作模擬示例,真實項目需要從數據庫中查詢數據進行比對 if("iron".equals(name) && "123456".equals(pwd)) { StpUtil.login(10001); return SaResult.ok("登錄成功"); } return SaResult.error("登錄失敗"); } // 查詢登錄狀態 ---- http://localhost:8081/acc/isLogin @RequestMapping("isLogin") public SaResult isLogin() { return SaResult.ok("是否登錄:" + StpUtil.isLogin()); } // 查詢 Token 信息 ---- http://localhost:8081/acc/tokenInfo @RequestMapping("tokenInfo") public SaResult tokenInfo() { return SaResult.data(StpUtil.getTokenInfo()); } // 測試注銷 ---- http://localhost:8081/acc/logout @RequestMapping("logout") public SaResult logout() { StpUtil.logout(); return SaResult.ok(); }}
所謂權限認證,核心邏輯就是判斷一個賬號是否擁有指定權限:
深入到底層數據中,就是每個賬號都會擁有一個權限碼集合,框架來校驗這個集合中是否包含指定的權限碼。
例如:當前賬號擁有權限碼集合 ["user-add", "user-delete", "user-get"],這時候我來校驗權限 "user-update",則其結果就是:驗證失敗,禁止訪問。
所以現在問題的核心就是:
因為每個項目的需求不同,其權限設計也千變萬化,因此 [ 獲取當前賬號權限碼集合 ] 這一操作不可能內置到框架中, 所以 Sa-Token 將此操作以接口的方式暴露給你,以方便你根據自己的業務邏輯進行重寫。
你需要做的就是新建一個類,實現 StpInterface接口,例如以下代碼:
/** * 自定義權限驗證接口擴展 */@Component // 保證此類被SpringBoot掃描,完成Sa-Token的自定義權限驗證擴展 public class StpInterfaceImpl implements StpInterface { /** * 返回一個賬號所擁有的權限碼集合 */ @Override public List<String> getPermissionList(Object loginId, String loginType) { // 本list僅做模擬,實際項目中要根據具體業務邏輯來查詢權限 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; } /** * 返回一個賬號所擁有的角色標識集合 (權限與角色可分開校驗) */ @Override public List<String> getRoleList(Object loginId, String loginType) { // 本list僅做模擬,實際項目中要根據具體業務邏輯來查詢角色 List<String> list = new ArrayList<String>(); list.add("admin"); list.add("super-admin"); return list; }}
參數解釋:
可參考代碼:碼云:StpInterfaceImpl.java
注意: StpInterface 接口在需要鑒權時由框架自動調用,開發者只需要配置好就可以使用下面的鑒權方法或后面的注解鑒權
然后就可以用以下api來鑒權了
// 獲取:當前賬號所擁有的權限集合StpUtil.getPermissionList();// 判斷:當前賬號是否含有指定權限, 返回 true 或 falseStpUtil.hasPermission("user.add"); // 校驗:當前賬號是否含有指定權限, 如果驗證未通過,則拋出異常: NotPermissionException StpUtil.checkPermission("user.add"); // 校驗:當前賬號是否含有指定權限 [指定多個,必須全部驗證通過]StpUtil.checkPermissionAnd("user.add", "user.delete", "user.get"); // 校驗:當前賬號是否含有指定權限 [指定多個,只要其一驗證通過即可]StpUtil.checkPermissionOr("user.add", "user.delete", "user.get");
擴展:NotPermissionException 對象可通過 getLoginType() 方法獲取具體是哪個 StpLogic 拋出的異常
在Sa-Token中,角色和權限可以獨立驗證
// 獲取:當前賬號所擁有的角色集合StpUtil.getRoleList();// 判斷:當前賬號是否擁有指定角色, 返回 true 或 falseStpUtil.hasRole("super-admin"); // 校驗:當前賬號是否含有指定角色標識, 如果驗證未通過,則拋出異常: NotRoleExceptionStpUtil.checkRole("super-admin"); // 校驗:當前賬號是否含有指定角色標識 [指定多個,必須全部驗證通過]StpUtil.checkRoleAnd("super-admin", "shop-admin"); // 校驗:當前賬號是否含有指定角色標識 [指定多個,只要其一驗證通過即可] StpUtil.checkRoleOr("super-admin", "shop-admin");
擴展:NotRoleException 對象可通過 getLoginType() 方法獲取具體是哪個 StpLogic 拋出的異常
Sa-Token允許你根據通配符指定泛權限,例如當一個賬號擁有art.*的權限時,art.add、art.delete、art.update都將匹配通過
// 當擁有 art.* 權限時StpUtil.hasPermission("art.add"); // trueStpUtil.hasPermission("art.update"); // trueStpUtil.hasPermission("goods.add"); // false// 當擁有 *.delete 權限時StpUtil.hasPermission("art.delete"); // trueStpUtil.hasPermission("user.delete"); // trueStpUtil.hasPermission("user.update"); // false// 當擁有 *.js 權限時StpUtil.hasPermission("index.js"); // trueStpUtil.hasPermission("index.css"); // falseStpUtil.hasPermission("index.html"); // false
上帝權限:當一個賬號擁有 "*" 權限時,他可以驗證通過任何權限碼 (角色認證同理)
權限精確到按鈕級的意思就是指:權限范圍可以控制到頁面上的每一個按鈕是否顯示。
思路:如此精確的范圍控制只依賴后端已經難以完成,此時需要前端進行一定的邏輯判斷。
如果是前后端一體項目,可以參考:Thymeleaf 標簽方言,如果是前后端分離項目,則:
<button v-if="arr.indexOf('user.delete') > -1">刪除按鈕</button>
其中:arr是當前用戶擁有的權限碼數組,user.delete是顯示按鈕需要擁有的權限碼,刪除按鈕是用戶擁有權限碼才可以看到的內容。
注意:以上寫法只為提供一個參考示例,不同框架有不同寫法,大家可根據項目技術棧靈活封裝進行調用。
所謂踢人下線,核心操作就是找到指定 loginId 對應的 Token,并設置其失效。
StpUtil.logout(10001); // 強制指定賬號注銷下線 StpUtil.logout(10001, "PC"); // 強制指定賬號指定端注銷下線 StpUtil.logoutByTokenValue("token"); // 強制指定 Token 注銷下線
StpUtil.kickout(10001); // 將指定賬號踢下線 StpUtil.kickout(10001, "PC"); // 將指定賬號指定端踢下線StpUtil.kickoutByTokenValue("token"); // 將指定 Token 踢下線
強制注銷 和 踢人下線 的區別在于:
有同學表示:盡管使用代碼鑒權非常方便,但是我仍希望把鑒權邏輯和業務邏輯分離開來,我可以使用注解鑒權嗎?當然可以!
注解鑒權 —— 優雅的將鑒權與業務代碼分離!
Sa-Token 使用全局攔截器完成注解鑒權功能,為了不為項目帶來不必要的性能負擔,攔截器默認處于關閉狀態因此,為了使用注解鑒權,你必須手動將 Sa-Token 的全局攔截器注冊到你項目中
以SpringBoot2.0為例,新建配置類SaTokenConfigure.java
@Configurationpublic class SaTokenConfigure implements WebMvcConfigurer { // 注冊 Sa-Token 攔截器,打開注解式鑒權功能 @Override public void addInterceptors(InterceptorRegistry registry) { // 注冊 Sa-Token 攔截器,打開注解式鑒權功能 registry.addInterceptor(new SaInterceptor()).addPathPatterns("/**"); }}
保證此類被springboot啟動類掃描到即可
然后我們就可以愉快的使用注解鑒權了:
// 登錄校驗:只有登錄之后才能進入該方法 @SaCheckLogin @RequestMapping("info") public String info() { return "查詢用戶信息";}// 角色校驗:必須具有指定角色才能進入該方法 @SaCheckRole("super-admin") @RequestMapping("add") public String add() { return "用戶增加";}// 權限校驗:必須具有指定權限才能進入該方法 @SaCheckPermission("user-add") @RequestMapping("add") public String add() { return "用戶增加";}// 二級認證校驗:必須二級認證之后才能進入該方法 @SaCheckSafe() @RequestMapping("add") public String add() { return "用戶增加";}// Http Basic 校驗:只有通過 Basic 認證后才能進入該方法 @SaCheckBasic(account = "sa:123456") @RequestMapping("add") public String add() { return "用戶增加";}// 校驗當前賬號是否被封禁 comment 服務,如果已被封禁會拋出異常,無法進入方法 @SaCheckDisable("comment") @RequestMapping("send") public String send() { return "查詢用戶信息";}
注:以上注解都可以加在類上,代表為這個類所有方法進行鑒權
@SaCheckRole與@SaCheckPermission注解可設置校驗模式,例如:
// 注解式鑒權:只要具有其中一個權限即可通過校驗 @RequestMapping("atJurOr")@SaCheckPermission(value = {"user-add", "user-all", "user-delete"}, mode = SaMode.OR) public SaResult atJurOr() { return SaResult.data("用戶信息");}
mode有兩種取值:
假設有以下業務場景:一個接口在具有權限 user.add 或角色 admin 時可以調通。怎么寫?
// 角色權限雙重 “or校驗”:具備指定權限或者指定角色即可通過校驗@RequestMapping("userAdd")@SaCheckPermission(value = "user.add", orRole = "admin") public SaResult userAdd() { return SaResult.data("用戶信息");}
orRole 字段代表權限認證未通過時的次要選擇,兩者只要其一認證成功即可通過校驗,其有三種寫法:
使用 @SaIgnore 可表示一個接口忽略認證:
@SaCheckLogin@RestControllerpublic class TestController { // ... 其它方法 // 此接口加上了 @SaIgnore 可以游客訪問 @SaIgnore @RequestMapping("getList") public SaResult getList() { // ... return SaResult.ok(); }}
如上代碼表示:TestController 中的所有方法都需要登錄后才可以訪問,但是 getList 接口可以匿名游客訪問。
疑問:我能否將注解寫在其它架構層呢,比如業務邏輯層?
使用攔截器模式,只能在Controller層進行注解鑒權,如需在任意層級使用注解鑒權,請參考:AOP注解鑒權
sa-token: # token前綴 token-prefix: Bearer
Sa-Token默認的token生成策略是uuid風格,其模樣類似于:623368f0-ae5e-4475-a53f-93e4225f16ae。如果你對這種風格不太感冒,還可以將token生成設置為其他風格。
怎么設置呢?只需要在yml配置文件里設置 sa-token.token-style=風格類型 即可,其有多種取值:
// 1. token-style=uuid —— uuid風格 (默認風格)"623368f0-ae5e-4475-a53f-93e4225f16ae"http:// 2. token-style=simple-uuid —— 同上,uuid風格, 只不過去掉了中劃線"6fd4221395024b5f87edd34bc3258ee8"http:// 3. token-style=random-32 —— 隨機32位字符串"qEjyPsEA1Bkc9dr8YP6okFr5umCZNR6W"http:// 4. token-style=random-64 —— 隨機64位字符串"v4ueNLEpPwMtmOPMBtOOeIQsvP8z9gkMgIVibTUVjkrNrlfra5CGwQkViDjO8jcc"http:// 5. token-style=random-128 —— 隨機128位字符串"nojYPmcEtrFEaN0Otpssa8I8jpk8FO53UcMZkCP9qyoHaDbKS6dxoRPky9c6QlftQ0pdzxRGXsKZmUSrPeZBOD6kJFfmfgiRyUmYWcj4WU4SSP2ilakWN1HYnIuX0Olj"http:// 6. token-style=tik —— tik風格"gr_SwoIN0MC1ewxHX_vfCW3BothWDZMMtx__"
如果你覺著以上風格都不是你喜歡的類型,那么你還可以自定義token生成策略,來定制化token生成風格。
怎么做呢?只需要重寫 SaStrategy 策略類的 createToken 算法即可:
1、在SaTokenConfigure配置類中添加代碼:
@Configurationpublic class SaTokenConfigure { /** * 重寫 Sa-Token 框架內部算法策略 */ @Autowired public void rewriteSaStrategy() { // 重寫 Token 生成策略 SaStrategy.me.createToken = (loginId, loginType) -> { return SaFoxUtil.getRandomString(60); // 隨機60位長度字符串 }; }}
2、再次調用 StpUtil.login(10001)方法進行登錄,觀察其生成的token樣式:
fuPSwZsnUhwgz08GTCH4wOgasWtc3odP4HLwXJ7NDGOximTvT4OlW19zeLH
首先在項目已經引入 Sa-Token 的基礎上,繼續添加:
<!-- Sa-Token 整合 jwt --><dependency> <groupId>cn.dev33</groupId> <artifactId>sa-token-jwt</artifactId> <version>1.34.0</version></dependency>
注意: sa-token-jwt 顯式依賴 hutool-jwt 5.7.14 版本,意味著:你的項目中要么不引入 Hutool,要么引入版本 >= 5.7.14 的 Hutool 版本
在 application.yml 配置文件中配置 jwt 生成秘鑰:
sa-token: # jwt秘鑰 jwt-secret-key: asdasdasifhueuiwyurfewbfjsdafjk
根據不同的整合規則,插件提供了三種不同的模式,你需要 選擇其中一種 注入到你的項目中
@Configurationpublic class SaTokenConfigure { // Sa-Token 整合 jwt (Simple 簡單模式) @Bean public StpLogic getStpLogicJwt() { return new StpLogicJwtForSimple(); } // Sa-Token 整合 jwt (Mixin 混入模式) @Bean public StpLogic getStpLogicJwt() { return new StpLogicJwtForMixin(); } // Sa-Token 整合 jwt (Stateless 無狀態模式) @Bean public StpLogic getStpLogicJwt() { return new StpLogicJwtForStateless(); }}
注入不同模式會讓框架具有不同的行為策略,以下是三種模式的差異點(為方便敘述,以下比較以同時引入 jwt 與 Redis 作為前提):
功能點 | Simple 簡單模式 | Mixin 混入模式 | Stateless 無狀態模式 |
Token風格 | jwt風格 | jwt風格 | jwt風格 |
登錄數據存儲 | Redis中 | Token中 | Token中 |
Session存儲 | Redis中 | Redis中 | 無Session |
注銷下線 | 前后端雙清數據 | 前后端雙清數據 | 前端清除數據 |
踢人下線API | 支持 | 不支持 | 不支持 |
頂人下線API | 支持 | 不支持 | 不支持 |
登錄認證 | 支持 | 支持 | 支持 |
角色認證 | 支持 | 支持 | 支持 |
權限認證 | 支持 | 支持 | 支持 |
timeout 有效期 | 支持 | 支持 | 支持 |
activity-timeout 有效期 | 支持 | 支持 | 不支持 |
id反查Token | 支持 | 支持 | 不支持 |
會話管理 | 支持 | 部分支持 | 不支持 |
注解鑒權 | 支持 | 支持 | 支持 |
路由攔截鑒權 | 支持 | 支持 | 支持 |
賬號封禁 | 支持 | 支持 | 不支持 |
身份切換 | 支持 | 支持 | 支持 |
二級認證 | 支持 | 支持 | 支持 |
模式總結 | Token風格替換 | jwt 與 Redis 邏輯混合 | 完全舍棄Redis,只用jwt |
你可以通過以下方式在登錄時注入擴展參數:
// 登錄10001賬號,并為生成的 Token 追加擴展參數nameStpUtil.login(10001, SaLoginConfig.setExtra("name", "zhangsan"));// 連綴寫法追加多個StpUtil.login(10001, SaLoginConfig .setExtra("name", "zhangsan") .setExtra("age", 18) .setExtra("role", "超級管理員"));// 獲取擴展參數 String name = StpUtil.getExtra("name");// 獲取任意 Token 的擴展參數 String name = StpUtil.getExtra("tokenValue", "name");
sa-token-jwt 插件默認只為 StpUtil 注入 StpLogicJwtFoxXxx 實現,自定義的 StpUserUtil 是不會自動注入的,我們需要幫其手動注入:
/** * 為 StpUserUtil 注入 StpLogicJwt 實現 */@Autowiredpublic void setUserStpLogic() { StpUserUtil.setStpLogic(new StpLogicJwtForSimple(StpUserUtil.TYPE));}
如果需要自定義生成 token 的算法(例如更換sign方式),直接重寫 SaJwtTemplate 對象即可:
/** * 自定義 SaJwtUtil 生成 token 的算法 */@Autowiredpublic void setSaJwtTemplate() { SaJwtUtil.setSaJwtTemplate(new SaJwtTemplate() { @Override public String generateToken(JWT jwt, String keyt) { System.out.println("------ 自定義了 token 生成算法"); return super.generateToken(jwt, keyt); } });}
1、使用 jwt-simple 模式后,is-share=false 恒等于 false。
is-share=true 的意思是每次登錄都產生一樣的 token,這種策略和 [ 為每個 token 單獨設定 setExtra 數據 ] 不兼容的, 為保證正確設定 Extra 數據,當使用 jwt-simple 模式后,is-share 配置項 恒等于 false。
2、使用 jwt-mixin 模式后,is-concurrent 必須為 true。
is-concurrent=false 代表每次登錄都把舊登錄頂下線,但是 jwt-mixin 模式登錄的 token 并不會記錄在持久庫數據中, 技術上來講無法將其踢下線,所以此時頂人下線和踢人下線等 API 都屬于不可用狀態,所以此時 is-concurrent 配置項必須配置為 true。
Sa-Token 提供一種偵聽器機制,通過注冊偵聽器,你可以訂閱框架的一些關鍵性事件,例如:用戶登錄、退出、被踢下線等。
事件觸發流程大致如下:
框架默認內置了偵聽器 SaTokenListenerForLog 實現:代碼參考 ,功能是控制臺 log 打印輸出,你可以通過配置sa-token.is-log=true開啟。
要注冊自定義的偵聽器也非常簡單:
新建MySaTokenListener.java,實現SaTokenListener接口,并添加上注解@Component,保證此類被SpringBoot掃描到:
/** * 自定義偵聽器的實現 */@Componentpublic class MySaTokenListener implements SaTokenListener { /** 每次登錄時觸發 */ @Override public void doLogin(String loginType, Object loginId, String tokenValue, SaLoginModel loginModel) { System.out.println("---------- 自定義偵聽器實現 doLogin"); } /** 每次注銷時觸發 */ @Override public void doLogout(String loginType, Object loginId, String tokenValue) { System.out.println("---------- 自定義偵聽器實現 doLogout"); } /** 每次被踢下線時觸發 */ @Override public void doKickout(String loginType, Object loginId, String tokenValue) { System.out.println("---------- 自定義偵聽器實現 doKickout"); } /** 每次被頂下線時觸發 */ @Override public void doReplaced(String loginType, Object loginId, String tokenValue) { System.out.println("---------- 自定義偵聽器實現 doReplaced"); } /** 每次被封禁時觸發 */ @Override public void doDisable(String loginType, Object loginId, String service, int level, long disableTime) { System.out.println("---------- 自定義偵聽器實現 doDisable"); } /** 每次被解封時觸發 */ @Override public void doUntieDisable(String loginType, Object loginId, String service) { System.out.println("---------- 自定義偵聽器實現 doUntieDisable"); } /** 每次二級認證時觸發 */ @Override public void doOpenSafe(String loginType, String tokenValue, String service, long safeTime) { System.out.println("---------- 自定義偵聽器實現 doOpenSafe"); } /** 每次退出二級認證時觸發 */ @Override public void doCloseSafe(String loginType, String tokenValue, String service) { System.out.println("---------- 自定義偵聽器實現 doCloseSafe"); } /** 每次創建Session時觸發 */ @Override public void doCreateSession(String id) { System.out.println("---------- 自定義偵聽器實現 doCreateSession"); } /** 每次注銷Session時觸發 */ @Override public void doLogoutSession(String id) { System.out.println("---------- 自定義偵聽器實現 doLogoutSession"); } /** 每次Token續期時觸發 */ @Override public void doRenewTimeout(String tokenValue, Object loginId, long timeout) { System.out.println("---------- 自定義偵聽器實現 doRenewTimeout"); }}
以上代碼由于添加了 @Component 注解,會被 SpringBoot 掃描并自動注冊到事件中心,此時我們無需手動注冊。
如果我們沒有添加 @Component 注解或者項目屬于非 IOC 自動注入環境,則需要我們手動將這個偵聽器注冊到事件中心:
// 將偵聽器注冊到事件發布中心SaTokenEventCenter.registerListener(new MySaTokenListener());
事件中心的其它一些常用方法:
// 獲取已注冊的所有偵聽器 SaTokenEventCenter.getListenerList(); // 重置偵聽器集合 SaTokenEventCenter.setListenerList(listenerList); // 注冊一個偵聽器 SaTokenEventCenter.registerListener(listener); // 注冊一組偵聽器 SaTokenEventCenter.registerListenerList(listenerList); // 移除一個偵聽器 SaTokenEventCenter.removeListener(listener); // 移除指定類型的所有偵聽器 SaTokenEventCenter.removeListener(cls); // 清空所有已注冊的偵聽器 SaTokenEventCenter.clearListener(); // 判斷是否已經注冊了指定偵聽器 SaTokenEventCenter.hasListener(listener); // 判斷是否已經注冊了指定類型的偵聽器 SaTokenEventCenter.hasListener(cls);
在 TestController 中添加登錄測試代碼:
// 測試登錄接口 @RequestMapping("login")public SaResult login() { System.out.println("登錄前"); StpUtil.login(10001); System.out.println("登錄后"); return SaResult.ok();}
@Componentpublic class MySaTokenListener extends SaTokenListenerForSimple { /* * SaTokenListenerForSimple 對所有事件提供了空實現,通過繼承此類,你只需重寫一部分方法即可實現一個可用的偵聽器。 */ /** 每次登錄時觸發 */ @Override public void doLogin(String loginType, Object loginId, String tokenValue, SaLoginModel loginModel) { System.out.println("---------- 自定義偵聽器實現 doLogin"); }}3.2、使用匿名內部類的方式注冊:// 登錄時觸發 SaTokenEventCenter.registerListener(new SaTokenListenerForSimple() { @Override public void doLogin(String loginType, Object loginId, String tokenValue, SaLoginModel loginModel) { System.out.println("---------------- doLogin"); }});
如果你認為你的事件處理代碼是不安全的(代碼可能在運行時拋出異常),則需要使用 try-catch 包裹代碼,以防因為拋出異常導致 Sa-Token 的整個登錄流程被強制中斷。
// 登錄時觸發 SaTokenEventCenter.registerListener(new SaTokenListenerForSimple() { @Override public void doLogin(String loginType, Object loginId, String tokenValue, SaLoginModel loginModel) { try { // 不安全代碼需要寫在 try-catch 里 // ...... } catch (Exception e) { e.printStackTrace(); } }});
可以,多個偵聽器間彼此獨立,互不影響,按照注冊順序依次接受到事件通知。
功能點 | SSO單點登錄 | OAuth2.0 |
統一認證 | 支持度高 | 支持度高 |
統一注銷 | 支持度高 | 支持度低 |
多個系統會話一致性 | 強一致 | 弱一致 |
第三方應用授權管理 | 不支持 | 支持度高 |
自有系統授權管理 | 支持度高 | 支持度低 |
Client級的權限校驗 | 不支持 | 支持度高 |
集成簡易度 | 比較簡單 | 難度中等 |
注:以上僅為在 Sa-Token 中兩種技術的差異度比較,不同框架的實現可能略有差異,但整體思想是一致的。
舉個場景,假設我們的系統被切割為N個部分:商城、論壇、直播、社交…… 如果用戶每訪問一個模塊都要登錄一次,那么用戶將會瘋掉, 為了優化用戶體驗,我們急需一套機制將這N個系統的認證授權互通共享,讓用戶在一個系統登錄之后,便可以暢通無阻的訪問其它所有系統。
單點登錄——就是為了解決這個問題而生!
簡而言之,單點登錄可以做到:在多個互相信任的系統中,用戶只需登錄一次,就可以訪問所有系統。
Sa-Token-SSO 由簡入難劃分為三種模式,解決不同架構下的 SSO 接入問題:
系統架構 | 采用模式 | 簡介 | 文檔鏈接 |
前端同域 + 后端同 Redis | 模式一 | 共享 Cookie 同步會話 | 文檔 、示例 |
前端不同域 + 后端同 Redis | 模式二 | URL重定向傳播會話 | 文檔 、示例 |
前端不同域 + 后端不同 Redis | 模式三 | Http請求獲取會話 | 文檔 、示例 |
<!-- Sa-Token 權限認證,在線文檔:https://sa-token.cc --><dependency> <groupId>cn.dev33</groupId> <artifactId>sa-token-spring-boot-starter</artifactId> <version>1.34.0</version></dependency><!-- Sa-Token 插件:整合SSO --><dependency> <groupId>cn.dev33</groupId> <artifactId>sa-token-sso</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><dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId></dependency><!-- 視圖引擎(在前后端不分離模式下提供視圖支持) --><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId></dependency><!-- Http請求工具(在模式三的單點注銷功能下用到,如不需要可以注釋掉) --><dependency> <groupId>com.dtflys.forest</groupId> <artifactId>forest-spring-boot-starter</artifactId> <version>1.5.26</version></dependency>
除了 sa-token-spring-boot-starter 和 sa-token-sso 以外,其它包都是可選的:
建議先完整測試三種模式之后再對pom依賴進行酌情刪減。
/** * Sa-Token-SSO Server端 Controller */@RestControllerpublic class SsoServerController { /* * SSO-Server端:處理所有SSO相關請求 (下面的章節我們會詳細列出開放的接口) */ @RequestMapping("/sso/*") public Object ssoRequest() { return SaSsoProcessor.instance.serverDister(); } /** * 配置SSO相關參數 */ @Autowired private void configSso(SaSsoConfig sso) { // 配置:未登錄時返回的View sso.setNotLoginView(() -> { String msg = "當前會話在SSO-Server端尚未登錄,請先訪問" + "<a href='/sso/doLogin?name=sa&pwd=123456' target='_blank'> doLogin登錄 </a>" + "進行登錄之后,刷新頁面開始授權"; return msg; }); // 配置:登錄處理函數 sso.setDoLoginHandle((name, pwd) -> { // 此處僅做模擬登錄,真實環境應該查詢數據進行登錄 if("sa".equals(name) && "123456".equals(pwd)) { StpUtil.login(10001); return SaResult.ok("登錄成功!").setData(StpUtil.getTokenValue()); } return SaResult.error("登錄失敗!"); }); // 配置 Http 請求處理器 (在模式三的單點注銷功能下用到,如不需要可以注釋掉) sso.setSendHttp(url -> { try { // 發起 http 請求 System.out.println("------ 發起請求:" + url); return Forest.get(url).executeAsString(); } catch (Exception e) { e.printStackTrace(); return null; } }); }}
# 端口server: port: 9000# Sa-Token 配置sa-token: # ------- SSO-模式一相關配置 (非模式一不需要配置) # cookie: # 配置 Cookie 作用域 # domain: stp.com # ------- SSO-模式二相關配置 sso: # Ticket有效期 (單位: 秒),默認五分鐘 ticket-timeout: 300 # 所有允許的授權回調地址 allow-url: "*" # 是否打開單點注銷功能 is-slo: true # ------- SSO-模式三相關配置 (下面的配置在SSO模式三并且 is-slo=true 時打開) # 是否打開模式三 isHttp: true # 接口調用秘鑰(用于SSO模式三的單點注銷功能) secretkey: kQwIOrYvnXmSDkwEiFngrKidMcdrgKor # ---- 除了以上配置項,你還需要為 Sa-Token 配置http請求處理器(文檔有步驟說明) spring: # Redis配置 (SSO模式一和模式二使用Redis來同步會話) redis: # Redis數據庫索引(默認為0) database: 1 # Redis服務器地址 host: 127.0.0.1 # Redis服務器連接端口 port: 6379 # Redis服務器連接密碼(默認為空) password: forest: # 關閉 forest 請求日志打印 log-enabled: false
訪問統一授權地址:
可以看到這個頁面目前非常簡陋,這是因為我們以上的代碼示例,主要目標是為了帶大家從零搭建一個可用的SSO認證服務端,所以就對一些不太必要的步驟做了簡化。
本文鏈接:http://www.tebozhan.com/showinfo-26-35341-0.html再見,Shiro !你好,Sa-Token!
聲明:本網頁內容旨在傳播知識,若有侵權等問題請及時與本網聯系,我們將在第一時間刪除處理。郵件:2376512515@qq.com