在ASP.NET身份验证中,如何在登录后安全地缓存用户的密码?

时间:2018-04-12 09:10:46

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

我有一个Intranet应用程序,其中所有用户操作都是通过对远程系统的API调用(没有本地表)进行的。一些API调用需要用户的密码。我无法真正要求用户在使用网站时重新输入密码(有时他们刚刚登录后几秒钟)。

因此,如果不将密码保存到数据库,我可以在用户登录期间安全地缓存密码(注意:"登录",不是"会话&#34 )。我尝试将它们存储在会话状态,但问题是会话只持续20分钟,但登录令牌有效24小时。

理想情况下,我希望它(以某种方式)直接链接到.AspNet.ApplicationCookie,因此登录和缓存的密码不会失去同步,但它没有看到可以添加自定义值到那个cookie。如果此cookie尚未加密,则可以加密。

修改: 由于"记得我"功能,登录可以持续比Session.TimeOut值更长的时间,因此我不想使用会话。

2 个答案:

答案 0 :(得分:3)

我有一个项目,我必须实现完全相同,最终得到ASP.NET Identity接口的自定义实现。 (在我的例子中,用户名和密码由带有API的外部系统管理。)
我将解释代码的主意和主要部分。

所需的userinfo(例如用户名和密码)存储在自定义ConcurrentDictionary内的IUserStore内存中,根据定义,可以获取用户信息的位置。
注意;我将跳过安全性最佳实践。

唯一可以通过自定义PasswordSignInAsync的{​​{1}}方法访问用户密码的地方。
这里事情变得不同了!
在默认/常规流程中,SignInManager使用SignInManager检索userinfo以进行密码检查。但是因为IUserStore的角色变成了一个不再可能的被动记忆存储器;这个初始查找必须通过例如。数据库查找 然后IUserStore进行密码检查 如果有效,则会将userinfo添加或更新到自定义SignInManager中(通过IUserStore上的自定义方法。)
每次用户登录时都必须执行更新,否则密码将保持陈旧状态,因为密码会在应用程序期间保留在内存中。

如果Web应用程序被回收并且CustomUserStore中的userinfo丢失,ASP.NET身份框架会通过将用户再次重定向到登录页面来解决此问题,通过该页面再次启动上述流程

下一个要求是自定义Dictionary,因为我的UserManager没有实现ASP.NET身份所需的所有接口;请参阅代码中的注释。这可能与您的情况不同。

完成所有这些后,您可以通过IUserStore检索CustomUser;用户对象持有密码:

UserManager

以下是实施的一些摘录。

存储在内存中的数据:

CustomUser user = this._userManager.FindById(userName); 

自定义public class UserInfo { String Password { get; set; } String Id { get; set; } String UserName { get; set; } }

IUser

自定义public class CustomUser : IUser<String> { public String Id { get; } public String Password { get; set; } public String UserName { get; set; } } 及其写入方法:

IUserStore

自定义public interface ICustomUserStore : IUserStore<CustomUser> { void CreateOrUpdate(UserInfo user); }

UserStore

自定义public class CustomUserStore : ICustomUserStore { private readonly ConcurrentDictionary<String, CustomUser> _users = new ConcurrentDictionary<String, CustomUser>(StringComparer.OrdinalIgnoreCase); public Task<CustomUser> FindByIdAsync(String userId) { // UserId and userName are being treated as the same. return this.FindByNameAsync(userId); } public Task<CustomUser> FindByNameAsync(String userName) { if (!this._users.ContainsKey(userName)) { return Task.FromResult(null as CustomUser); } CustomUser user; if (!this._users.TryGetValue(userName, out user)) { return Task.FromResult(null as CustomUser); } return Task.FromResult(user); } public void CreateOrUpdate(UserInfo userInfo) { if (userInfo != null) { this._users.AddOrUpdate(userInfo.UserName, // Add. key => new CustomUser { Id = userInfo.Id, UserName = userInfo.UserName, Password = userInfo.Password) } // Update; prevent stale password. (key, value) => { value.Password = userInfo.Password; return value }); } } }

UserManager

自定义public class CustomUserManager : UserManager<CustomUser> { public CustomUserManager(ICustomUserStore userStore) : base(userStore) {} /// Must be overridden because ICustomUserStore does not implement IUserPasswordStore<CustomUser>. public override Task<Boolean> CheckPasswordAsync(CustomUser user, String password) { return Task.FromResult(true); } /// Must be overridden because ICustomUserStore does not implement IUserTwoFactorStore<CustomUser>. public override Task<Boolean> GetTwoFactorEnabledAsync(String userId) { return Task.FromResult(false); } /// Must be overridden because ICustomUserStore does not implement IUserLockoutStore<CustomUser>. public override Task<Boolean> IsLockedOutAsync(String userId) { return Task.FromResult(false); } /// Must be overridden because ICustomUserStore does not implement IUserLockoutStore<CustomUser>. public override Task<IdentityResult> ResetAccessFailedCountAsync(String userId) { Task.FromResult(IdentityResult.Success); } }

SignInManager:

答案 1 :(得分:3)

免责声明:您在此处将密码放入Cookie中。加密的cookie,但密码。从安全角度来看,这不是最佳实践。因此,如果您的系统可以接受,请自行做出决定。

我认为最好的方法是将密码存储为身份验证Cookie中的声明。 Auth cookie在传输时会被加密,但您不必自己处理加密 - 这是由OWIN为您完成的。这需要更少的管道。

首先按如下方式重写您的登录操作:

    [HttpPost]
    [AllowAnonymous]
    [ValidateAntiForgeryToken]
    public async Task<ActionResult> Login(LoginViewModel model, string returnUrl)
    {
        if (!ModelState.IsValid)
        {
            return View(model);
        }

        var user = await UserManager.FindAsync(model.Email, model.Password);

        if (user == null)
        {
            // user with this username/password not found
            ModelState.AddModelError("", "Invalid login attempt.");
            return View(model);
        }

        // BEWARE this does not check if user is disabled, locked or does not have a confirmed user
        // I'll leave this for you to implement if needed.

        var userIdentity = await UserManager.CreateIdentityAsync(user, DefaultAuthenticationTypes.ApplicationCookie);
        userIdentity.AddClaim(new Claim("MyApplication:Password", model.Password));

        AuthenticationManager.SignIn(new AuthenticationProperties() { IsPersistent = true }, userIdentity);

        return RedirectToLocal(returnUrl);
    }

这会在登录时获取密码,并将其作为对Identity的声明添加,然后将其序列化并加密为cookie。

请注意,这里省略了很多逻辑 - 如果您需要检查用户是否已被禁用,已锁定或没有确认的电子邮件,您需要自行添加。我怀疑你不需要那个,因为你提到这是一个内部唯一的网站。

接下来,您需要一个扩展方法来解压缩密码:

using System;
using System.Security.Claims;
using System.Security.Principal;
public static class PrincipalExtensions
{
    public static String GetStoredPassword(this IPrincipal principal)
    {
        var claimsPrincipal = principal as ClaimsPrincipal;
        if (claimsPrincipal == null)
        {
            throw new Exception("Expecting ClaimsPrincipal");
        }

        var passwordClaim = claimsPrincipal.FindFirst("MyApplication:Password");

        if (passwordClaim == null)
        {
            throw new Exception("Password is not stored");
        }

        var password = passwordClaim.Value;

        return password;
    }
}

这就是它。现在,在每个操作中,您都可以在User属性上应用该方法:

    [Authorize]
    public ActionResult MyPassword()
    {
        var myPassword = User.GetStoredPassword();

        return View((object)myPassword);
    }

相应的观点将是这样的:

@model String

<h2>Password is @Model</h2>

但是,根据您的要求,此密码声明可能会随着时间的推移而被终止或被保留。默认身份模板启用cookie上每30分钟执行一次的SecurityStampInvalidator,并从数据库中重新刷新它。通常,像这样添加的ad-hoc声明无法在此重写中存活。

要保留密码值超过cookie年龄30分钟,请参加此课程:

using System;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.Owin.Security.Cookies;
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.Owin;

// This is mostly copy of original security stamp validator, only with addition to keep hold of password claim
// https://github.com/aspnet/AspNetIdentity/blob/a24b776676f12cf7f0e13944783cf8e379b3ef70/src/Microsoft.AspNet.Identity.Owin/SecurityStampValidator.cs#L1
public class MySecurityStampValidator
{
    /// <summary>
    ///     Can be used as the ValidateIdentity method for a CookieAuthenticationProvider which will check a user's security
    ///     stamp after validateInterval
    ///     Rejects the identity if the stamp changes, and otherwise will call regenerateIdentity to sign in a new
    ///     ClaimsIdentity
    /// </summary>
    /// <typeparam name="TManager"></typeparam>
    /// <typeparam name="TUser"></typeparam>
    /// <param name="validateInterval"></param>
    /// <param name="regenerateIdentity"></param>
    /// <returns></returns>
    public static Func<CookieValidateIdentityContext, Task> OnValidateIdentity<TManager, TUser>(
        TimeSpan validateInterval, Func<TManager, TUser, Task<ClaimsIdentity>> regenerateIdentity)
        where TManager : UserManager<TUser, string>
        where TUser : class, IUser<string>
    {
        return OnValidateIdentity(validateInterval, regenerateIdentity, id => id.GetUserId());
    }

    /// <summary>
    ///     Can be used as the ValidateIdentity method for a CookieAuthenticationProvider which will check a user's security
    ///     stamp after validateInterval
    ///     Rejects the identity if the stamp changes, and otherwise will call regenerateIdentity to sign in a new
    ///     ClaimsIdentity
    /// </summary>
    /// <typeparam name="TManager"></typeparam>
    /// <typeparam name="TUser"></typeparam>
    /// <typeparam name="TKey"></typeparam>
    /// <param name="validateInterval"></param>
    /// <param name="regenerateIdentityCallback"></param>
    /// <param name="getUserIdCallback"></param>
    /// <returns></returns>
    public static Func<CookieValidateIdentityContext, Task> OnValidateIdentity<TManager, TUser, TKey>(
        TimeSpan validateInterval, Func<TManager, TUser, Task<ClaimsIdentity>> regenerateIdentityCallback,
        Func<ClaimsIdentity, TKey> getUserIdCallback)
        where TManager : UserManager<TUser, TKey>
        where TUser : class, IUser<TKey>
        where TKey : IEquatable<TKey>
    {
        if (getUserIdCallback == null)
        {
            throw new ArgumentNullException("getUserIdCallback");
        }
        return async context =>
        {
            var currentUtc = DateTimeOffset.UtcNow;
            if (context.Options != null && context.Options.SystemClock != null)
            {
                currentUtc = context.Options.SystemClock.UtcNow;
            }
            var issuedUtc = context.Properties.IssuedUtc;

            // Only validate if enough time has elapsed
            var validate = (issuedUtc == null);
            if (issuedUtc != null)
            {
                var timeElapsed = currentUtc.Subtract(issuedUtc.Value);
                validate = timeElapsed > validateInterval;
            }
            if (validate)
            {
                var manager = context.OwinContext.GetUserManager<TManager>();
                var userId = getUserIdCallback(context.Identity);
                if (manager != null && userId != null)
                {
                    var user = await manager.FindByIdAsync(userId);
                    var reject = true;
                    // Refresh the identity if the stamp matches, otherwise reject
                    if (user != null && manager.SupportsUserSecurityStamp)
                    {
                        var securityStamp =
                            context.Identity.FindFirstValue(Constants.DefaultSecurityStampClaimType);
                        if (securityStamp == await manager.GetSecurityStampAsync(userId))
                        {
                            reject = false;
                            // Regenerate fresh claims if possible and resign in
                            if (regenerateIdentityCallback != null)
                            {
                                var identity = await regenerateIdentityCallback.Invoke(manager, user);
                                if (identity != null)
                                {
                                    var passwordClaim = context.Identity.FindFirst("MyApplication:Password");
                                    if (passwordClaim != null)
                                    {
                                        identity.AddClaim(passwordClaim);
                                    }

                                    // Fix for regression where this value is not updated
                                    // Setting it to null so that it is refreshed by the cookie middleware
                                    context.Properties.IssuedUtc = null;
                                    context.Properties.ExpiresUtc = null;
                                    context.OwinContext.Authentication.SignIn(context.Properties, identity);
                                }
                            }
                        }
                    }
                    if (reject)
                    {
                        context.RejectIdentity();
                        context.OwinContext.Authentication.SignOut(context.Options.AuthenticationType);
                    }
                }
            }
        };
    }
}

请注意,这是original Identity code的直接副本,只需稍加修改即可保留密码声明。

要激活此类,请在Startup.Auth.cs中执行以下操作:

app.UseCookieAuthentication(new CookieAuthenticationOptions
{
    AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
    LoginPath = new PathString("/Account/Login"),
    Provider = new CookieAuthenticationProvider
    {
        // use MySecurityStampValidator here
        OnValidateIdentity = MySecurityStampValidator.OnValidateIdentity<ApplicationUserManager, ApplicationUser>(
            validateInterval: TimeSpan.FromMinutes(10), // adjust time as required
            regenerateIdentity: (manager, user) => user.GenerateUserIdentityAsync(manager))
    }
});

Here is a working sample code