[開源]快速構建一個WebApi項目


  1. 項目代碼:MasterChief.DotNet.ProjectTemplate.WebApi
  2. 示例代碼:https://github.com/YanZhiwei/MasterChief.ProjectTemplate.WebApiSample
  3. Nuget : Install-Package MasterChief.DotNet.ProjectTemplate.WebApi
  4. 實現WebApi開發中諸如授權驗證,緩存,參數驗證,異常處理等,方便快速構建項目而無需過多關心技術細節;
  5. 歡迎Star,歡迎Issues;

目錄

Created by gh-md-toc

授權

  1. 授權接口,通過該接口自定義授權實現,項目默認實現基於Jwt授權

    /// <summary>
    ///     WebApi 授權接口
    /// </summary>
    public interface IApiAuthorize
    {
        /// <summary>
        ///     檢查請求簽名合法性
        /// </summary>
        /// <param name="signature">加密簽名字符串</param>
        /// <param name="timestamp">時間戳</param>
        /// <param name="nonce">隨機數</param>
        /// <param name="appConfig">應用接入配置信息</param>
        /// <returns>CheckResult</returns>
        CheckResult CheckRequestSignature(string signature, string timestamp, string nonce, AppConfig appConfig);
     
     
        /// <summary>
        ///     創建合法用戶獲取訪問令牌接口數據
        /// </summary>
        /// <param name="identityUser">IdentityUser</param>
        /// <param name="appConfig">AppConfig</param>
        /// <returns>IdentityToken</returns>
        ApiResult<IdentityToken> CreateIdentityToken(IdentityUser identityUser, AppConfig appConfig);
    }
    
  2. 基於Jwt授權實現

    /// <summary>
    ///     基於Jwt 授權實現
    /// </summary>
    public sealed class JwtApiAuthorize : IApiAuthorize
    {
        /// <summary>
        ///     檢查請求簽名合法性
        /// </summary>
        /// <param name="signature">加密簽名字符串</param>
        /// <param name="timestamp">時間戳</param>
        /// <param name="nonce">隨機數</param>
        /// <param name="appConfig">應用接入配置信息</param>
        /// <returns>CheckResult</returns>
        public CheckResult CheckRequestSignature(string signature, string timestamp, string nonce, AppConfig appConfig)
        {
            ValidateOperator.Begin()
                .NotNullOrEmpty(signature, "加密簽名字符串")
                .NotNullOrEmpty(timestamp, "時間戳")
                .NotNullOrEmpty(nonce, "隨機數")
                .NotNull(appConfig, "AppConfig");
            var appSecret = appConfig.AppSecret;
            var signatureExpired = appConfig.SignatureExpiredMinutes;
            string[] data = {appSecret, timestamp, nonce};
            Array.Sort(data);
            var signatureText = string.Join("", data);
            signatureText = Md5Encryptor.Encrypt(signatureText);
     
            if (!signature.CompareIgnoreCase(signatureText) && CheckHelper.IsNumber(timestamp))
                return CheckResult.Success();
            var timestampMillis =
                UnixEpochHelper.DateTimeFromUnixTimestampMillis(timestamp.ToDoubleOrDefault());
            var minutes = DateTime.UtcNow.Subtract(timestampMillis).TotalMinutes;
     
            return minutes > signatureExpired ? CheckResult.Fail("簽名時間戳失效") : CheckResult.Success();
        }
     
        /// <summary>
        ///     創建合法用戶獲取訪問令牌接口數據
        /// </summary>
        /// <param name="identityUser">IdentityUser</param>
        /// <param name="appConfig">AppConfig</param>
        /// <returns>IdentityToken</returns>
        public ApiResult<IdentityToken> CreateIdentityToken(IdentityUser identityUser, AppConfig appConfig)
        {
            ValidateOperator.Begin()
                .NotNull(identityUser, "IdentityUser")
                .NotNull(appConfig, "AppConfig");
            var payload = new Dictionary<string, object>
            {
                {"iss", identityUser.UserId},
                {"iat", UnixEpochHelper.GetCurrentUnixTimestamp().TotalSeconds}
            };
            var identityToken = new IdentityToken
            {
                AccessToken = CreateIdentityToken(appConfig.SharedKey, payload),
                ExpiresIn = appConfig.TokenExpiredDay * 24 * 3600
            };
            return ApiResult<IdentityToken>.Success(identityToken);
        }
     
        /// <summary>
        ///     創建Token
        /// </summary>
        /// <param name="secret">密鑰</param>
        /// <param name="payload">負載數據</param>
        /// <returns>Token令牌</returns>
        public static string CreateIdentityToken(string secret, Dictionary<string, object> payload)
        {
            ValidateOperator.Begin().NotNull(payload, "負載數據").NotNullOrEmpty(secret, "密鑰");
            IJwtAlgorithm algorithm = new HMACSHA256Algorithm();
            IJsonSerializer serializer = new JsonNetSerializer();
            IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder();
            IJwtEncoder encoder = new JwtEncoder(algorithm, serializer, urlEncoder);
            return encoder.Encode(payload, secret);
        }
    }
    

