.NET WebAPI ActionFilterAttributeでtokenトークン検証とActionに対する権限制御を実現

27322 ワード

プロジェクトの背景は1つのコミュニティ類のAPP(軽く吐くことを求めます...)で、ブロガーは主にバックグラウンドの業務とインタフェースを担当します.以前はwebAPIをしたことがありませんが、リーダーはこれを使わなければならないと要求し(具体的な原因は知っています)、無理に頭を上げました.
 
最近権限を作ったばかりで、みんなに分かち合います.いろいろなツッコミ批判を歓迎します...
 
まずユーザーのアイデンティティの識別について話して、簡単にtokenメカニズムを作りました.ユーザーがログインし、バックグラウンドでトークンを生成し、トークンを発行し、ユーザーがトークンを持ってアクセスする...
1.cache管理クラスは、ブロガーが使用しているHttpRuntime.Cacheがtokenを格納しているため、IISが再起動したり、予期せぬシャットダウンをしたりすることでcacheがクリアされるため、データベースでcacheのバックアップを行うしかなく、cacheが空の場合はデータベースにcacheデータがあるかどうかを照会し、cacheが意図的にクリアされている場合は、cacheに再配置する必要があります.
    /// <summary>
    ///     
    ///    、                  Cache 
    /// </summary>
    public class CacheManager
    {
private static readonly ITempCacheService tempCacheService = ServiceLocator.Instance.GetService<ITempCacheService>(); /// <summary> /// /// </summary> /// token /// uuid ID /// userType /// timeout /// <remarks> /// </remarks> private static void CacheInit() { if (HttpRuntime.Cache["PASSPORT.TOKEN"] == null) { DataTable dt = new DataTable(); dt.Columns.Add("token", Type.GetType("System.String")); dt.Columns["token"].Unique = true; dt.Columns.Add("uuid", Type.GetType("System.Object")); dt.Columns["uuid"].DefaultValue = null; dt.Columns.Add("userType", Type.GetType("System.String")); dt.Columns["userType"].DefaultValue = null; dt.Columns.Add("timeout", Type.GetType("System.DateTime")); dt.Columns["timeout"].DefaultValue = DateTime.Now.AddDays(7); DataColumn[] keys = new DataColumn[1]; keys[0] = dt.Columns["token"]; dt.PrimaryKey = keys; var tempCaches = tempCacheService.GetAllCaches(); if (tempCaches.Any()) { foreach (var tempCacheDTOShow in tempCaches) { DataRow dr = dt.NewRow(); dr["token"] = tempCacheDTOShow.UserToken; dr["uuid"] = tempCacheDTOShow.UserAccountId; dr["userType"] = tempCacheDTOShow.UserType.ToString(); dr["timeout"] = tempCacheDTOShow.EndTime; dt.Rows.Add(dr); } } //Cache *2 HttpRuntime.Cache.Insert("PASSPORT.TOKEN", dt, null, DateTime.MaxValue, TimeSpan.FromDays(7 * 2)); } } /// <summary> /// UUID /// </summary> /// <param name="token"></param> /// <returns></returns> public static Guid GetUUID(string token) { CacheInit(); DataTable dt = (DataTable)HttpRuntime.Cache["PASSPORT.TOKEN"]; DataRow[] dr = dt.Select("token = '" + token + "'"); if (dr.Length > 0) { return new Guid(dr[0]["uuid"].ToString()); } return Guid.Empty; } /// <summary> /// ( 、 、 、 , ) /// </summary> /// <param name="token"></param> /// <returns></returns> public static string GetUserType(string token) { CacheInit(); DataTable dt = (DataTable)HttpRuntime.Cache["PASSPORT.TOKEN"]; DataRow[] dr = dt.Select("token = '" + token + "'"); if (dr.Length > 0) { return dr[0]["userType"].ToString(); } return null; } /// <summary> /// /// </summary> /// <param name="token"> </param> /// <returns></returns> public static bool TokenIsExist(string token) { CacheInit(); DataTable dt = (DataTable)HttpRuntime.Cache["PASSPORT.TOKEN"]; DataRow[] dr = dt.Select("token = '" + token + "'"); if (dr.Length > 0) { var timeout = DateTime.Parse(dr[0]["timeout"].ToString()); if (timeout > DateTime.Now) { return true; } else { RemoveToken(token); return false; } } return false; } /// <summary> /// /// </summary> /// <param name="token"></param> /// <returns></returns> public static bool RemoveToken(string token) { CacheInit(); DataTable dt = (DataTable)HttpRuntime.Cache["PASSPORT.TOKEN"]; DataRow[] dr = dt.Select("token = '" + token + "'"); if (dr.Length > 0) { dt.Rows.Remove(dr[0]); } return true; } /// <summary> /// /// </summary> /// <param name="token"> </param> /// <param name="time"> </param> public static void TokenTimeUpdate(string token, DateTime time) { CacheInit(); DataTable dt = (DataTable)HttpRuntime.Cache["PASSPORT.TOKEN"]; DataRow[] dr = dt.Select("token = '" + token + "'"); if (dr.Length > 0) { dr[0]["timeout"] = time; } } /// <summary> /// /// </summary> /// <param name="token"> </param> /// <param name="uuid"> ID </param> /// <param name="userType"> </param> /// <param name="timeout"> </param> public static void TokenInsert(string token, object uuid, string userType, DateTime timeout) { CacheInit(); // token if (!TokenIsExist(token)) { DataTable dt = (DataTable)HttpRuntime.Cache["PASSPORT.TOKEN"]; DataRow dr = dt.NewRow(); dr["token"] = token; dr["uuid"] = uuid; dr["userType"] = userType; dr["timeout"] = timeout; dt.Rows.Add(dr); HttpRuntime.Cache["PASSPORT.TOKEN"] = dt; tempCacheService.Add_TempCaches(new List<TempCacheDTO_ADD>() { new TempCacheDTO_ADD() { EndTime = timeout, UserAccountId = new Guid(uuid.ToString()), UserToken = new Guid(token), UserType = (UserType)Enum.Parse(typeof(UserType),userType) } }); } // token else { TokenTimeUpdate(token, timeout); tempCacheService.Update_TempCaches(new Guid(token), timeout); } } }
2.次にユーザが携帯しているtokenを検証し、ActionFilterAttributeを継承することで実現したが、ここでは匿名アクセスAPIも考慮する必要があり、一部のAPIでは匿名アクセス(ログインしないアクセス)が許可されている.匿名を表すAttributeを書きます
    /// <summary>
    ///       
    /// </summary>
    [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
    public class AnonymousAttribute : Attribute
    {
    }
匿名アクセスが許可されているアクションに[Anonymous]ラベルを付けるとOKです.token検証コードを見てみましょう.
    /// <summary>
    ///       /// </summary>
    public class TokenProjectorAttribute : ActionFilterAttribute
    {
        private const string UserToken = "token";
        private readonly IAccountInfoService accountInfoService = ServiceLocator.Instance.GetService<IAccountInfoService>();
        private readonly ITempCacheService tempCacheService = ServiceLocator.Instance.GetService<ITempCacheService>();

        public override void OnActionExecuting(HttpActionContext actionContext)
        {
            //       
            var anonymousAction = actionContext.ActionDescriptor.GetCustomAttributes<AnonymousAttribute>();
            if (!anonymousAction.Any())
            {
                //   token
                var token = TokenVerification(actionContext);
            }

            base.OnActionExecuting(actionContext);
        }

        /// <summary>
        ///       
        /// </summary>
        /// <param name="actionContext"></param>
        protected virtual string TokenVerification(HttpActionContext actionContext)
        {
            //   token
            var token = GetToken(actionContext.ActionArguments, actionContext.Request.Method);

            //   token    
            if (!CacheManager.TokenIsExist(token))
            {
                throw new UserLoginException("Token   ,     !");
            }

            //          
            if (accountInfoService.Exist_User_IsForzen(AccountHelper.GetUUID(token)))
            {
                CacheManager.RemoveToken(token);
                tempCacheService.Delete_OneTempCaches(new Guid(token));
                throw new UserLoginException("       ,     !");
            }

            return token;
        }

        private string GetToken(Dictionary<string, object> actionArguments, HttpMethod type)
        {
            var token = "";

            if (type == HttpMethod.Post)
            {
                foreach (var value in actionArguments.Values)
                {
                    token = value.GetType().GetProperty(UserToken) == null
                        ? GetToken(actionArguments, HttpMethod.Get)
                        : value.GetType().GetProperty(UserToken).GetValue(value).ToString();
                }
            }
            else if (type == HttpMethod.Get)
            {
                if (!actionArguments.ContainsKey(UserToken))
                {
                    throw new Exception("   token!");
                }

                if (actionArguments[UserToken] != null)
                {
                    token = actionArguments[UserToken].ToString();
                }
                else
                {
                    throw new Exception("token    !");
                }
            }
            else
            {
                throw new Exception("          !");
            }

            return token;
        }
    }
ここでGetTokenメソッドについて説明します.
1.ブロガーはPOSTとGET方法の検証しかしていません.その他の要求は使用していません.
2.POST方式におけるコールバックは、POST要求インタフェースに単純なパラメータが1つしかない場合、例えば以下のインタフェースを解決する.
        /// <summary>
        ///           
        /// </summary>
        /// <returns></returns>
        [HttpPost]
        [Route("api/Common/PushNewUserSMS")]public PushNewWorkSMSResult PushNewUserSMS([FromBody]string token)
        {
            sendMessagesService.PushNewUserSMS();
            return new PushNewWorkSMSResult() { Code = 0 };
        }
もちろん、POST方式ではパラメータを1つのクラスに書くのが一般的で、1つのパラメータの場合、ブロガーはそんなことをするのが好きではありません.このように書くと、AJAXが時空変数名を提出してから取得する必要があります.
    //          
    function pushNewUserSMS() {
        $(".tuiguang").unbind("click");
// “” $.post(Config.Api.Common.PushNewUserSMS, {
"": $.cookie("MPCBtoken") }, function (data) { if (data.Code == 0) { alert(" !"); _initDatas(); } else { Config.Method.JudgeCode(data, 1); } }); }
これにより,各コントローラに[TokenProjector]ラベルを付け,匿名を許可するActionに[Anonymous]ラベルを付けるだけで簡単にtoken検証が可能になる.
3.token検証のほかに、登録ユーザーの財布残高を取得したAction(A)は、従業員、企業のみがアクセスできるに違いない.管理者、カスタマーサービスには財布がないのでアクセスできない.業務上、指定ユーザーの財布残高を取得した別のAction(B)にアクセスすべきである.もちろん、このアクションは従業員や企業に権限を開放することはできません.これは、アクションのアクセス権を制御する機能を実現する必要があることに関連しています.ユーザーのtokenを検証すると、tokenを取得するとユーザーの基本情報(ロールを含む)が得られます.それでは、アクションに対する権限のマークアップをするだけで問題を解決できます.まず、権限制御を代表するAttributeを書きます.
    /// <summary>
    ///       
    /// </summary>
    [AttributeUsage(AttributeTargets.Method, Inherited = true, AllowMultiple = true)]
    public class ModuleAuthorizationAttribute : Attribute
    {
        public ModuleAuthorizationAttribute(params string[] authorization)
        {
            this.Authorizations = authorization;
        }

        /// <summary>
        ///       
        /// </summary>
        public string[] Authorizations { get; set; }
    }
