我有一个Intranet应用程序,其中所有用户操作都是通过对远程系统的API调用(没有本地表)进行的。一些API调用需要用户的密码。我无法真正要求用户在使用网站时重新输入密码(有时他们刚刚登录后几秒钟)。
因此,如果不将密码保存到数据库,我可以在用户登录期间安全地缓存密码(注意:"登录",不是"会话&#34 )。我尝试将它们存储在会话状态,但问题是会话只持续20分钟,但登录令牌有效24小时。
理想情况下,我希望它(以某种方式)直接链接到.AspNet.ApplicationCookie,因此登录和缓存的密码不会失去同步,但它没有看到可以添加自定义值到那个cookie。如果此cookie尚未加密,则可以加密。
修改: 由于"记得我"功能,登录可以持续比Session.TimeOut值更长的时间,因此我不想使用会话。
答案 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))
}
});