鑒權

  1. Token令牌鑒定接口,通過該接口可以自定義擴展實現方式,項目默認實現基於Jwt鑒權

    /// <summary>
    ///     webApi 驗證系統基本接口
    /// </summary>
    public interface IApiAuthenticate
    {
        #region Methods
     
        /// <summary>
        ///     驗證Token令牌是否合法
        /// </summary>
        /// <param name="token">令牌</param>
        /// <param name="appConfig">AppConfig</param>
        /// <returns>CheckResult</returns>
        ApiResult<string> CheckIdentityToken(string token, AppConfig appConfig);
     
        #endregion Methods
    }
    
  2. 基於Jwt鑒權實現

    /// <summary>
    ///     基於Jwt 授權驗證實現
    /// </summary>
    public sealed class JwtApiAuthenticate : IApiAuthenticate
    {
        /// <summary>
        ///     檢查Token是否合法
        /// </summary>
        /// <param name="token">用戶令牌</param>
        /// <param name="appConfig">AppConfig</param>
        /// <returns></returns>
        public ApiResult<string> CheckIdentityToken(string token, AppConfig appConfig)
        {
            ValidateOperator.Begin()
                .NotNullOrEmpty(token, "Token")
                .NotNull(appConfig, "AppConfig");
            try
            {
                var tokenText = ParseTokens(token, appConfig.SharedKey);
                if (string.IsNullOrEmpty(tokenText))
                    return ApiResult<string>.Fail("用戶令牌Token為空");
     
                dynamic root = JObject.Parse(tokenText);
                string userid = root.iss;
                double iat = root.iat;
                var validTokenExpired =
                    new TimeSpan((int) (UnixEpochHelper.GetCurrentUnixTimestamp().TotalSeconds - iat))
                        .TotalDays > appConfig.TokenExpiredDay;
                return validTokenExpired
                    ? ApiResult<string>.Fail($"用戶ID{userid}令牌失效")
                    : ApiResult<string>.Success(userid);
            }
            catch (FormatException)
            {
                return ApiResult<string>.Fail("用戶令牌非法");
            }
            catch (SignatureVerificationException)
            {
                return ApiResult<string>.Fail("用戶令牌非法");
            }
        }
     
        /// <summary>
        ///     轉換Token
        /// </summary>
        /// <param name="token">令牌</param>
        /// <param name="secret">密鑰</param>
        /// <returns>Token以及負載數據</returns>
        private string ParseTokens(string token, string secret)
        {
            ValidateOperator.Begin()
                .NotNullOrEmpty(token, "令牌")
                .NotNullOrEmpty(secret, "密鑰");
     
            IJsonSerializer serializer = new JsonNetSerializer();
            IDateTimeProvider provider = new UtcDateTimeProvider();
            IJwtValidator validator = new JwtValidator(serializer, provider);
            IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder();
            IJwtDecoder decoder = new JwtDecoder(serializer, validator, urlEncoder);
            return decoder.Decode(token, secret, true);
        }
    }
    

