asp.net-mvc - ASP.NET Identity,持久性 cookie - 是内置的类似的东西吗?

标签 asp.net-mvc cookies asp.net-identity

我们正在使用CookieAuthenticationProvider,并希望在我们的应用程序中实现“记住我”功能,其工作方式如下:

  1. 无论是否选中“记住我”复选框, token 过期时间应始终设置为 30 分钟(启用 SlidingExpiration) )

  2. 如果用户没有选中“记住我”,我们所做的就是检查 token 是否过期 - 如果过期,则用户将被重定向到登录屏幕(这是内置于 OWIN 中并且工作正常)

  3. 但是,如果用户选中“记住我”,他的凭据应保存在附加 Cookie 中(默认有效期为 30 天)。如果他的 token 过期(超时仍应设置为 30 分钟),OWIN 应使用该附加 cookie 在后台自动更新 token 。换句话说 - 如果用户选中“记住我”,他应该登录 30 天或直到他注销为止。

问题是 - 如何使用 OWIN 完成这样的事情?据我所知,默认实现仍然使用 ExpireTimeSpan 参数 - 唯一的区别是,cookie 被标记为持久性,因此如果用户重新启动浏览器,他就会登录 - 但 token 过期仍然存在受 ExpireTimeSpan 限制。

我想我必须在登录期间以某种方式手动保存用户凭据并覆盖OnApplyRedirect事件(这似乎是未经授权的用户尝试登录时触发的唯一事件)访问需要授权的 View ),而不是重定向,而是以某种方式重新生成用户的 token ......但是有人知道到底该怎么做吗?

最佳答案

最后,我编写了自定义中间件并将其插入:

RememberMeTokenMiddleware.cs:

using Microsoft.AspNet.Identity;
using Microsoft.Owin;
using Microsoft.Owin.Security.Cookies;
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;
using System.Web;
using System.Web.Security;
using WebApplicationtoRemove.Owin.HelperClasses;
using Microsoft.AspNet.Identity.Owin;

namespace WebApplicationtoRemove.Owin.Middleware
{
    public class RememberMeTokenMiddleware : OwinMiddleware
    {
        #region Private Members

        private static double RememberMeTokenPeriodOfvalidityInMinutes = 43200;

        private IOwinContext Context { get; set; }

        #endregion

        #region Public Static Members



        #endregion

        #region Constructor

        public RememberMeTokenMiddleware(OwinMiddleware next)
            : base(next)
        {
        }

        public RememberMeTokenMiddleware(OwinMiddleware next, double RememberMeTokenPeriodOfvalidityInMinutes)
            : base(next)
        {
            RememberMeTokenMiddleware.RememberMeTokenPeriodOfvalidityInMinutes = RememberMeTokenPeriodOfvalidityInMinutes;
        }

        #endregion

        #region Public Methods

        public override async Task Invoke(IOwinContext context)
        {
            try
            {
                Context = context;

                bool shouldDeleteRememberMeToken = CheckIfRememberMeTokenShouldBeDeleted(context);

                if (shouldDeleteRememberMeToken)
                {
                    context.Response.Cookies.Delete("RemoveRememberMeToken");

                    context.Response.Cookies.Delete("RememberMeToken");
                }
                else
                {
                    if (context.Authentication.User == null || !context.Authentication.User.Identity.IsAuthenticated)
                    {
                        //User is either not set or is not authenticated - try to log him in, using the RememberMeCookie
                        Login(context);
                    }
                }
            }
            catch (Exception ex)
            {
                //Something went wrong - we assume that cookie and/or token was damaged and should be deleted
                context.Response.Cookies.Delete("RememberMeToken");
            }


            await this.Next.Invoke(context);
        }

        #endregion

        #region Static Methods

