ASP.NET Web API編程——接口安全與角色控制


1 API接口驗證與授權

JWT

JWT定義,它包含三部分:headerpayloadsignature;每一部分都是使用Base64編碼的JSON字符串。之間以句號分隔。signature”header.payload”經加密后的字符串。

采用JWT實現驗證與授權檢驗機制,JWT格式為:

header

{

  "typ": "JWT",

  "alg": "HS256"

}

payloadappid為GUID,timestampunix時間戳

{

    "appid": GUID,

    "timestamp": Unix time

}

Signature使用HS256HMAC SHA-256,SHA Secure Hash Algorithm,安全散列算法headerpayload‘.’連接的字符串進行簽名。

 

JWT加密:采用RSA加密算法對其進行加密。

密鑰發放

發放給客戶端的參數:appIdappSecretpublicKeyprivateKeyId。其中publicKeyRSA公鑰,privateKeyId為服務端私鑰Id。服務端或根據privateKeyId在緩存(本地或Redis等)中查找RSA私鑰。

合成accessToken:header、payload與上述相同,簽名密鑰為appSecret。合成以后,使用publicKey對其進行加密。

合成headerJson:accessToken和privateKeyId構成的Json字符串,然后將字符串用Base64編碼方式編碼。

 

驗證流程

客戶端將上述headerB64放入請求頭,向服務端發起請求,服務端從請求頭中拿到headerJson並解碼headerJson,進而從中得到accessTokenprivateKeyId,服務端根據privateKeyId找到privateKey,使用privateKeyaccessToken解密,根據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為用戶賬號,timestampUNIX時間戳。

客戶端不生成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);
                }
            });
        }
    }

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM