1. 實現代碼: " MasterChief.DotNet.ProjectTemplate.WebApi " 2. Demo Code: 3. Nuget : Install Package MasterChief.DotNet.ProjectTemplate.WebApi 4. 實現WebApi開 ...
- 項目代碼:MasterChief.DotNet.ProjectTemplate.WebApi
- 示例代碼:https://github.com/YanZhiwei/MasterChief.ProjectTemplate.WebApiSample
- Nuget : Install-Package MasterChief.DotNet.ProjectTemplate.WebApi
- 實現WebApi開發中諸如授權驗證,緩存,參數驗證,異常處理等,方便快速構建項目而無需過多關心技術細節;
- 歡迎Star,歡迎PR;
目錄
Created by gh-md-toc
授權
授權介面,通過該介面自定義授權實現,項目預設實現基於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); }
基於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); } }
鑒權
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 }
基於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); } }
授權與鑒權使用
授權使用,通過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 }
鑒權使用,通過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 }
基於請求緩存處理
通過ICacheProvider介面,可以擴展緩存數據方式;
通過配置DependsOnIdentity參數,可以配置是否依賴Token令牌進行緩存;
通過配置CacheMinutes參數,可以指定具體介面緩存時間,當設置0的時候不啟用緩存;
通過實現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); } }
異常處理
通過實現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; } }
參數驗證
通過實現ValidateModelAttribute,以及DataAnnotations快速構建請求參數驗證
請求參數只需要DataAnnotations標註即可;
public sealed class ArticleRequest { [Required(ErrorMessage = "缺少文章ID")] public int Id { get; set; } }
項目實現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)); } }