主要介紹Sa-Token的鑒權(quán)使用以及實(shí)現(xiàn)原理。
簡介
官網(wǎng)介紹的非常詳細(xì),主要突出這是一個(gè)輕量級(jí)鑒權(quán)框架的特點(diǎn),詳情可自行訪問:https://sa-token.dev33.cn/doc.html#/
使用
旨在簡單使用,大部分功能均可以在一行代碼內(nèi)實(shí)現(xiàn),這里舉幾個(gè)官網(wǎng)示例:
首先添加依賴:
<!-- Sa-Token 權(quán)限認(rèn)證,在線文檔:https://sa-token.cc -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
<version>1.33.0</version>
</dependency>
yaml配置文件:
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: false
// 會(huì)話登錄,參數(shù)填登錄人的賬號(hào)id
StpUtil.login(10001);
// 校驗(yàn)當(dāng)前客戶端是否已經(jīng)登錄,如果未登錄則拋出 `NotLoginException` 異常
StpUtil.checkLogin();
// 將賬號(hào)id為 10077 的會(huì)話踢下線
StpUtil.kickout(10077);
功能圖如下所示:
源碼解釋
解釋登錄原理
StpUtil.login(usId);
這短短的一句話代碼蘊(yùn)藏了多少玄機(jī),我們一探究竟。
首先我們觀察現(xiàn)象,模擬一個(gè)登錄入口/login,里面做一個(gè)最簡單的動(dòng)作,就是將前端傳入的usId作為用戶id并交給StpUtil執(zhí)行登錄邏輯,并最終將usId返回。
@GetMapping("/login")
private String login(String usId){
StpUtil.login(usId);
return usId;
}
訪問 http://localhost:8081/login?usId=123,成功返回結(jié)果123。
隨后我們F12查看瀏覽器的控制臺(tái),Application->打開Cookies:
能夠觀察到Cookies自動(dòng)新增了一個(gè)鍵為satoken的cookie鍵值對(duì),值類似于uuid隨機(jī)字符串,此處示例為9ea38efb-228f-4131-b844-903467caf205,過期時(shí)間設(shè)置了30天,可對(duì)應(yīng)前面配置文件的配置信息,satoken的cookie名稱以及過期時(shí)間均支持配置調(diào)整。
接下來我們分析源碼:
首先通過StpUtil作為入口,實(shí)際上通過實(shí)例化StpLogic來進(jìn)行調(diào)用:
// StpUtil.java
public static StpLogic stpLogic = new StpLogic("login");
public static void login(Object id) {
stpLogic.login(id);
}
隨后在StpLogic中,通過傳入的Object id以及SaLoginModel進(jìn)行構(gòu)建會(huì)話:
// StpLogic.java
public void login(Object id) {
this.login(id, new SaLoginModel());
}
// 通過這兩個(gè)方法進(jìn)行會(huì)話建立 createLoginSession()/setTokenValue()
public void login(Object id, SaLoginModel loginModel) {
// 1、創(chuàng)建會(huì)話
String token = this.createLoginSession(id, loginModel);
// 2、在當(dāng)前客戶端注入Token
setTokenValue(token, loginModel);
}
創(chuàng)建會(huì)話
分析StpLogic.createLoginSession()方法
public String createLoginSession(Object id, SaLoginModel loginModel) {
// 1.這個(gè)設(shè)計(jì)可以借鑒一下,直接可以判斷id值為null就拋異常,將異常封裝起來
SaTokenException.throwByNull(id, "賬號(hào)id不能為空", 11002);
// 2.獲取配置信息
SaTokenConfig config = this.getConfig();
loginModel.build(config);
// 3.生成token(若已經(jīng)有存在生效的token則使用原先的token)
String tokenValue = this.distUsableToken(id, loginModel);
// 4.獲取 User-Session/首次登錄則創(chuàng)建會(huì)話,使用自定義封裝的SaSession對(duì)象接收
SaSession session = this.getSessionByLoginId(id, true);
session.updateMinTimeout(loginModel.getTimeout());
session.addTokenSign(tokenValue, loginModel.getDeviceOrDefault());
// 5.設(shè)置token -> id 映射關(guān)系
this.saveTokenToIdMapping(tokenValue, id, loginModel.getTimeout());
// 調(diào)用getSaTokenDao().set設(shè)置tokenValue與失效時(shí)間的關(guān)系
this.setLastActivityToNow(tokenValue);
// 6.事件發(fā)布
SaTokenEventCenter.doLogin(this.loginType, id, tokenValue, loginModel);
// 7.檢查此賬號(hào)會(huì)話數(shù)量是否超出最大值,-1表示不限會(huì)話數(shù)量
if (config.getMaxLoginCount() != -1) {
logoutByMaxLoginCount(id, session, (String)null, config.getMaxLoginCount());
}
return tokenValue;
}
1.前置檢查
判斷Object id是否為null
SaTokenException.throwByNull(id, "賬號(hào)id不能為空", 11002);
public static void throwByNull(Object value, String message, int code) {
if(SaFoxUtil.isEmpty(value)) {
throw new SaTokenException(message).setCode(code);
}
}
2.獲取配置
StpLogic.getConfig():
// 2.獲取配置信息
SaTokenConfig config = this.getConfig();
3.分配token
StpLogic.distUsableToken():
protected String distUsableToken(Object id, SaLoginModel loginModel) {
Boolean isConcurrent = this.getConfig().getIsConcurrent();
if (!isConcurrent) {
this.replaced(id, loginModel.getDevice());
}
if (SaFoxUtil.isNotEmpty(loginModel.getToken())) {
return loginModel.getToken();
} else {
if (isConcurrent && this.getConfigOfIsShare() && !loginModel.isSetExtraData()) {
// 獲取token
String tokenValue = this.getTokenValueByLoginId(id, loginModel.getDeviceOrDefault());
// 已經(jīng)存在會(huì)話,將之前生成的token返回
if (SaFoxUtil.isNotEmpty(tokenValue)) {
return tokenValue;
}
}
// *新會(huì)話,生成一個(gè)新token,可借鑒,見下方解析
return this.createTokenValue(id, loginModel.getDeviceOrDefault(), loginModel.getTimeout(), loginModel.getExtraData());
}
}
解析StpLogic.createTokenValue():
public class StpLogic {
// ...
public String createTokenValue(Object loginId, String device, long timeout, Map<String, Object> extraData) {
// SaStrategy.me:使用SaStrategy的單例引用
// SaStrategy.me.createToken:調(diào)用createToken()方法
// SaStrategy.me.createToken.apply:使得結(jié)果生效
return SaStrategy.me.createToken.apply(loginId, loginType);
}
}
public final class SaStrategy {
public static final SaStrategy me = new SaStrategy();
public BiFunction<Object, String, String> createToken = (loginId, loginType) -> {
// 根據(jù)配置的tokenStyle生成不同風(fēng)格的token
String tokenStyle = SaManager.getConfig().getTokenStyle();
// uuid
if(SaTokenConsts.TOKEN_STYLE_UUID.equals(tokenStyle)) {
return UUID.randomUUID().toString();
}
// 簡單uuid (不帶下劃線)
if(SaTokenConsts.TOKEN_STYLE_SIMPLE_UUID.equals(tokenStyle)) {
return UUID.randomUUID().toString().replaceAll("-", "");
}
// 32位隨機(jī)字符串
if(SaTokenConsts.TOKEN_STYLE_RANDOM_32.equals(tokenStyle)) {
return SaFoxUtil.getRandomString(32);
}
// 64位隨機(jī)字符串
if(SaTokenConsts.TOKEN_STYLE_RANDOM_64.equals(tokenStyle)) {
return SaFoxUtil.getRandomString(64);
}
// 128位隨機(jī)字符串
if(SaTokenConsts.TOKEN_STYLE_RANDOM_128.equals(tokenStyle)) {
return SaFoxUtil.getRandomString(128);
}
// tik風(fēng)格 (2_14_16)
if(SaTokenConsts.TOKEN_STYLE_TIK.equals(tokenStyle)) {
return SaFoxUtil.getRandomString(2) + "_" + SaFoxUtil.getRandomString(14) + "_" + SaFoxUtil.getRandomString(16) + "__";
}
// 默認(rèn),還是uuid
return UUID.randomUUID().toString();
};
}
4.獲取 User-Session
StpLogic.getSessionByLoginId()
public SaSession getSessionByLoginId(Object loginId, boolean isCreate) {
return getSessionBySessionId(splicingKeySession(loginId), isCreate);
}
/**
* 獲取指定key的Session, 如果Session尚未創(chuàng)建,isCreate=是否新建并返回
* @param sessionId SessionId
* @param isCreate 是否新建
* @return Session對(duì)象
*/
public SaSession getSessionBySessionId(String sessionId, boolean isCreate) {
// getSaTokenDao使用了懶加載初始化SaTokenDao對(duì)象,最終由new SaTokenDaoDefaultImpl()進(jìn)行實(shí)現(xiàn)具體方法
// 并根據(jù)sessionId獲取session
SaSession session = getSaTokenDao().getSession(sessionId);
// session暫未建立,進(jìn)行session新建
if(session == null && isCreate) {
// 與上方的createToken使用了同樣的設(shè)計(jì)
session = SaStrategy.me.createSession.apply(sessionId);
// 設(shè)置session與session過期時(shí)效
getSaTokenDao().setSession(session, getConfig().getTimeout());
}
return session;
}
5.設(shè)置token-id映射關(guān)系
StpLogic.saveTokenToIdMapping()
public void saveTokenToIdMapping(String tokenValue, Object loginId, long timeout) {
// 如果繼續(xù)往下深挖其實(shí)set方法的實(shí)現(xiàn)底層就是一個(gè)new ConcurrentHashMap<String, Object>()
// 并且封裝了一個(gè)new ConcurrentHashMap<String, Long>()來記錄key與過期時(shí)間的關(guān)系
// 并且設(shè)置的過期過期時(shí)間
getSaTokenDao().set(splicingKeyTokenValue(tokenValue), String.valueOf(loginId), timeout);
}
這里淺淺看一下設(shè)置的過期時(shí)間如何實(shí)現(xiàn):
/**
* 數(shù)據(jù)集合
*/
public Map<String, Object> dataMap = new ConcurrentHashMap<String, Object>();
/**
* 過期時(shí)間集合 (單位: 毫秒) , 記錄所有key的到期時(shí)間 [注意不是剩余存活時(shí)間]
*/
public Map<String, Long> expireMap = new ConcurrentHashMap<String, Long>();
@Override
public void set(String key, String value, long timeout) {
if(timeout == 0 || timeout <= SaTokenDao.NOT_VALUE_EXPIRE) {
return;
}
// 設(shè)置key-value
dataMap.put(key, value);
// 設(shè)置key-到期時(shí)間
expireMap.put(key, (timeout == SaTokenDao.NEVER_EXPIRE) ? (SaTokenDao.NEVER_EXPIRE) : (System.currentTimeMillis() + timeout * 1000));
}
/**
* 如果指定key已經(jīng)過期,則立即清除它。在每個(gè)get方法前都首先調(diào)用一下這個(gè)方法
* @param key 指定key
*/
void clearKeyByTimeout(String key) {
Long expirationTime = expireMap.get(key);
// 清除條件:如果不為空 && 不是[永不過期] && 已經(jīng)超過過期時(shí)間
if(expirationTime != null && expirationTime != SaTokenDao.NEVER_EXPIRE && expirationTime < System.currentTimeMillis()) {
dataMap.remove(key);
expireMap.remove(key);
}
}
// ------------------------ String 讀寫操作
@Override
public String get(String key) {
// 首先判斷一下key是否已經(jīng)過期
clearKeyByTimeout(key);
return (String)dataMap.get(key);
}
6.登錄成功事件發(fā)布
SaTokenEventCenter.doLogin
// --------- 注冊(cè)偵聽器
private static List<SaTokenListener> listenerList = new ArrayList<>();
/**
* 每次登錄時(shí)觸發(fā)
* @param loginType 賬號(hào)類別
* @param loginId 賬號(hào)id
* @param tokenValue 本次登錄產(chǎn)生的 token 值
* @param loginModel 登錄參數(shù)
*/
public static void doLogin(String loginType, Object loginId, String tokenValue, SaLoginModel loginModel) {
for (SaTokenListener listener : listenerList) {
listener.doLogin(loginType, loginId, tokenValue, loginModel);
}
}
主要目的是登錄成功后的一些后置處理方法回調(diào)等,通過觀察我們可以發(fā)現(xiàn)只要注冊(cè)到SaTokenEventCenter.listenerList中即可在遍歷中執(zhí)行監(jiān)聽器的doLogin()方法。目前默認(rèn)添加控制臺(tái)日志偵聽器,new SaTokenListenerForLog(),主要實(shí)現(xiàn)一些日志打?。?/p>
還有一個(gè)空實(shí)現(xiàn)的SaTokenListenerForSimple監(jiān)聽器,后續(xù)我們?nèi)绻胍鲆恍┳远x擴(kuò)展,就可以繼承SaTokenListenerForSimple做一些屬于我們自己的業(yè)務(wù)監(jiān)聽器處理:
7.檢查會(huì)話數(shù)量
StpLogic.logoutByMaxLoginCount()
/**
* 會(huì)話注銷,根據(jù)賬號(hào)id 和 設(shè)備類型 和 最大同時(shí)在線數(shù)量
*
* @param loginId 賬號(hào)id
* @param session 此賬號(hào)的 Session 對(duì)象,可填寫null,框架將自動(dòng)獲取
* @param device 設(shè)備類型 (填null代表注銷所有設(shè)備類型)
* @param maxLoginCount 保留最近的幾次登錄
*/
public void logoutByMaxLoginCount(Object loginId, SaSession session, String device, int maxLoginCount) {
if(session == null) {
session = getSessionByLoginId(loginId, false);
if(session == null) {
return;
}
}
List<TokenSign> list = session.tokenSignListCopyByDevice(device);
// 遍歷操作
for (int i = 0; i < list.size(); i++) {
// 只操作前n條
if(i >= list.size() - maxLoginCount) {
continue;
}
// 清理: token簽名、token最后活躍時(shí)間
String tokenValue = list.get(i).getValue();
session.removeTokenSign(tokenValue);
clearLastActivity(tokenValue);
// 刪除Token-Id映射 & 清除Token-Session
deleteTokenToIdMapping(tokenValue);
deleteTokenSession(tokenValue);
// $$ 發(fā)布事件:指定賬號(hào)注銷
SaTokenEventCenter.doLogout(loginType, loginId, tokenValue);
}
// 注銷 Session
session.logoutByTokenSignCountToZero();
}
客戶端注入Token
分析StpLogic.setTokenValue()方法文章來源:http://www.zghlxwxcb.cn/news/detail-484784.html
/**
* 在當(dāng)前會(huì)話寫入當(dāng)前TokenValue
* @param tokenValue token值
* @param loginModel 登錄參數(shù)
*/
public void setTokenValue(String tokenValue, SaLoginModel loginModel){
if(SaFoxUtil.isEmpty(tokenValue)) {
return;
}
// 1. 將 Token 保存到 [存儲(chǔ)器] 里
setTokenValueToStorage(tokenValue);
// 2. 將 Token 保存到 [Cookie] 里 此處對(duì)應(yīng)
if (getConfig().getIsReadCookie()) {
setTokenValueToCookie(tokenValue, loginModel.getCookieTimeout());
}
// 3. 將 Token 寫入到響應(yīng)頭里
if(loginModel.getIsWriteHeaderOrGlobalConfig()) {
setTokenValueToResponseHeader(tokenValue);
}
}
參考資料:文章來源地址http://www.zghlxwxcb.cn/news/detail-484784.html
- Sa-Token官網(wǎng)介紹
- sa-token使用(源碼解析 + 萬字)
到了這里,關(guān)于Sa-Token淺談的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!