1. 程式人生 > >ASP.NET Core中使用自定義驗證屬性控制訪問許可權

ASP.NET Core中使用自定義驗證屬性控制訪問許可權

在應用中,有時我們需要對訪問的客戶端進行有效性驗證,只有提供有效憑證(AccessToken)的終端應用能訪問我們的受控站點(如WebAPI站點),此時我們可以通過驗證屬性的方法來解決。

一、public class Startup的配置:

//啟用跨域訪問(不同埠也是跨域) services.AddCors(options => { options.AddPolicy("AllowOriginOtherBis", builder => builder.WithOrigins("https://1.16.9.12:4432", "https://pc12.ato.biz:4432", "https://localhost:44384", "https://1.16.9.12:4432", "https://pc12.ato.biz:4432").AllowAnyMethod().AllowAnyHeader()); });

//啟用自定義屬性以便對控制器或Action進行[TerminalApp()]定義。

services.AddSingleton<IAuthorizationHandler, TerminalAppAuthorizationHandler>(); services.AddAuthorization(options => { options.AddPolicy("TerminalApp", policyBuilder => { policyBuilder.Requirements.Add(new TerminalAppAuthorizationRequirement()); }); });

二、public void Configure(IApplicationBuilder app, IHostingEnvironment env)中的配置:

app.UseHttpsRedirection();  //使用Https傳輸 app.UseCors("AllowOriginOtherBis"); //根據定義啟用跨域設定

三、示例WebApi專案結構:

四、主要程式碼(我採用的從資料庫進行驗證):

    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true
)] internal class TerminalAppAttribute : AuthorizeAttribute { public string AppID { get; } /// <summary> /// 指定客戶端訪問API /// </summary> /// <param name="appID"></param> public TerminalAppAttribute(string appID="") : base("TerminalApp") { AppID = appID; } }
TerminalAppAttribute.cs
    public abstract class AttributeAuthorizationHandler<TRequirement, TAttribute> : AuthorizationHandler<TRequirement> where TRequirement : IAuthorizationRequirement where TAttribute : Attribute
    {
        protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, TRequirement requirement)
        {
            var attributes = new List<TAttribute>();

            if ((context.Resource as AuthorizationFilterContext)?.ActionDescriptor is ControllerActionDescriptor action)
            {
                attributes.AddRange(GetAttributes(action.ControllerTypeInfo.UnderlyingSystemType));
                attributes.AddRange(GetAttributes(action.MethodInfo));
            }

            return HandleRequirementAsync(context, requirement, attributes);
        }

        protected abstract Task HandleRequirementAsync(AuthorizationHandlerContext context, TRequirement requirement, IEnumerable<TAttribute> attributes);

        private static IEnumerable<TAttribute> GetAttributes(MemberInfo memberInfo)
        {
            return memberInfo.GetCustomAttributes(typeof(TAttribute), false).Cast<TAttribute>();
        }
    }

    internal class TerminalAppAuthorizationHandler : AttributeAuthorizationHandler<TerminalAppAuthorizationRequirement,TerminalAppAttribute>
    {
        protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, TerminalAppAuthorizationRequirement requirement, IEnumerable<TerminalAppAttribute> attributes)
        {
            object errorMsg = string.Empty;
            //如果取不到身份驗證資訊,並且不允許匿名訪問,則返回未驗證403
            if (context.Resource is AuthorizationFilterContext filterContext &&
filterContext.ActionDescriptor is ControllerActionDescriptor descriptor)
            {
                //先判斷是否是匿名訪問,
                if (descriptor != null)
                {
                    var actionAttributes = descriptor.MethodInfo.GetCustomAttributes(inherit: true);
                    bool isAnonymous = actionAttributes.Any(a => a is AllowAnonymousAttribute);
                    //非匿名的方法,連結中新增accesstoken值
                    if (isAnonymous)
                    {
                        context.Succeed(requirement);
                        return Task.CompletedTask;
                    }
                    else
                    {
                        //url獲取access_token
                        //從AuthorizationHandlerContext轉成HttpContext,以便取出表求資訊
                        var httpContext = (context.Resource as AuthorizationFilterContext).HttpContext;
                        //var questUrl = httpContext.Request.Path.Value.ToLower();
                        string requestAppID = httpContext.Request.Headers["appid"];
                        string requestAccessToken = httpContext.Request.Headers["access_token"];
                        if ((!string.IsNullOrEmpty(requestAppID)) && (!string.IsNullOrEmpty(requestAccessToken)))
                        {
                            if (attributes != null)
                            {
                                //當不指定具體的客戶端AppID僅運用驗證屬性時預設所有客戶端都接受
                                if (attributes.ToArray().ToString()=="") 
                                {
                                    //任意一個在資料庫列表中的App都可以執行,否則先判斷提交的APPID與需要ID是否相符
                                    bool mat = false;
                                    foreach (var terminalAppAttribute in attributes)
                                    {
                                        if (terminalAppAttribute.AppID == requestAppID)
                                        {
                                            mat = true;
                                            break;
                                        }
                                    }
                                    if (!mat)
                                    {
                                        errorMsg = ReturnStd.NotAuthorize("客戶端應用未在服務端登記或未被授權運用當前功能.");
                                        return HandleBlockedAsync(context, requirement, errorMsg);
                                    }
                                }
                            }

                            //如果未指定attributes,則表示任何一個終端服務都可以呼叫服務, 在驗證區域驗證終端提供的ID是否匹配資料庫記錄
                            string valRst = ValidateToken(requestAppID, requestAccessToken);
                            if (string.IsNullOrEmpty(valRst))
                            {
                                context.Succeed(requirement);
                                return Task.CompletedTask;
                            }
                            else
                            {
                                errorMsg = ReturnStd.NotAuthorize("AccessToken驗證失敗(" + valRst + ")","91");
                                return HandleBlockedAsync(context, requirement, errorMsg);
                            }
                        }
                        else
                        {
                            errorMsg = ReturnStd.NotAuthorize("未提供AppID或Token."); 
                            return HandleBlockedAsync(context, requirement, errorMsg);
                            //return Task.CompletedTask;
                        }
                    }
                }
            }
            else
            {
                errorMsg = ReturnStd.NotAuthorize("FilterContext型別不匹配.");
                return HandleBlockedAsync(context, requirement, errorMsg);
            }

            errorMsg = ReturnStd.NotAuthorize("未知錯誤.");
            return HandleBlockedAsync(context,requirement, errorMsg);
        }


        //校驗票據(資料庫資料匹配)
        /// <summary>
        /// 驗證終端服務程式提供的AccessToken是否合法
        /// </summary>
        /// <param name="appID">終端APP的ID</param>
        /// <param name="accessToken">終端APP利用其自身AppKEY運算出來的AccessToken,與伺服器生成的進行比對</param>
        /// <returns></returns>
        private string ValidateToken(string appID,string accessToken)
        {
            try
            {
                DBContextMain dBContext = new DBContextMain();
                string appKeyOnServer = string.Empty;
                //從資料庫讀取AppID對應的KEY(此KEY為加解密演算法的AES_KEY
                AuthApp authApp = dBContext.AuthApps.FirstOrDefault(a => a.AppID == appID);
                if (authApp == null)
                {
                    return "客戶端應用沒有在雲端登記!";
                }
                else
                {
                    appKeyOnServer = authApp.APPKey;
                }
                if (string.IsNullOrEmpty(appKeyOnServer))
                {
                    return "客戶端應用基礎資訊有誤!"; 
                }

                string tmpToken = string.Empty;
                tmpToken = System.Net.WebUtility.UrlDecode(accessToken);//解碼相應的Token到原始字元(因其中可能會有+=等特殊字元,必須編碼後傳遞)
                tmpToken = OCrypto.AES16Decrypt(tmpToken, appKeyOnServer); //使用APPKEY解密並分析

                if (string.IsNullOrEmpty(tmpToken))
                {
                    return "客戶端提交的身份令牌運算為空!";
                }
                else
                {
                    try
                    {
                        //原始驗證碼為im_cloud_sv001-appid-ticks格式
                        //取出時間,與伺服器時間對比,超過10秒即拒絕服務
                        long tmpTime =Convert.ToInt64(tmpToken.Substring(tmpToken.LastIndexOf("-")+1));
                        //DateTime dt = DateTime.ParseExact(tmpTime, "yyyyMMddHHmmss", CultureInfo.CurrentCulture);
                        DateTime dt= new DateTime(tmpTime);
                        bool IsInTimeSpan = (Convert.ToDouble(ODateTime.DateDiffSeconds(dt, DateTime.Now)) <= 7200);
                        bool IsInternalApp = (tmpToken.IndexOf("im_cloud_sv001-") >= 0);
                        if (!IsInternalApp || !IsInTimeSpan)
                        {
                            return "令牌未被許可或已經失效!";
                        }
                        else
                        {
                            return string.Empty; //成功驗證
                        }
                    }
                    catch (Exception ex)
                    {
                        return "令牌解析出錯(" + ex.Message + ")";
                    }

                }
            }
            catch (Exception ex)
            {
                return "令牌解析出錯(" + ex.Message + ")";
            }
        }

        private Task HandleBlockedAsync(AuthorizationHandlerContext context, TerminalAppAuthorizationRequirement requirement, object errorMsg)
        {
            var authorizationFilterContext = context.Resource as AuthorizationFilterContext;
            authorizationFilterContext.Result = new JsonResult(errorMsg) { StatusCode = 202 };
            //設定為403會顯示不了自定義資訊,改為Accepted202,由客戶端處理
            context.Succeed(requirement);
            return Task.CompletedTask;
        }
    }
