[开源]WebApi 快速构建示例

2021-06-09 20:05

阅读:655

标签:failed   ict   password   tap   validate   info   resin   gui   sealed   

  1. 实现代码:MasterChief.DotNet.ProjectTemplate.WebApi
  2. Demo Code:https://github.com/YanZhiwei/MasterChief.ProjectTemplate.WebApiSample
  3. Nuget : Install-Package MasterChief.DotNet.ProjectTemplate.WebApi
  4. 实现WebApi开发中诸如授权验证,缓存,参数验证,异常处理等,方便快速构建项目而无需过多关心技术细节;
  5. 欢迎Star,欢迎PR;

目录

  • 授权
  • 鉴权
  • 授权与鉴权使用
  • 基于请求缓存处理
  • 异常处理
  • 参数验证

Created by gh-md-toc

授权

  1. 授权接口,通过该接口自定义授权实现,项目默认实现基于Jwt授权

    /// 
    ///     WebApi 授权接口
    /// 
    public interface IApiAuthorize
    {
        /// 
        ///     检查请求签名合法性
        /// 
        /// 加密签名字符串
        /// 时间戳
        /// 随机数
        /// 应用接入配置信息
        /// CheckResult
        CheckResult CheckRequestSignature(string signature, string timestamp, string nonce, AppConfig appConfig);
    
    
        /// 
        ///     创建合法用户获取访问令牌接口数据
        /// 
        /// IdentityUser
        /// AppConfig
        /// IdentityToken
        ApiResult CreateIdentityToken(IdentityUser identityUser, AppConfig appConfig);
    }
  2. 基于Jwt授权实现

    /// 
    ///     基于Jwt 授权实现
    /// 
    public sealed class JwtApiAuthorize : IApiAuthorize
    {
        /// 
        ///     检查请求签名合法性
        /// 
        /// 加密签名字符串
        /// 时间戳
        /// 随机数
        /// 应用接入配置信息
        /// CheckResult
        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();
        }
    
        /// 
        ///     创建合法用户获取访问令牌接口数据
        /// 
        /// IdentityUser
        /// AppConfig
        /// IdentityToken
        public ApiResult CreateIdentityToken(IdentityUser identityUser, AppConfig appConfig)
        {
            ValidateOperator.Begin()
                .NotNull(identityUser, "IdentityUser")
                .NotNull(appConfig, "AppConfig");
            var payload = new Dictionary
            {
                {"iss", identityUser.UserId},
                {"iat", UnixEpochHelper.GetCurrentUnixTimestamp().TotalSeconds}
            };
            var identityToken = new IdentityToken
            {
                AccessToken = CreateIdentityToken(appConfig.SharedKey, payload),
                ExpiresIn = appConfig.TokenExpiredDay * 24 * 3600
            };
            return ApiResult.Success(identityToken);
        }
    
        /// 
        ///     创建Token
        /// 
        /// 密钥
        /// 负载数据
        /// Token令牌
        public static string CreateIdentityToken(string secret, Dictionary 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鉴权

    /// 
    ///     webApi 验证系统基本接口
    /// 
    public interface IApiAuthenticate
    {
        #region Methods
    
        /// 
        ///     验证Token令牌是否合法
        /// 
        /// 令牌
        /// AppConfig
        /// CheckResult
        ApiResult CheckIdentityToken(string token, AppConfig appConfig);
    
        #endregion Methods
    }
  2. 基于Jwt鉴权实现

    /// 
    ///     基于Jwt 授权验证实现
    /// 
    public sealed class JwtApiAuthenticate : IApiAuthenticate
    {
        /// 
        ///     检查Token是否合法
        /// 
        /// 用户令牌
        /// AppConfig
        /// 
        public ApiResult 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.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.Fail($"用户ID{userid}令牌失效")
                    : ApiResult.Success(userid);
            }
            catch (FormatException)
            {
                return ApiResult.Fail("用户令牌非法");
            }
            catch (SignatureVerificationException)
            {
                return ApiResult.Fail("用户令牌非法");
            }
        }
    
        /// 
        ///     转换Token
        /// 
        /// 令牌
        /// 密钥
        /// Token以及负载数据
        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构造函数方式,代码如下

    /// 
    ///     Api授权
    /// 
    public abstract class AuthorizeController : ApiBaseController
    {
        #region Constructors
    
        /// 
        ///     构造函数
        /// 
        /// IApiAuthorize
        /// IAppConfigService
        protected AuthorizeController(IApiAuthorize apiAuthorize, IAppConfigService appCfgService)
        {
            ValidateOperator.Begin()
                .NotNull(apiAuthorize, "IApiAuthorize")
                .NotNull(appCfgService, "IAppConfigService");
            ApiAuthorize = apiAuthorize;
            AppCfgService = appCfgService;
        }
    
        #endregion Constructors
    
        #region Fields
    
        /// 
        ///     授权接口
        /// 
        protected readonly IApiAuthorize ApiAuthorize;
    
        /// 
        ///     请求通道配置信息,可以从文件或者数据库获取
        /// 
        protected readonly IAppConfigService AppCfgService;
    
        #endregion Fields
    
        #region Methods
    
        /// 
        ///     创建合法用户的Token
        /// 
        /// 用户Id
        /// 用户密码
        /// 加密签名字符串
        /// 时间戳
        /// 随机数
        /// 应用接入ID
        /// OperatedResult
        protected virtual ApiResult 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.Fail(checkResult.Message);
    
            #endregion
    
            #region 用户鉴权
    
            var getIdentityUser = GetIdentityUser(userId, passWord);
    
            if (!getIdentityUser.State) return ApiResult.Fail(getIdentityUser.Message);
    
            #endregion
    
            #region 请求通道检查
    
            var getAppConfig = AppCfgService.Get(appid);
    
            if (!getAppConfig.State) return ApiResult.Fail(getAppConfig.Message);
            var appConfig = getAppConfig.Data;
    
            #endregion
    
            #region 检查请求签名检查
    
            var checkSignatureResult = ApiAuthorize.CheckRequestSignature(signature, timestamp, nonce, appConfig);
            if (!checkSignatureResult.State) return ApiResult.Fail(checkSignatureResult.Message);
    
            #endregion
    
            #region 生成基于Jwt Token
    
            var getTokenResult = ApiAuthorize.CreateIdentityToken(getIdentityUser.Data, getAppConfig.Data);
            if (!getTokenResult.State) return ApiResult.Fail(getTokenResult.Message);
    
            return ApiResult.Success(getTokenResult.Data);
    
            #endregion
        }
    
    
        /// 
        ///     检查用户的合法性
        /// 
        /// 用户Id
        /// 用户密码
        /// UserInfo
        protected abstract CheckResult 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形式,标注请求是否需要鉴权

    /// 
     ///     WebApi 授权验证实现
     /// 
     [AttributeUsage(AttributeTargets.Method)]
     public abstract class AuthenticateAttribute : AuthorizationFilterAttribute
     {
         #region Constructors
    
         /// 
         ///     构造函数
         /// 
         /// IApiAuthenticate
         /// appCfgService
         protected AuthenticateAttribute(IApiAuthenticate apiAuthenticate, IAppConfigService appCfgService)
         {
             ValidateOperator.Begin()
                 .NotNull(apiAuthenticate, "IApiAuthenticate")
                 .NotNull(appCfgService, "IAppConfigService");
             ApiAuthenticate = apiAuthenticate;
             AppCfgService = appCfgService;
         }
    
         #endregion Constructors
    
         #region Fields
    
         /// 
         ///     授权验证接口
         /// 
         protected readonly IApiAuthenticate ApiAuthenticate;
    
         /// 
         ///     请求通道配置信息,可以从文件或者数据库获取
         /// 
         protected readonly IAppConfigService AppCfgService;
    
         #endregion Fields
    
         #region Methods
    
         /// 
         ///     验证Token令牌是否合法
         /// 
         /// 令牌
         /// 应用ID
         /// CheckResult
         protected virtual ApiResult CheckIdentityToken(string token, Guid appid)
         {
             #region 请求参数检查
    
             var checkResult = CheckRequest(token, appid);
    
             if (!checkResult.State)
                 return ApiResult.Fail(checkResult.Message);
    
             #endregion
    
             #region 请求通道检查
    
             var getAppConfig = AppCfgService.Get(appid);
    
             if (!getAppConfig.State) return ApiResult.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,可以自定义构建参数处理方式

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

[开源]WebApi 快速构建示例

标签:failed   ict   password   tap   validate   info   resin   gui   sealed   

原文地址:https://www.cnblogs.com/MeetYan/p/WebApi.html


评论


亲,登录后才可以留言!