很久沒寫更新內容了,新的一年也開始了,是時候該把自己的東西整理一遍了。2018年也沒少看書,但是真正屬於自己的東西很少很少,或者學習的時候淺嘗輒止,也是時候給自己清醒清醒了。
公司自己的項目是基於Spring Boot敏捷開發的,起初對於接口的鑑權等認證操作都很粗糙,網上也蒐集了一下其他資料,總的來說。比較詳細的鑑權的兩種方式如下:
其一是認證與鑑權,對於請求的用戶身份的授權以及合法性鑑權;其二是API級別的操作權限控制,這個在第一點之後,當鑑定完用戶身份合法之後,對於該用戶的某個具體請求是否具有該操作執行權限進行校驗。
認證與鑑權
對於第一個需求,筆者調查了一些實現方案:
-
分佈式
Session
方案
分佈式會話方案原理主要是將關於用戶認證的信息存儲在共享存儲中,且通常由用戶會話作爲 key 來實現的簡單分佈式哈希映射。當用戶訪問微服務時,用戶數據可以從共享存儲中獲取。在某些場景下,這種方案很不錯,用戶登錄狀態是不透明的。同時也是一個高可用且可擴展的解決方案。這種方案的缺點在於共享存儲需要一定保護機制,因此需要通過安全鏈接來訪問,這時解決方案的實現就通常具有相當高的複雜性了。 -
基於
OAuth2 Token
方案
隨着 Restful API、微服務的興起,基於Token
的認證現在已經越來越普遍。Token和Session ID 不同,並非只是一個 key。Token 一般會包含用戶的相關信息,通過驗證 Token 就可以完成身份校驗。用戶輸入登錄信息,發送到身份認證服務進行認證。AuthorizationServer驗證登錄信息是否正確,返回用戶基礎信息、權限範圍、有效時間等信息,客戶端存儲接口。用戶將 Token 放在 HTTP 請求頭中,發起相關 API 調用。被調用的微服務,驗證Token
。ResourceServer返回相關資源和數據。
這邊選用了第二種方案,基於OAuth2 Token
認證的好處如下:
- 服務端無狀態:Token 機制在服務端不需要存儲 session 信息,因爲 Token 自身包含了所有用戶的相關信息。
- 性能較好,因爲在驗證 Token 時不用再去訪問數據庫或者遠程服務進行權限校驗,自然可以提升不少性能。
- 現在很多應用都是同時面向移動端和web端,
OAuth2 Token
機制可以支持移動設備。 - OAuth2與Spring Security結合使用,有提供很多開箱即用的功能,大多特性都可以通過配置靈活的變更。
- 最後一點,也很重要,Spring Security OAuth2的文檔寫得較爲詳細。
oauth2根據使用場景不同,分成了4種模式:
- 授權碼模式(authorization code)
- 簡化模式(implicit)
- 密碼模式(resource owner password credentials)
- 客戶端模式(client credentials)
對於上述oauth2四種模式不熟的同學,可以自行百度oauth2,阮一峯的文章有解釋。常使用的是password模式和client模式。
操作權限控制
對於第二個需求,筆者主要看了Spring Security和Shiro。
-
Shiro
Shiro是一個強大而靈活的開源安全框架,能夠非常清晰的處理認證、授權、管理會話以及密碼加密。Shiro很容易入手,上手快控制粒度可糙可細。自由度高,Shiro既能配合Spring使用也可以單獨使用。 -
Spring Security
Spring社區生態很強大。除了不能脫離Spring,Spring Security具有Shiro所有的功能。而且Spring Security對Oauth、OpenID也有支持,Shiro則需要自己手動實現。Spring Security的權限細粒度更高。但是Spring Security太過複雜。
看了下網上的評論,貌似一邊倒向Shiro。大部分人提出的Spring Security
問題就是比較複雜難懂,文檔太長。不管是Shiro
還是Spring Security
,其實現都是基於過濾器,對於自定義實現過濾器,我想對於很多開發者並不是很難,但是這需要團隊花費時間與封裝可用的jar包出來,對於後期維護和升級,以及功能的擴展。很多中小型公司並不一定具有這樣的時間和人力投入這件事。筆者綜合評估了下複雜性與所要實現的權限需求,以及上一個需求調研的結果,既然Spring Security
功能足夠強大且穩定,最終選擇了Spring Security
。
廢話少說,先來一波基於JWT實現的TOKEN的方式:
package com.whb.web.utils;
import java.text.ParseException;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import com.nimbusds.jose.JOSEException;
import com.nimbusds.jose.JWSAlgorithm;
import com.nimbusds.jose.JWSHeader;
import com.nimbusds.jose.JWSObject;
import com.nimbusds.jose.JWSSigner;
import com.nimbusds.jose.JWSVerifier;
import com.nimbusds.jose.Payload;
import com.nimbusds.jose.crypto.MACSigner;
import com.nimbusds.jose.crypto.MACVerifier;
import net.minidev.json.JSONObject;
public class JwtUtils {
/**
* 1.創建一個32-byte的密匙
*/
private static final byte[] secret = "geiwodiangasfdjsikolkjikolkijswe".getBytes();
// 生成一個token
public static String creatToken(Map<String, Object> payloadMap) throws JOSEException {
// 3.先建立一個頭部Header
/**
* JWSHeader參數:1.加密算法法則,2.類型,3.。。。。。。。 一般只需要傳入加密算法法則就可以。 這裏則採用HS256
*
* JWSAlgorithm類裏面有所有的加密算法法則,直接調用。
*/
JWSHeader jwsHeader = new JWSHeader(JWSAlgorithm.HS256);
// 建立一個載荷Payload
Payload payload = new Payload(new JSONObject(payloadMap));
// 將頭部和載荷結合在一起
JWSObject jwsObject = new JWSObject(jwsHeader, payload);
// 建立一個密匙
JWSSigner jwsSigner = new MACSigner(secret);
// 簽名
jwsObject.sign(jwsSigner);
// 生成token
return jwsObject.serialize();
}
/**
* 解析一個token
*
* @param token
* @return
* @throws ParseException
* @throws JOSEException
* @throws java.text.ParseException
*/
public static Map<String, Object> valid(String token) throws ParseException, JOSEException {
// 解析token
JWSObject jwsObject = JWSObject.parse(token);
// 獲取到載荷
Payload payload = jwsObject.getPayload();
// 建立一個解鎖密匙
JWSVerifier jwsVerifier = new MACVerifier(secret);
Map<String, Object> resultMap = new HashMap<>();
// 判斷token
if (jwsObject.verify(jwsVerifier)) {
resultMap.put("Result", 0);
// 載荷的數據解析成json對象。
JSONObject jsonObject = payload.toJSONObject();
resultMap.put("data", jsonObject);
// 判斷token是否過期
if (jsonObject.containsKey("exp")) {
Long expTime = Long.valueOf(jsonObject.get("exp").toString());
Long nowTime = new Date().getTime();
// 判斷是否過期
if (nowTime > expTime) {
// 已經過期
resultMap.clear();
resultMap.put("Result", 2);
}
}
} else {
resultMap.put("Result", 1);
}
return resultMap;
}
//生成token的業務邏輯
public static String TokenTest(String uid) {
//獲取生成token
Map<String, Object> map = new HashMap<>();
//建立載荷,這些數據根據業務,自己定義。
map.put("uid", uid);
//生成時間
map.put("sta", new Date().getTime());
//過期時間
map.put("exp", new Date().getTime()+6);
try {
String token = JwtUtils.creatToken(map);
System.out.println("token="+token);
return token;
} catch (JOSEException e) {
System.out.println("生成token失敗");
e.printStackTrace();
}
return null;
}
//處理解析的業務邏輯
public static void ValidToken(String token) {
//解析token
try {
if (token != null) {
Map<String, Object> validMap = JwtUtils.valid(token);
int i = (int) validMap.get("Result");
if (i == 0) {
System.out.println("token解析成功");
JSONObject jsonObject = (JSONObject) validMap.get("data");
System.out.println("uid是" + jsonObject.get("uid"));
System.out.println("sta是"+jsonObject.get("sta"));
System.out.println("exp是"+jsonObject.get("exp"));
} else if (i == 2) {
System.out.println("token已經過期");
}
}
} catch (ParseException e) {
e.printStackTrace();
} catch (JOSEException e) {
e.printStackTrace();
}
}
public static void main(String[] ages) {
//獲取token
String uid = "kkksuejrmf";
String token = TokenTest(uid);
//解析token
ValidToken(token);
}
}