1 API接口驗證與授權
JWT
JWT定義,它包含三部分:header,payload,signature;每一部分都是使用Base64編碼的JSON字符串。之間以句號分隔。signature是”header.payload”經加密后的字符串。
采用JWT實現驗證與授權檢驗機制,JWT格式為:
header :
{
"typ": "JWT",
"alg": "HS256"
}
payload:appid為GUID,timestamp為unix時間戳
{
"appid": GUID,
"timestamp": Unix time
}
Signature:使用HS256(HMAC SHA-256,SHA Secure Hash Algorithm,安全散列算法)對header和payload以‘.’連接的字符串進行簽名。
對JWT加密:采用RSA加密算法對其進行加密。
密鑰發放
發放給客戶端的參數:appId、appSecret、publicKey、privateKeyId。其中publicKey為RSA公鑰,privateKeyId為服務端私鑰Id。服務端或根據privateKeyId在緩存(本地或Redis等)中查找RSA私鑰。
合成accessToken:header、payload與上述相同,簽名密鑰為appSecret。合成以后,使用publicKey對其進行加密。
合成headerJson:由accessToken和privateKeyId構成的Json字符串,然后將字符串用Base64編碼方式編碼。
驗證流程
客戶端將上述headerB64放入請求頭,向服務端發起請求,服務端從請求頭中拿到headerJson並解碼headerJson,進而從中得到accessToken和privateKeyId,服務端根據privateKeyId找到privateKey,使用privateKey對accessToken解密,根據payload中的timestamp驗證過期,若未過期,那么進行簽名校驗,驗證通過授權用戶端。
示例代碼(關鍵性代碼)
public abstract class BasicAuthenticationAttribute : Attribute, IAuthenticationFilter { public async Task AuthenticateAsync(HttpAuthenticationContext context, System.Threading.CancellationToken cancellationToken) { await Task.Factory.StartNew(()=> { //解析頭信息,獲得appid和timestamp var header = ... //如果未獲得上述信息 if (header == null) { context.ErrorResult = new AuthenticationFailureResult(requestHeaderAnalysis.ExecStatus, context.Request); return; } //從緩存中獲得RSA私鑰 string privateKey= ... if (String.IsNullOrWhiteSpace(privateKey)) { context.ErrorResult = new AuthenticationFailureResult(StatusCodeManager.GetStatusInfo("B07", "a001"), context.Request); return; } //使用RSA私鑰對AccessToken解密 string accessToken = Decrypt(requestHeaderInfo.AccessToken, privateKey); if (String.IsNullOrWhiteSpace(accessToken)) {//驗證憑據是空,設置錯誤信息 context.ErrorResult = new AuthenticationFailureResult(StatusCodeManager.GetStatusInfo("B05", "a001"), context.Request); return; } //從AccessToken的payload中獲得appKey和timestamp(時間戳) var payloadDict = JsonWebToken.DecodeToObject(accessToken); string appKey = Convert.ToString(payloadDict["appKey"]); string timestamp = Convert.ToString(payloadDict["timestamp"]); //在服務端數據庫中,根據appKey查找appSecret ApiAccount apiAccount = GetApiAccount(appKey); if (apiAccount==null||string.IsNullOrWhiteSpace(apiAccount.AppSecret)) { context.ErrorResult = new AuthenticationFailureResult(StatusCodeManager.GetStatusInfo("B05", "a001"), context.Request); return; } //驗證是否超時,簽名是否被篡改 try { //允許的時間段(小時轉化為秒) JsonWebToken.Validate(accessToken, apiAccount.AppSecret, (int)AppSettings.TokenTimeout.TotalSeconds); } catch (TokenExpiredException ex) { context.ErrorResult = new AuthenticationFailureResult(StatusCodeManager.GetStatusInfo("B03", "a001"), context.Request); return; } catch (SignatureVerificationException ex) { context.ErrorResult = new AuthenticationFailureResult(StatusCodeManager.GetStatusInfo("B02", "a001"), context.Request); return; } }); //其他驗證邏輯 await AuthenticateHockAsync(context, cancellationToken); } //// <summary> /// 子類中重寫 /// 實現他驗證邏輯 /// </summary> protected abstract Task AuthenticateHockAsync(HttpAuthenticationContext context, System.Threading.CancellationToken cancellationToken); /// <summary> /// 設置principal /// </summary> public Task ChallengeAsync(HttpAuthenticationChallengeContext context, System.Threading.CancellationToken cancellationToken) { return Task.FromResult(0); } public bool AllowMultiple { get { return true; } } }
2 用戶授權
某些數據只有用戶登陸了才能夠獲得,並且不同的用戶對數據的訪問級別也不一樣,為實現登陸驗證與角色控制,采用以下方式。
在上述實現API接入權限驗證的基礎上,為headerJson增加一個字段:loginToken;和accessToken相似,loginToken也是JWT標准字符串,不同的是loginToken的payload部分,loginToken的payload結構為:
{
"identifyingCode": GUID,
"account":userAccount
"timestamp": Unix time
}
其中:identifyingCode值為GUID,account為用戶賬號,timestamp是UNIX時間戳。
客戶端不生成loginToken,在客戶端合成accessToken后,調用服務端的登陸方法,成功登陸后獲得loginToken。
服務端驗證流程
客戶端調用登陸方法的同時,如果登陸成功,服務端會將登陸信息存儲到緩存中,主要的就是loginToken,根據業務需要可以增加其他信息。每一個loginToken對應了一個鍵值,這里使用useAccount,即用戶賬號作為鍵值。服務端獲得loginToken后,根據privateKeyId(headerJson字段之一)獲得privateKey對loginToken解密,根據payload中的timestamp驗證是否過期,然后驗證簽名是否正確,接着根據account找到上次登陸時服務端緩存中存儲的loginToken,比較本次loginToken中的identifyingCode是否與上次一樣,不一樣表明,其在另一台設備登陸過。
單設備登陸:
某些情形下,不允許多設備同時使用同一賬號登陸或多人同時使用同一賬號,上述方法采用loginToken中添加identifyingCode字段來控制多設備同時使用同一賬號的情形。
示例代碼(關鍵性代碼)
public class LoginAuthenticationAttribute : BasicAuthenticationAttribute { protected override async Task AuthenticateHockAsync(HttpAuthenticationContext context, System.Threading.CancellationToken cancellationToken) { await Task.Factory.StartNew(() => { //解析頭信息,獲得appid和timestamp var header = ... //如果未獲得上述信息 if (header == null) { context.ErrorResult = new AuthenticationFailureResult(...); return; } //獲得LoginToken if (String.IsNullOrWhiteSpace(requestHeaderInfo.LoginToken)) { //驗證憑據是空,設置錯誤信息 context.ErrorResult = new AuthenticationFailureResult(StatusCodeManager.GetStatusInfo("B01", "a002"), context.Request); return; } //從loginToken的payload中獲得account,timestamp(時間戳) var payloadDict = JsonWebToken.DecodeToObject(requestHeaderInfo.LoginToken); string identifyingCode = Convert.ToString(payloadDict["identifyingCode"]); string account = Convert.ToString(payloadDict["account"]); string timestamp = Convert.ToString(payloadDict["timestamp"]); //從緩存中獲得LoginToken LoginInfoDAL loginInfoDAL = new LoginInfoDAL(AppSettings.TokenTimeout); LoginCacheModel loginInfo = loginInfoDAL.GetLoginInfo(account); if (loginInfo == null) { context.ErrorResult = new AuthenticationFailureResult(StatusCodeManager.GetStatusInfo("C13", "a002"), context.Request); return; } //比較客戶端傳入LoginToken和緩存中的LoginToken的userId var payloadDictCache = JsonWebToken.DecodeToObject(loginInfo.LoginToken); string identifyingCodeCache = Convert.ToString(payloadDictCache["identifyingCode"]); if (identifyingCodeCache != identifyingCode) {//不相等,提示在另一台設備登陸 context.ErrorResult = new AuthenticationFailureResult(StatusCodeManager.GetStatusInfo("C08", "a002"), context.Request); return; } //得到密鑰 TokenKeyDAL tokenKeyDAL = new TokenKeyDAL(AppSettings.TokenTimeout); string loginTokenKey = tokenKeyDAL.GetTokenKey(account); if (string.IsNullOrWhiteSpace(loginTokenKey)) { context.ErrorResult = new AuthenticationFailureResult(StatusCodeManager.GetStatusInfo("B04","a002"), context.Request); return; } //驗證是否超時,LoginToken是否被篡改 try { //允許的時間段(小時轉化為秒) int allowSpan = (int)AppSettings.TokenTimeout.TotalSeconds; JsonWebToken.Validate(requestHeaderInfo.LoginToken, loginTokenKey, allowSpan); } catch (TokenExpiredException ex) { context.ErrorResult = new AuthenticationFailureResult(StatusCodeManager.GetStatusInfo("B03", "a002"), context.Request); } catch (SignatureVerificationException ex) { context.ErrorResult = new AuthenticationFailureResult(StatusCodeManager.GetStatusInfo("B02", "a002"), context.Request); } }); } }