授權與鑒權使用

  1. 授權使用,通過Controller構造函數方式,代碼如下

    /// <summary>
    ///     Api授權
    /// </summary>
    public abstract class AuthorizeController : ApiBaseController
    {
        #region Constructors
     
        /// <summary>
        ///     構造函數
        /// </summary>
        /// <param name="apiAuthorize">IApiAuthorize</param>
        /// <param name="appCfgService">IAppConfigService</param>
        protected AuthorizeController(IApiAuthorize apiAuthorize, IAppConfigService appCfgService)
        {
            ValidateOperator.Begin()
                .NotNull(apiAuthorize, "IApiAuthorize")
                .NotNull(appCfgService, "IAppConfigService");
            ApiAuthorize = apiAuthorize;
            AppCfgService = appCfgService;
        }
     
        #endregion Constructors
     
        #region Fields
     
        /// <summary>
        ///     授權接口
        /// </summary>
        protected readonly IApiAuthorize ApiAuthorize;
     
        /// <summary>
        ///     請求通道配置信息,可以從文件或者數據庫獲取
        /// </summary>
        protected readonly IAppConfigService AppCfgService;
     
        #endregion Fields
     
        #region Methods
     
        /// <summary>
        ///     創建合法用戶的Token
        /// </summary>
        /// <param name="userId">用戶Id</param>
        /// <param name="passWord">用戶密碼</param>
        /// <param name="signature">加密簽名字符串</param>
        /// <param name="timestamp">時間戳</param>
        /// <param name="nonce">隨機數</param>
        /// <param name="appid">應用接入ID</param>
        /// <returns>OperatedResult</returns>
        protected virtual ApiResult<IdentityToken> CreateIdentityToken(string userId, string passWord,
            string signature, string timestamp,
            string nonce, Guid appid)
        {
            #region  參數檢查
     
            var checkResult = CheckRequest(userId, passWord, signature, timestamp, nonce, appid);
     
            if (!checkResult.State)
                return ApiResult<IdentityToken>.Fail(checkResult.Message);
     
            #endregion
     
            #region 用戶鑒權
     
            var getIdentityUser = GetIdentityUser(userId, passWord);
     
            if (!getIdentityUser.State) return ApiResult<IdentityToken>.Fail(getIdentityUser.Message);
     
            #endregion
     
            #region 請求通道檢查
     
            var getAppConfig = AppCfgService.Get(appid);
     
            if (!getAppConfig.State) return ApiResult<IdentityToken>.Fail(getAppConfig.Message);
            var appConfig = getAppConfig.Data;
     
            #endregion
     
            #region 檢查請求簽名檢查
     
            var checkSignatureResult = ApiAuthorize.CheckRequestSignature(signature, timestamp, nonce, appConfig);
            if (!checkSignatureResult.State) return ApiResult<IdentityToken>.Fail(checkSignatureResult.Message);
     
            #endregion
     
            #region 生成基於Jwt Token
     
            var getTokenResult = ApiAuthorize.CreateIdentityToken(getIdentityUser.Data, getAppConfig.Data);
            if (!getTokenResult.State) return ApiResult<IdentityToken>.Fail(getTokenResult.Message);
     
            return ApiResult<IdentityToken>.Success(getTokenResult.Data);
     
            #endregion
        }
     
     
        /// <summary>
        ///     檢查用戶的合法性
        /// </summary>
        /// <param name="userId">用戶Id</param>
        /// <param name="passWord">用戶密碼</param>
        /// <returns>UserInfo</returns>
        protected abstract CheckResult<IdentityUser> GetIdentityUser(string userId, string passWord);
     
        private CheckResult CheckRequest(string userId, string passWord, string signature, string timestamp,
            string nonce, Guid appid)
        {
            if (string.IsNullOrEmpty(userId) || string.IsNullOrEmpty(passWord))
                return CheckResult.Fail("用戶名或密碼為空");
     
            if (string.IsNullOrEmpty(signature))
                return CheckResult.Fail("請求簽名為空");
     
            if (string.IsNullOrEmpty(timestamp))
                return CheckResult.Fail("時間戳為空");
     
            if (string.IsNullOrEmpty(nonce))
                return CheckResult.Fail("隨機數為空");
     
            if (appid == Guid.Empty)
                return CheckResult.Fail("應用接入ID非法");
     
            return CheckResult.Success();
        }
     
        #endregion Methods
    }
    
  2. 鑒權使用,通過AuthorizationFilterAttribute形式,標注請求是否需要鑒權

    /// <summary>
     ///     WebApi 授權驗證實現
     /// </summary>
     [AttributeUsage(AttributeTargets.Method)]
     public abstract class AuthenticateAttribute : AuthorizationFilterAttribute
     {
         #region Constructors
        
         /// <summary>
         ///     構造函數
         /// </summary>
         /// <param name="apiAuthenticate">IApiAuthenticate</param>
         /// <param name="appCfgService">appCfgService</param>
         protected AuthenticateAttribute(IApiAuthenticate apiAuthenticate, IAppConfigService appCfgService)
         {
             ValidateOperator.Begin()
                 .NotNull(apiAuthenticate, "IApiAuthenticate")
                 .NotNull(appCfgService, "IAppConfigService");
             ApiAuthenticate = apiAuthenticate;
             AppCfgService = appCfgService;
         }
     
         #endregion Constructors
     
         #region Fields
     
         /// <summary>
         ///     授權驗證接口
         /// </summary>
         protected readonly IApiAuthenticate ApiAuthenticate;
     
         /// <summary>
         ///     請求通道配置信息,可以從文件或者數據庫獲取
         /// </summary>
         protected readonly IAppConfigService AppCfgService;
     
         #endregion Fields
     
         #region Methods
     
         /// <summary>
         ///     驗證Token令牌是否合法
         /// </summary>
         /// <param name="token">令牌</param>
         /// <param name="appid">應用ID</param>
         /// <returns>CheckResult</returns>
         protected virtual ApiResult<string> CheckIdentityToken(string token, Guid appid)
         {
             #region 請求參數檢查
     
             var checkResult = CheckRequest(token, appid);
     
             if (!checkResult.State)
                 return ApiResult<string>.Fail(checkResult.Message);
     
             #endregion
     
             #region 請求通道檢查
     
             var getAppConfig = AppCfgService.Get(appid);
     
             if (!getAppConfig.State) return ApiResult<string>.Fail(getAppConfig.Message);
             var appConfig = getAppConfig.Data;
     
             #endregion
     
             return ApiAuthenticate.CheckIdentityToken(token, appConfig);
         }
     
         private CheckResult CheckRequest(string token, Guid appid)
         {
             if (string.IsNullOrEmpty(token))
                 return CheckResult.Fail("用戶令牌為空");
             return Guid.Empty == appid ? CheckResult.Fail("應用ID非法") : CheckResult.Success();
         }
     
         #endregion Methods
     }
    