        /// <summary>
        /// Check conditions and creates RememberMeToken cookie if necessary. This should be called inside SidnedIn event of CookieProvider
        /// </summary>
        public static void CheckAndCreateRememberMeToken(CookieResponseSignedInContext ctx)
        {
            try
            {
                bool signedInFromRememberMeToken = CheckIfUserWasSignedInFromRememberMeToken(ctx.OwinContext);

                if (!signedInFromRememberMeToken && ctx.Properties.IsPersistent)
                {
                    //Login occured using 'normal' path and IsPersistant was set - generate RememberMeToken cookie
                    var claimsToAdd = GenerateSerializableClaimListFromIdentity(ctx.Identity);

                    SerializableClaim cookieExpirationDate = GenerateRememberMeTokenExpirationDateClaim();

                    claimsToAdd.Add(cookieExpirationDate);

                    var allClaimsInFinalCompressedAndProtectedBase64Token = GenerateProtectedAndBase64EncodedClaimsToken(claimsToAdd);

                    ctx.Response.Cookies.Append("RememberMeToken", allClaimsInFinalCompressedAndProtectedBase64Token, new CookieOptions()
                    {
                        Expires = DateTime.Now.AddMinutes(RememberMeTokenPeriodOfvalidityInMinutes)
                    });

                    //Remove the SignedInFromRememberMeToken cookie, to let the middleware know, that user was signed in using normal path
                    ctx.OwinContext.Set("SignedInFromRememberMeToken", false);
                }
            }
            catch (Exception ex)
            {
                //Log errors using your favorite logger here
            }
        }

        /// <summary>
        /// User logged out - sets information (using cookie) for RememberMeTokenMiddleware that RememberMeToken should be removed
        /// </summary>
        public static void Logout(IOwinContext ctx)
        {
            ctx.Response.Cookies.Append("RemoveRememberMeToken", "");
        }

        #endregion

        #region Private Methods

        /// <summary>
        /// Returns information if user was signed in from RememberMeToken cookie - this information should be used to determine if RememberMeToken lifetime should be regenerated or not (it should be, if user signed in using normal path)
        /// </summary>
        private static bool CheckIfUserWasSignedInFromRememberMeToken(IOwinContext ctx)
        {
            bool signedInFromRememberMeToken = ctx.Get<bool>("SignedInFromRememberMeToken");

            return signedInFromRememberMeToken;
        }

        /// <summary>
        /// Generates serializable collection of user claims, that will be saved inside the cookie token. Custom class is used because Claim class causes 'Circular Reference Exception.'
        /// </summary>
        private static List<SerializableClaim> GenerateSerializableClaimListFromIdentity(ClaimsIdentity identity)
        {
            var dataToReturn = identity.Claims.Select(x =>
                                new SerializableClaim()
                                {
                                    Type = x.Type,
                                    ValueType = x.ValueType,
                                    Value = x.Value
                                }).ToList();

            return dataToReturn;
        }

        /// <summary>
        /// Generates a special claim containing an expiration date of RememberMeToken cookie. This is necessary because we CANNOT rely on browsers here - since each one threat cookies differently
        /// </summary>
        private static SerializableClaim GenerateRememberMeTokenExpirationDateClaim()
        {
            SerializableClaim cookieExpirationDate = new SerializableClaim()
            {
                Type = "RememberMeTokenExpirationDate",
                Value = DateTime.Now.AddMinutes(RememberMeTokenPeriodOfvalidityInMinutes).ToBinary().ToString()
            };
            return cookieExpirationDate;
        }

        /// <summary>
        /// Generates token containing user claims. The token is compressed, encrypted using machine key and returned as base64 string - this string will be saved inside RememberMeToken cookie
        /// </summary>
        private static string GenerateProtectedAndBase64EncodedClaimsToken(List<SerializableClaim> claimsToAdd)
        {
            var allClaimsAsString = JsonConvert.SerializeObject(claimsToAdd);

            var allClaimsAsBytes = Encoding.UTF8.GetBytes(allClaimsAsString);

            var allClaimsAsCompressedBytes = CompressionHelper.CompressDeflate(allClaimsAsBytes);

            var allClaimsAsCompressedBytesProtected = MachineKey.Protect(allClaimsAsCompressedBytes, "RememberMeToken");

            var allClaimsInFinalCompressedAndProtectedBase64Token = Convert.ToBase64String(allClaimsAsCompressedBytesProtected);

            return allClaimsInFinalCompressedAndProtectedBase64Token;
        }

