我们正在使用 CookieAuthenticationProvider ,并希望在我们的应用程序中实现'记住我功能,如下所示:
无论是否选中'记住我复选框,令牌过期时间应始终设置为30分钟 (启用了SlidingExpiration)
如果用户没有检查“记住我'我们所做的只是检查令牌是否过期 - 如果是,则用户被重定向到登录屏幕(这是内置到OWIN并且工作正常)
但是,如果用户检查“记得我”,他的凭据应保存在附加 Cookie中(默认生命周期为30天)。如果他的令牌过期(超时仍应设置为30分钟), OWIN应使用该附加cookie在后台自动续订令牌。换句话说 - 如果用户检查'记住我'他应该登录30天或直到他退出。
问题是 - 如何用OWIN完成这样的事情?据我所知,默认实现仍然使用 ExpireTimeSpan 参数 - 唯一的区别是,cookie被标记为持久性,因此如果用户重新启动浏览器,则他已登录 - 但令牌过期仍然存在由ExpireTimeSpan限制。
我想我必须以某种方式在 SignIn 期间手动保存用户凭据并覆盖 OnApplyRedirect 事件(这似乎是未经授权的用户尝试触发的唯一事件访问需要授权的视图,而不是重定向,以某种方式重新生成用户的令牌......但有人知道如何做到这一点吗?
答案 0 :(得分:2)
最后,我最终编写了自定义中间件并将其插入:
<强> 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();
}
}
}
<强> SerializableClaim.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>();
这应该启用中间件。如何工作:如果用户选中“记住我”复选框,则会创建 RememberMeToken Cookie (包含用户在登录时拥有的所有声明)以及'AspNet.ApplicationCookie ”。
当会话超时时,中间件将检查RememberMeToken是否存在且仍然有效 - 如果是这样:它将在后台无缝登录用户。
希望这有助于任何人。