Token
Token是一種分佈在客戶端的無狀態用戶登陸證明,其誕生場景如下所述。
服務器 無法保留或不願意保留(服務器開銷)用戶的登陸狀態,要想確認先後兩次發來的兩條消息來自同一個用戶,無非以下兩種情況:
- 用戶每次發送請求都帶上自己的認證ID和敏感信息,如賬號和密碼;服務器每次處理處理請求時都從服務器數據庫驗證用戶身份以確定用戶的登陸狀態;
- 基於Session等方式記錄用戶的登陸信息;
很顯然,上面兩種方式,第一種方式容易造成用戶敏感信息泄露;而第二種方式又增大了服務器開銷。那麼,有沒有什麼能夠即不造成用戶敏感信息泄露同時又能減輕服務器負擔呢?
Token 就是這樣一種技術。
說是一種技術,其實更是一種思想,它的操作步驟是上面第一種方式基於數學方式的改良:
- 用戶未登陸時,發送認證ID(身份標識)和密碼等敏感信息進行登陸;如果登陸成功,服務器根據身份標識和服務器私鑰計算Token並返回(計算的方法下面會提到),客戶端保留這個Token;
- 登陸後的用戶每次發送請求時都帶上這個Token以及自己的身份標識,顯然身份標識和Token並不是敏感數據(Token一般具有時效性和經過數字摘要處理,因此不屬於敏感數據)。
- 服務器根據身份標識和服務器私鑰再計算一次Token,和用戶附帶的Token進行比較,如果一致,則認爲是同一個用戶。
- 顯然,經過以上步驟,用戶的登陸狀態就被安全的保存到了客戶端,而非擠佔服務器空間。
- 但是這種方法也有一個很大的問題:Token泄露。
Token泄露
Token泄露有多種情況,例如駭客通過某些方式獲得了服務器的私鑰和猜到了數字摘要計算方法,或者直接明文竊取一個已登陸的用戶發往服務器的Token,僞裝自己是這個用戶騙取服務器數據。
- 如果只是泄露私鑰(內鬼),駭客不知道數字摘要的計算方法依舊無法模擬出目標用戶的Token,因此利用PBKDF2和等摘要算法對摘要的哈希、裁剪,完全可以應對這個問題。
- 而如果是竊取報文偷取Token,只能通過SSL等加密通信進行解決;因此完全基於Token的用戶狀態認證並不是安全的服務器數據訪問方式,需要搭配數據安全層協議才能投入使用。
數字摘要
數字摘要所使用的算法常見的如MD5、SHA256、SHA512等,更有經過哈希散列的HmacMD5等。
這裏有基於JAVA的摘要算法封裝工具類:
[JAVA]數字摘要算法工具類——(Hamc)MD5/SHA1/SHA256/SHA512/PBKDF2
簡單Token管理類實例
代碼獲取:>此處下載<或直接從下文中複製。
設計結構
兩個函數類接口:
接口名 | 接口說明 |
---|---|
TokenPartsInterface | 除了用戶發往服務器的身份標識外,Token原值的其它內容,如服務器私鑰等 |
TokenGenerateAlgsInterface | 對輸入的數據進行數字摘要的計算 |
一個管理類TokenManager
API:
方法名 | 方法說明 |
---|---|
generateToken | 產生一個Token |
authenticateToken | 認證Token是否來自同一個用戶 |
addTokenParts | 添加Token原值的組成部分(TokenPartsInterface ) |
setGenerator | 設置數字摘要的產生器(TokenGenerateAlgsInterface) |
注意:
- TokenManager對TokenParts的處理簡單以
"身份標識+parts"
的方式處理。如有特殊需要,請重寫TokenManager.combineTokenParts()
方法。clipToken- TokenManager對Token的剪裁簡單以返回原值的方式處理。如有特殊需要,請重寫
TokenManager.clipToken()
方法。- 數字摘要生成器的使用可以參考下文中的代碼使用Demo。
完整代碼
兩個接口:
@FunctionalInterface
public interface TokenPartsInterface {
public String getParts();
}
@FunctionalInterface
public interface TokenGenerateAlgsInterface {
public String generate(String msgs);
}
TokenManager:
import java.util.Arrays;
import java.util.LinkedList;
public class TokenManager {
private TokenGenerateAlgsInterface generator;
private LinkedList<TokenPartsInterface> tokenPartsList = new LinkedList<>();
public TokenManager(){}
public TokenManager(TokenGenerateAlgsInterface generator){
this.generator = generator;
}
/**
* 設置一個摘要產生器
* @param generator 摘要產生器,如經過包裝的:
* @see java.security.MessageDigest
*/
public void setGenerator(TokenGenerateAlgsInterface generator) {
this.generator = generator;
}
/**
* 添加Token的組成部分
* @param tokenParts Token的組成部分
* @return 操作指示符
*/
public boolean addTokenParts(TokenPartsInterface tokenParts){
return this.tokenPartsList.add(tokenParts);
}
/**
* 移除Token的組成部分
* @param tokenParts Token的組成部分
* @return 操作指示符
*/
public boolean removeTokenParts(TokenPartsInterface tokenParts){
return this.tokenPartsList.removeFirstOccurrence(tokenParts);
}
/**
* 將外來輸入和Token組成部分進行組合,生成Token的原始值
* @param outComeMsgs 外來輸入
* @return 將外來輸入和Token組成部分進行組合,生成Token
*/
private String combineTokenParts(String outComeMsgs[]){
StringBuffer result = new StringBuffer();
Arrays.asList(outComeMsgs).forEach(result::append);
this.tokenPartsList.forEach(x->result.append(x.getParts()));
return result.toString();
}
/**
* 將外來輸入和Token組成部分進行組合,生成Token
* @param outComeMsgs 外來輸入
* @return Token
*/
public String generateToken(String outComeMsgs[]){
assert this.generator != null;
return clipToken(this.generator.generate(this.combineTokenParts(outComeMsgs)));
}
/**
* 對比Token是否合法
* @param outComeMsgs 外來輸入
* @return Token
*/
public boolean authenticateToken(String token, String outComeMsgs[]){
return this.generateToken(outComeMsgs).equals(token);
}
/**
* 對Token的剪裁方法
* @param token 輸入的token
* @return 剪裁後的token
*/
private String clipToken(String token){return token;}
}
使用示例
public static void main(String[] args){
// ENV:JDK8
// 模擬客戶端發來的身份標識
String[] msgs = new String[]{"userId"};
// 時效性參數生成器
SimpleDateFormat sdf = new SimpleDateFormat("YYYY-MM-dd");
// 服務器私鑰
String key = "Shenpibaipao";
// 1 初始化實例
// 1.1 摘要生成器 此處用的是:
// @See https://blog.csdn.net/Shenpibaipao/article/details/88391561
TokenManager tkm = new TokenManager(MDBuilder::getMD5);
// 1.2 Token的其它組成部分
tkm.addTokenParts(() -> key);
tkm.addTokenParts(() -> sdf.format(new Date()));
// 2 生成token
String token = tkm.generateToken(msgs);
// 3 驗證token
boolean authentication = tkm.authenticateToken(token, msgs);
// System.out.println(token);
// System.out.println(authentication);
// 生成算法默認情況下等效於:
// System.out.println(MDBuilder.getMD5("userId"+ key +sdf.format(new Date())));
}