        /// <summary>
        /// Primary login method
        /// </summary>
        private void Login(IOwinContext context)
        {
            var base64ProtectedCompressedRememberMeTokenBytes = context.Request.Cookies["RememberMeToken"];

            if (!string.IsNullOrEmpty(base64ProtectedCompressedRememberMeTokenBytes))
            {
                var RememberMeToken = GetRememberMeTokenFromData(base64ProtectedCompressedRememberMeTokenBytes);

                var claims = JsonConvert.DeserializeObject<IEnumerable<SerializableClaim>>(RememberMeToken);

                bool isRememberMeTokenStillValid = IsRememberMeTokenStillValid(claims);

                if (isRememberMeTokenStillValid)
                {
                    //Token is still valid - sign in
                    SignInUser(context, claims);

                    //We set information that user was signed in using the RememberMeToken cookie
                    context.Set("SignedInFromRememberMeToken", true);
                }
                else
                {
                    //Token is invalid or expired - we remove unnecessary cookie
                    context.Response.Cookies.Delete("RememberMeToken");
                }
            }
        }

        /// <summary>
        /// We log user, using passed claims
        /// </summary>
        private void SignInUser(IOwinContext context, IEnumerable<SerializableClaim> claims)
        {
            List<Claim> claimList = new List<Claim>();

            foreach (var item in claims)
            {
                string type = item.Type;

                string value = item.Value;

                claimList.Add(new Claim(type, value));
            }

            ClaimsIdentity ci = new ClaimsIdentity(claimList, DefaultAuthenticationTypes.ApplicationCookie);

            context.Authentication.SignIn(ci);

            context.Authentication.User = context.Authentication.AuthenticationResponseGrant.Principal;
        }

        /// <summary>
        /// Get information if RememberMeToken cookie is still valid (checks not only the date, but also some additional information)
        /// </summary>
        private bool IsRememberMeTokenStillValid(IEnumerable<SerializableClaim> claims)
        {
            var userIdClaim = claims.Where(x => x.Type == ClaimTypes.NameIdentifier).SingleOrDefault();

            if (userIdClaim == null)
            {
                throw new Exception("RememberMeTokenAuthMiddleware. Claim of type NameIdentifier was not found.");
            }

            var userSecurityStampClaim = claims.Where(x => x.Type == "AspNet.Identity.SecurityStamp").SingleOrDefault();

            if (userSecurityStampClaim == null)
            {
                throw new Exception("RememberMeTokenAuthMiddleware. Claim of type SecurityStamp was not found.");
            }

            string userId = userIdClaim.Value;

            var userManager = Context.GetUserManager<ApplicationUserManager>();

            if (userManager == null)
            {
                throw new Exception("RememberMeTokenAuthMiddleware. Unable to get UserManager");
            }

            var currentUserData = userManager.FindById(userId);

            if (currentUserData == null)
            {
                return false;
            }

            if (currentUserData.LockoutEndDateUtc >=  DateTime.Now)
            {
                return false;
            }

            if (currentUserData.SecurityStamp != userSecurityStampClaim.Value)
            {
                //User Securitystamp was changed

                return false;
            }

            return GetRememberMeTokenExpirationMinutesLeft(claims) > 0;
        }

