從零搭建開發腳手架 集成認證授權 Sa-Token(嘗鮮)
本文轉載自微信公眾號「Java大廠面試官」,作者laker。轉載本文請聯系Java大廠面試官公眾號。
目前我僅以學習和嘗鮮為目的來集成,不建議用于公司等正式環境,公司還是建議Shiro和Spring Security那一套。(等我實戰一波看看效果再說)
為什么要嘗鮮Sa-Token
之前我還是挺排斥國產小作坊的開源作品,畢竟不是根紅苗正,但是隨著近幾年國內開源社區的大力發展,以及在平時工作中又接觸了解很多,慢慢改變了我的看法,其實國人開源作品還是很香的,其Api簡單易用,源碼和官方文檔都是中文的,功能豐富且能滿足很多中國式需求,各種QQ、微信交流群活躍度非常高,總之就是極大程度滿足中國式需求。
在權限認證框架領域,使用最多的莫過于Shiro和Spring Security,但是一天在逛同性交友網站(github)的時候,赫然發現了Sa-Token其竟然有2K的star數量,看其中文介紹竟然是輕量級Java權限認證框架,看了下其特性和功能點,就喚起了我強烈的好奇心,于是乎就有了今天的嘗鮮。
Sa-Token是什么?
sa-token是一個輕量級Java權限認證框架,主要解決:登錄認證、權限認證、Session會話、單點登錄、OAuth2.0 等一系列權限相關問題
框架針對踢人下線、自動續簽、前后臺分離、分布式會話……等常見業務進行N多適配,通過sa-token,你可以以一種極簡的方式實現系統的權限認證部分
與其它權限認證框架相比,sa-token 具有以下優勢:
- 簡單 :可零配置啟動框架,真正的開箱即用,低成本上手
- 強大 :目前已集成幾十項權限相關特性,涵蓋了大部分業務場景的解決方案
- 易用 :如絲般順滑的API調用,大量高級特性統統只需一行代碼即可實現
- 高擴展 :幾乎所有組件都提供了擴展接口,90%以上的邏輯都可以按需重寫
Sa-Token 能做什么?
- 登錄驗證 —— 輕松登錄鑒權,并提供五種細分場景值
- 權限驗證 —— 適配RBAC權限模型,不同角色不同授權
- Session會話 —— 專業的數據緩存中心
- 踢人下線 —— 將違規用戶立刻清退下線
- 持久層擴展 —— 可集成Redis、Memcached等專業緩存中間件,重啟數據不丟失
- 分布式會話 —— 提供jwt集成和共享數據中心兩種分布式會話方案
- 單點登錄 —— 一處登錄,處處通行
- 模擬他人賬號 —— 實時操作任意用戶狀態數據
- 臨時身份切換 —— 將會話身份臨時切換為其它賬號
- 無Cookie模式 —— APP、小程序等前后臺分離場景
- 同端互斥登錄 —— 像QQ一樣手機電腦同時在線,但是兩個手機上互斥登錄
- 多賬號認證體系 —— 比如一個商城項目的user表和admin表分開鑒權
- 花式token生成 —— 內置六種token風格,還可自定義token生成策略
- 注解式鑒權 —— 優雅的將鑒權與業務代碼分離
- 路由攔截式鑒權 —— 根據路由攔截鑒權,可適配restful模式
- 自動續簽 —— 提供兩種token過期策略,靈活搭配使用,還可自動續簽
- 會話治理 —— 提供方便靈活的會話查詢接口
- 記住我模式 —— 適配[記住我]模式,重啟瀏覽器免驗證
- 密碼加密 —— 提供密碼加密模塊,可快速MD5、SHA1、SHA256、AES、RSA加密
- 組件自動注入 —— 零配置與Spring等框架集成
快速集成
依賴導入
- <dependency>
- <groupId>cn.dev33</groupId>
- <artifactId>sa-token-spring-boot-starter</artifactId>
- <version>1.15.2</version>
- </dependency>
“最新版本去maven中央庫自己查詢下,當前是1.15.2。
配置文件
你可以零配置啟動項目但同時你也可以在application.yml中增加如下配置,定制性使用框架:
- spring:
- # sa-token配置
- sa-token:
- # token名稱 (同時也是cookie名稱)
- token-name: satoken
- # token有效期,單位s 默認30天, -1代表永不過期
- timeout: 2592000
- # token臨時有效期 (指定時間內無操作就視為token過期) 單位: 秒
- activity-timeout: -1
- # 是否允許同一賬號并發登錄 (為true時允許一起登錄, 為false時新登錄擠掉舊登錄)
- allow-concurrent-login: true
- # 在多人登錄同一賬號時,是否共用一個token (為true時所有登錄共用一個token, 為false時每次登錄新建一個token)
- is-share: false
- # token風格
- token-style: uuid
登錄
- @PostMapping("/api/v1/login")
- @ApiOperationSupport(order = 1)
- @ApiOperation(value = "登錄")
- public Response login(String userName, String pwd) {
- log.info("login,username:{},pwd:{}", userName, pwd);
- // 模擬 校驗用戶名密碼
- Long userId = check(userName,pwd);
- StpUtil.setLoginId(userId);
- return Response.ok(StpUtil.getTokenInfo());
- }
核心就一行StpUtil.setLoginId(userId),來看看它幫我們做了什么?
源碼及其簡單,還有很多中文注釋,跟著讀就行了,直接貼結論。
- 創建token
- 創建SaSession
- 在session上記錄token簽名
- 創建token、loginId映射
- token寫入cookie
底層會話等存儲使用的是Map
源碼如下:
- /**
- * 數據集合
- */
- public Map<String, Object> dataMap = new ConcurrentHashMap<String, Object>();
- /**
- * 過期時間集合 (單位: 毫秒) , 記錄所有key的到期時間 [注意不是剩余存活時間]
- */
- public Map<String, Long> expireMap = new ConcurrentHashMap<String, Long>();
調用結果如下:
- Response Heards
- Connection: keep-alive
- Content-Type: application/json
- Date: Fri, 09 Apr 2021 07:33:59 GMT
- Keep-Alive: timeout=60
- // 重點
- Set-Cookie: LakerToken=da14afd3f4b648a889a1e51ac3ec53d7; Max-Age=1800; Expires=Fri, 09-Apr-2021 08:03:59 GMT; Path=/
- Transfer-Encoding: chunked
- Response Body
- {
- "code": 200,
- "msg": "",
- "data": {
- "tokenName": "LakerToken",
- "tokenValue": "da14afd3f4b648a889a1e51ac3ec53d7",
- "isLogin": true,
- "loginId": "1",
- "loginKey": "login",
- "tokenTimeout": 1784,
- "sessionTimeout": 1784,
- "tokenSessionTimeout": -2,
- "tokenActivityTimeout": 30,
- "loginDevice": "default-device"
- }
- }
可以看到返回heards中已自動設置:Set-Cookie: LakerToken=da14afd3f4b648a889a1e51ac3ec53d7; Max-Age=1800; Expires=Fri, 09-Apr-2021 08:03:59 GMT; Path=/
登出
- @PostMapping("/api/v1/loginOut")
- @ApiOperationSupport(order = 3)
- @ApiOperation(value = "登出")
- @SaCheckLogin
- public Response loginOut() {
- StpUtil.logout();
- return Response.ok();
- }
核心也是一行StpUtil.logout(),來看看它幫我們做了什么?
- 獲取HttpRequest
- 嘗試從request里讀取token
- 嘗試從請求體里面讀取token
- 嘗試從header里讀取token
- 嘗試從cookie里讀取token
- 刪除cookie
- 刪除token、loginId映射
- 注銷session
請求攔截鑒權
第一步:配置全局攔截器
- @Configuration
- public class MySaTokenConfig implements WebMvcConfigurer {
- /**
- * 注冊sa-token的攔截器,打開注解式鑒權功能 (如果您不需要此功能,可以刪除此類)
- */
- @Override
- public void addInterceptors(InterceptorRegistry registry) {
- registry.addInterceptor(new SaAnnotationInterceptor()).addPathPatterns("/**");
- }
- }
第二步:在需要攔截的類或者方法上加注解
- @SaCheckLogin: 標注在方法或類上,當前會話必須處于登錄狀態才可通過校驗
- @SaCheckRole("admin"): 標注在方法或類上,當前會話必須具有指定角色標識才能通過校驗
- @SaCheckPermission("user:add"): 標注在方法或類上,當前會話必須具有指定權限才能通過校驗
例如:
- @GetMapping("/api/v1/tokenInfo")
- @ApiOperationSupport(order = 2)
- @ApiOperation(value = "獲取當前會話的token信息")
- @SaCheckLogin
- public Response tokenInfo() {
- return Response.ok(StpUtil.getTokenInfo());
- }
加上@SaCheckLogin則該接口必須處于登錄狀態才可通過校驗。
這里核心攔截校驗又是如何工作的呢?可以看下SaAnnotationInterceptor.java源碼,基于SpringMvc的攔截器實現的攔截校驗。
實現功能如下:
- 驗證登錄
- 驗證角色
- 驗證權限
實現流程原理如下:
- 獲取HttpRequest中的token
- 嘗試從request里讀取token
- 嘗試從請求體里面讀取token
- 嘗試從header里讀取token
- 嘗試從cookie里讀取token
- 判斷token
- 無效token
- 過期
- 被頂下線
- 被踢下線
- 自動續期
權限和角色擴展
直接實現StpInterface接口,覆寫getPermissionList和getRoleList方法即可。
- @Component
- public class StpInterfaceImpl implements StpInterface {
- /**
- * 返回一個賬號所擁有的權限碼集合
- */
- @Override
- public List<String> getPermissionList(Object loginId, String loginKey) {
- xxx
- }
- /**
- * 返回一個賬號所擁有的角色標識集合
- */
- @Override
- public List<String> getRoleList(Object loginId, String loginKey) {
- xxx
- }
- }
集群環境
Sa-token默認將會話數據保存在內存中,此模式讀寫速度最快,且避免了序列化與反序列化帶來的性能消耗,但是此模式也有一些缺點,比如:重啟后數據會丟失,無法在集群模式下共享數據。
為此,sa-token將數據持久操作全部抽象到 SaTokenDao 接口中,此設計可以保證開發者對框架進行靈活擴展,比如我們可以將會話數據存儲在 Redis、Memcached等專業的緩存中間件中,做到重啟數據不丟失,而且保證分布式環境下多節點的會話一致性。
除了框架內部對SaTokenDao提供的基于內存的默認實現,我們使用官網提供的Redis擴展。
依賴導入
- <!-- sa-token整合redis (使用jackson序列化方式) -->
- <dependency>
- <groupId>cn.dev33</groupId>
- <artifactId>sa-token-dao-redis-jackson</artifactId>
- <version>1.15.2</version>
- </dependency>
- <!-- 提供redis連接池 -->
- <dependency>
- <groupId>org.apache.commons</groupId>
- <artifactId>commons-pool2</artifactId>
- </dependency>
“使用Jackson序列化方式,Session序列化后可讀性強,可靈活手動修改
配置文件
- spring:
- # redis配置
- redis:
- # Redis數據庫索引(默認為0)
- database: 0
- # Redis服務器地址
- host: 127.0.0.1
- # Redis服務器連接端口
- port: 6379
- # Redis服務器連接密碼(默認為空)
- # password:
- # 連接超時時間(毫秒)
- timeout: 1000ms
- lettuce:
- pool:
- # 連接池最大連接數
- max-active: 200
- # 連接池最大阻塞等待時間(使用負值表示沒有限制)
- max-wait: -1ms
- # 連接池中的最大空閑連接
- max-idle: 10
- # 連接池中的最小空閑連接
- min-idle: 0
引入依賴和配置后,框架會自動使用Redis存儲。
總結
初步嘗試還挺不錯的,文檔和代碼示例都很全,基本功能都能滿足,源碼簡單易懂,可以隨意二開,封裝度非常高,不理解原理的就很容易變成工具人了,其他的等用一段時間再評論。
參考:
http://sa-token.dev33.cn/
https://github.com/dromara/sa-token