基於請求緩存處理

  1. 通過ICacheProvider接口,可以擴展緩存數據方式;

  2. 通過配置DependsOnIdentity參數,可以配置是否依賴Token令牌進行緩存;

  3. 通過配置CacheMinutes參數,可以指定具體接口緩存時間,當設置0的時候不啟用緩存;

  4. 通過實現ControllerCacheAttribute,可以在不同項目快速達到接口緩存功能;

    public class RequestCacheAttribute : ControllerCacheAttribute
    {
        public RequestCacheAttribute(int cacheMinutes) : this(cacheMinutes, true, new LocalCacheProvider())
        {
        }
     
        public RequestCacheAttribute(int cacheMinutes, bool dependsOnIdentity, ICacheProvider cacheProvider) : base(
            cacheMinutes, dependsOnIdentity, cacheProvider)
        {
        }
     
        protected override bool CheckedResponseAvailable(HttpActionContext context, string responseText)
        {
            return !string.IsNullOrEmpty(responseText) && context != null;
        }
     
        protected override string GetIdentityToken(HttpActionContext actionContext)
        {
            return actionContext.Request.GetUriOrHeaderValue("Access_token").ToStringOrDefault(string.Empty);
        }
    }
    

異常處理

  1. 通過實現ControllerExceptionAttribute,可以輕松簡單構建接口請求時候異常發生,並通過HttpRequestRaw requestRaw參數,可以獲取非常詳盡的請求信息;

    public sealed class ExceptionLogAttribute : ControllerExceptionAttribute
    {
        public override void OnActionExceptioning(HttpActionExecutedContext actionExecutedContext, string actionName,
            HttpStatusCode statusCode,
            HttpRequestRaw requestRaw)
        {
            var response = new HttpResponseMessage
            {
                Content = new StringContent("發生故障,請稍后重試!"),
                StatusCode = statusCode
            };
            actionExecutedContext.Response = response;
        }
    }
    

參數驗證

  1. 通過實現ValidateModelAttribute,以及DataAnnotations快速構建請求參數驗證

  2. 請求參數只需要DataAnnotations標注即可;

    public sealed class ArticleRequest
    {
        [Required(ErrorMessage = "缺少文章ID")]
        public int Id
        {
            get;
            set;
        }
     
    }
    
  3. 項目實現ValidateModelAttribute,可以自定義構建參數處理方式

    /// <summary>
    /// 請求參數
    /// </summary>
    public sealed class ValidateRequestAttribute : ValidateModelAttribute
    {
        public override void OnParameterIsNulling(HttpActionContext actionContext)
        {
            actionContext.Response =
                actionContext.Request.CreateResponse(HttpStatusCode.BadRequest, OperatedResult<string>.Fail("請求參數非法。"));
        }
     
        public override void OnParameterInvaliding(HttpActionContext actionContext, ValidationFailedResult result)
        {
            var message = result.Data.FirstOrDefault()?.Message;
            actionContext.Response =
                actionContext.Request.CreateResponse(HttpStatusCode.BadRequest, OperatedResult<string>.Fail(message));
        }
    }
    


免責聲明!

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



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