        /// <summary>
        /// Returns how many minutes the RememberMeToken will be valid - if it expired, returns zero or negative value
        /// </summary>
        private double GetRememberMeTokenExpirationMinutesLeft(IEnumerable<SerializableClaim> claims)
        {
            double dataToReturn = -1;

            var RememberMeTokenExpirationDate = GetRememberMeTokenExpirationDate(claims);

            dataToReturn = (RememberMeTokenExpirationDate - DateTime.Now).TotalMinutes;

            return dataToReturn;
        }

        /// <summary>
        /// Returns a DateTime object containing the expiration date of the RememberMeToken
        /// </summary>
        private DateTime GetRememberMeTokenExpirationDate(IEnumerable<SerializableClaim> claims)
        {
            DateTime RememberMeTokenExpirationDate = DateTime.Now.AddDays(-1);

            var RememberMeTokenExpirationClaim = GetRememberMeTokenExpirationDateClaim(claims);

            if (RememberMeTokenExpirationClaim == null)
            {
                throw new Exception("RememberMeTokenAuthMiddleware. RememberMeTokenExpirationClaim was not found.");
            }

            long binaryTime = Convert.ToInt64(RememberMeTokenExpirationClaim.Value);

            RememberMeTokenExpirationDate = DateTime.FromBinary(binaryTime);

            return RememberMeTokenExpirationDate;
        }

        /// <summary>
        /// Returns the claim determining the expiration date of the token
        /// </summary>
        private SerializableClaim GetRememberMeTokenExpirationDateClaim(IEnumerable<SerializableClaim> claims)
        {
            var RememberMeTokenExpirationClaim = claims.Where(x => x.Type == "RememberMeTokenExpirationDate").SingleOrDefault();

            return RememberMeTokenExpirationClaim;
        }

        /// <summary>
        /// Attempts to decipher the RememberMeToken to the JSON format containing claims
        /// </summary>
        private string GetRememberMeTokenFromData(string base64ProtectedCompressedRememberMeTokenBytes)
        {
            var protectedCompressedRememberMeTokenBytes = Convert.FromBase64String(base64ProtectedCompressedRememberMeTokenBytes);

            var compressedRememberMeTokenBytes = MachineKey.Unprotect(protectedCompressedRememberMeTokenBytes, "RememberMeToken");

            var RememberMeTokenBytes = CompressionHelper.DecompressDeflate(compressedRememberMeTokenBytes);

            var RememberMeToken = Encoding.UTF8.GetString(RememberMeTokenBytes);

            return RememberMeToken;
        }

        /// <summary>
        /// Returns information if token cookie should be delated (for example, when user click 'Logout')
        /// </summary>
        private bool CheckIfRememberMeTokenShouldBeDeleted(IOwinContext context)
        {
            bool shouldDeleteRememberMeToken = (context.Request.Cookies.Where(x => x.Key == "RemoveRememberMeToken").Count() > 0);

            return shouldDeleteRememberMeToken;
        }

        #endregion
    }
}

还有一些辅助类: CompressionHelper.cs:

using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Web;

namespace WebApplicationtoRemove.Owin.HelperClasses
{
    /// <summary>
    /// Data compression helper
    /// </summary>
    public static class CompressionHelper
    {
        public static byte[] CompressDeflate(byte[] data)
        {
            MemoryStream output = new MemoryStream();
            using (DeflateStream dstream = new DeflateStream(output, CompressionLevel.Optimal))
            {
                dstream.Write(data, 0, data.Length);
            }
            return output.ToArray();
        }

        public static byte[] DecompressDeflate(byte[] data)
        {
            MemoryStream input = new MemoryStream(data);
            MemoryStream output = new MemoryStream();
            using (DeflateStream dstream = new DeflateStream(input, CompressionMode.Decompress))
            {
                dstream.CopyTo(output);
            }
            return output.ToArray();
        }
    }
}

SerializedClaim.cs:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;

namespace WebApplicationtoRemove.Owin.HelperClasses
{
    public class SerializableClaim
    {
        public string Type { get; set; }

        public string ValueType { get; set; }

        public string Value { get; set; }
    }
}