TerminalAppAuthorizationHandler.cs
    internal class TerminalAppAuthorizationRequirement : IAuthorizationRequirement
    {
        public TerminalAppAuthorizationRequirement()
        {
        }
    }
TerminalAppAuthorizationRequirement.cs

五、相應的Token驗證程式碼:

    [AutoValidateAntiforgeryToken]  //在本控制器內自動啟用跨站攻擊防護
    [Route("api/get_accesstoken")]
    public class GetAccessTokenController : Controller
    {
        //尚未限制訪問頻率
        //返回{"access_token":"ACCESS_TOKEN","expires_in":7200} 有效期2個小時
        //錯誤時返回{"errcode":40013,"errmsg":"invalid appid"}
        [AllowAnonymous]
        public ActionResult<string> Get()
        {
            try
            {
                string tmpToken = string.Empty;

                string appID = HttpContext.Request.Headers["appid"];
                string appKey = HttpContext.Request.Headers["appkey"];

                if ((appID.Length < 5) || appKey.Length != 32)
                {
                    return "{'errcode':10000,'errmsg':'appid或appkey未提供'}";
                }
                //token採用im_cloud_sv001-appid-ticks數字
                long timeTk = DateTime.Now.Ticks; //輸出毫微秒:633603924670937500
                                                  //DateTime dt = new DateTime(timeTk);//可以還原時間

                string plToken = "im_cloud1-" + appID + "-" + timeTk;
                tmpToken = OCrypto.AES16Encrypt(plToken, appKey); //使用APPKEY加密

                tmpToken = System.Net.WebUtility.UrlEncode(tmpToken);
                //編碼相應的Token(因其中可能會有+=等特殊字元,必須編碼後傳遞)
                tmpToken = "{'access_token':'" + tmpToken + "','expires_in':7200}";
                return tmpToken;
            }
            catch (Exception ex)
            {
                return "{'errcode':10001,'errmsg':'" + ex.Message +"'}";
            }
        }
    }
GetAccessTokenController.cs

六、這樣,在我們需要控制的地方加上

[TerminalApp()] 即可,這樣所有授權的App都能訪問,當然,也可以使用[TerminalApp(“app01”)]限定某一個ID為app01的應用訪問。
    [Area("SYS")]        // 路由: api/sys/user
    [Produces("application/json")]
    [TerminalApp()]  
    public class UserController : Controller
{
//
}