権限管理が必要なアクションごとに[ModuleAuthorization]ラベルを付け、アクセスロールを明記します.
        /// <summary>
        ///       
        /// </summary>
        /// <param name="token"></param>
        /// <returns></returns>
        [HttpGet]
        [Route("api/Account/GetWalletBalance")]
        [ModuleAuthorization(new[] { "Staff", "Enterprise" })]
        public GetWalletBalanceResult GetWalletBalance(string token)
        {
            var result = this.walletService.Get_Wallet_Balance(AccountHelper.GetUUID(token));
            return new GetWalletBalanceResult()
            {
                Code = 0,
                Balance = result
            };
        }

        /// <summary>
        ///    
        ///       
        /// </summary>
        /// <param name="handleTempWithdrawalsModel"></param>
        /// <returns></returns>
        [HttpPost]
        [Route("api/Account/HandleTempWithdrawals")]
        [ModuleAuthorization(new[] { "PlatformCustomer" })]
        public HandleTempWithdrawalsResult HandleTempWithdrawals(
            [FromBody] HandleTempWithdrawalsModel handleTempWithdrawalsModel)
        {
            walletService.Handle_TempWithdrawals(AccountHelper.GetUUID(handleTempWithdrawalsModel.token),
                handleTempWithdrawalsModel.message, handleTempWithdrawalsModel.tempID,
                handleTempWithdrawalsModel.isSuccess);
            return new HandleTempWithdrawalsResult() { Code = 0 };
        }
次にTokenProjectorAttributeというクラスを変更し、tokenを検証した後に権限検証を行います.権限検証方法は以下の通りです.
        /// <summary>
        /// Action       
        /// </summary>
        /// <param name="token">    </param>
        /// <param name="actionContext"></param>
        /// <returns></returns>
        protected virtual void AuthorizeCore(string token, HttpActionContext actionContext)
        {
            //     Action  
            var moduleAuthorizationAction = actionContext.ActionDescriptor.GetCustomAttributes<ModuleAuthorizationAttribute>();
            if (moduleAuthorizationAction.Any())
            {
                var userRole = AccountHelper.GetUserType(token);
                if (!moduleAuthorizationAction[0].Authorizations.Contains(userRole.ToString()))
                {
                    throw new Exception("         ,token:" + token);
                }
            }
        }
 
OKは,ついにwebAPIによるユーザトークンとAction権限の検証を実現した.
 
もちろん、ブロガーもwebAPIに接触したばかりで、業務の需要は簡単で、もし間違っているところがあれば、皆さんの指摘を歓迎して、必ず謙虚に教えを求めます.