要测试上述内容 - 创建新的 MVC 4.6.x 项目(身份验证模式:个人用户帐户),向其中添加上述类,然后修改 Startup.Auth.cs:

using System;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.Owin;
using Microsoft.Owin;
using Microsoft.Owin.Security.Cookies;
using Microsoft.Owin.Security.Google;
using Owin;
using WebApplicationtoRemove.Models;
using WebApplicationtoRemove.Owin.Middleware;

namespace WebApplicationtoRemove
{
    public partial class Startup
    {
        // For more information on configuring authentication, please visit http://go.microsoft.com/fwlink/?LinkId=301864
        public void ConfigureAuth(IAppBuilder app)
        {
            // Configure the db context, user manager and signin manager to use a single instance per request
            app.CreatePerOwinContext(ApplicationDbContext.Create);
            app.CreatePerOwinContext<ApplicationUserManager>(ApplicationUserManager.Create);
            app.CreatePerOwinContext<ApplicationSignInManager>(ApplicationSignInManager.Create);

            // Enable the application to use a cookie to store information for the signed in user
            // and to use a cookie to temporarily store information about a user logging in with a third party login provider
            // Configure the sign in cookie
            app.UseCookieAuthentication(new CookieAuthenticationOptions
            {
                AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
                LoginPath = new PathString("/Account/Login"),
                Provider = new CookieAuthenticationProvider
                {
                    // Enables the application to validate the security stamp when the user logs in.
                    // This is a security feature which is used when you change a password or add an external login to your account.  
                    OnValidateIdentity = SecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
                        validateInterval: TimeSpan.FromMinutes(30),
                        regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager)),

                    OnResponseSignedIn = ctx =>
                    {
                        RememberMeTokenMiddleware.CheckAndCreateRememberMeToken(ctx);
                    },

                    OnResponseSignOut = ctx =>
                    {
                        RememberMeTokenMiddleware.Logout(ctx.OwinContext);
                    }
                }
            });

            app.Use<RememberMeTokenMiddleware>();
        }
    }
}

您感兴趣的是这些:

OnResponseSignedIn = ctx =>
{
    RememberMeTokenMiddleware.CheckAndCreateRememberMeToken(ctx);
},

OnResponseSignOut = ctx =>
{
    RememberMeTokenMiddleware.Logout(ctx.OwinContext);
}

这一行:

app.Use<RememberMeTokenMiddleware>();

这应该启用中间件。工作原理:如果用户选中“记住我”复选框,则会在“AspNet.ApplicationCookie”旁边创建一个 RememberMeToken cookie(包含用户在登录期间拥有的所有声明) '。

当 session 超时时,中间件将检查 RememberMeToken 是否存在,并且仍然有效 - 如果是:它将在后台无缝登录用户。

希望这对任何人都有帮助。

关于asp.net-mvc - ASP.NET Identity,持久性 cookie - 是内置的类似的东西吗?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/42752997/

相关文章:

asp.net - 为什么会在页面加载前后调用Owin Startup类的Configuration方法?

asp.net - MVC5 (VS2012) 标识 CreateIdentityAsync - 值不能为空

c# - 如何覆盖 ASP.NET MVC 3 默认模型绑定(bind)器以在模型创建期间解决依赖关系(使用 ninject)?

javascript - 为什么我的 jQuery.post() ajax 在我返回一个 View 时失败,但在我返回另一个 View 时却没有失败?

c# - 当我的 MVC Controller 中没有数据时,尝试读取无效

http - Cookies 在 Firefox 和 Chrome 中工作,不适用于 IE9

javascript - 无法在 Chrome 中设置 document.cookie

asp.net-mvc - 将额外数据传递给 EditorTemplate

javascript - 为什么 expressjs 会在每个 OPTIONS 响应中发送 Set-Cookie header ?

c# - MVC 5 : Should I inherit my User from IdentityUser class?