环境:
- .netcore 3.1.1.0
一、cookie认证的期限配置
有两个地方可以进行配置,一个是在startup
的AddCookie()
设置,另一个是在/Account/Login
Action中的HttpContext.SignInAsync()
里进行设置。
1.1 AddCookie()
配置项
在这里主要配置两个:ExpireTimeSpan和SlidingExpiration,前者表示一次登录有效的期限,默认是14天,后者表示是否启用滑动窗口,如果启用了那么在cookie快到期的时候会重新颁发个cookie。它们的使用方法如下:
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(options =>
{
options.ExpireTimeSpan = TimeSpan.FromDays(14);
options.SlidingExpiration = true;
});
它们的默认设置的源码如下:
using System;
using Microsoft.AspNetCore.DataProtection;
using Microsoft.AspNetCore.Http;
namespace Microsoft.AspNetCore.Authentication.Cookies
{
public class CookieAuthenticationOptions : AuthenticationSchemeOptions
{
......
public CookieAuthenticationOptions()
{
ExpireTimeSpan = TimeSpan.FromDays(14);
ReturnUrlParameter = CookieAuthenticationDefaults.ReturnUrlParameter;
SlidingExpiration = true;
Events = new CookieAuthenticationEvents();
}
......
}
}
1.2 HttpContext.SignInAsync()
配置项
这个地方是用来向web应用中注册登录信息的地方,可以指定IssuedUtc、ExpiresUtc、AllowRefresh和IsPersistent四项,它们表示的意思如下:
- IssuedUtc 凭证颁发的日期(一般不用设置)
- ExpiresUtc 凭证的有效截止日期(一般不用设置)
- AllowRefresh 快到期时是否刷新凭证
- IsPersistent 是否将凭证保存到cookie中
它的使用方法如下:
public class AccountController : Controller
{
public AccountController(ISystemClock clock)
{
Clock = clock;
}
public ISystemClock Clock { get; }
public IActionResult Login()
{
return View();
}
[HttpPost]
public async Task<IActionResult> Login(string username, string pwd)
{
var claimId = new Claim("ID", "20");
var claimsIdentity = new ClaimsIdentity(new Claim[] { claimId }, "ShenFenZheng");
var claimsPrincipal = new ClaimsPrincipal(claimsIdentity);
await ControllerContext.HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, claimsPrincipal, new AuthenticationProperties()
{
IssuedUtc = Clock.UtcNow,
AllowRefresh = true,
IsPersistent = true,
ExpiresUtc = Clock.UtcNow.AddDays(14)
});
return Json(new { Success = true, Data = "login success!" });
}
}
二、认证处理逻辑
2.1 访问登录接口时
- 第一步: 读取request中的cookie,此时肯定读取不到。
- 第二步:用户代码验证username和pwd后执行
HttpContext.SignInAsync()
方法 - 第三步:
CookieAuthenticationHandler
处理登录逻辑
2.2 登录后访问[Authorize]的Action
- 第一步:从request中读取cookie并解密成ticket,注意:在这个地方有两个有效期拦截,首先就是浏览器自身根据cookie的有效期进行过滤,再一个就是ticket记录的有效期过滤,只要有一个不通过就不会拿到正确的ticket。
- 第二步:检查cookie是否需要刷新,判断的依据是:
- 1.首先判断AddCookie的时候设置了
SlidingExpiration=true
- 2.然后判断SigninAsync中设置了
AllowRefresh=true
- 3.最后计算当前的时间是否已经超过了认证开始时间和认证结束时间的一半,如下图所示:
- 1.首先判断AddCookie的时候设置了
- 第三步:如果需要刷新就更新认证开始时间和认证结束时间,改变前后如下图所示:
理论上,只要你访问系统的频率足够低,cookie就可以一直刷新,看起来就像是永远在线一样。
三、总结使用方法
- 如果要开启cookie认证,那么你应该:
1). 在AddCookie()
中设置ExpireTimeSpan
,虽然也可以不设置,但是建议自己设置一下,默认的一次登录是14天内有效
2).在HttpContext.SignInAsync()
中设置IsPersistent=true
- 如果要开启滑动登录窗口,那么除了上面设置的你还需要:
1).在AddCookie
中设置SlidingExpiration=true
2).在HttpContext.SignInAsync()
中设置AllowRefresh=true
代码示例:
四、附CookieAuthenticationHandler
的源码
其实认证处理的代码都在
CookieAuthenticationHandler
里面了,这里直接全部粘贴过来:
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Linq;
using System.Security.Claims;
using System.Text.Encodings.Web;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.Net.Http.Headers;
namespace Microsoft.AspNetCore.Authentication.Cookies
{
public class CookieAuthenticationHandler : SignInAuthenticationHandler<CookieAuthenticationOptions>
{
private const string HeaderValueNoCache = "no-cache";
private const string HeaderValueEpocDate = "Thu, 01 Jan 1970 00:00:00 GMT";
private const string SessionIdClaim = "Microsoft.AspNetCore.Authentication.Cookies-SessionId";
private bool _shouldRefresh;
private bool _signInCalled;
private bool _signOutCalled;
private DateTimeOffset? _refreshIssuedUtc;
private DateTimeOffset? _refreshExpiresUtc;
private string _sessionKey;
private Task<AuthenticateResult> _readCookieTask;
private AuthenticationTicket _refreshTicket;
public CookieAuthenticationHandler(IOptionsMonitor<CookieAuthenticationOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
: base(options, logger, encoder, clock)
{ }
/// <summary>
/// The handler calls methods on the events which give the application control at certain points where processing is occurring.
/// If it is not provided a default instance is supplied which does nothing when the methods are called.
/// </summary>
protected new CookieAuthenticationEvents Events
{
get { return (CookieAuthenticationEvents)base.Events; }
set { base.Events = value; }
}
protected override Task InitializeHandlerAsync()
{
// Cookies needs to finish the response
Context.Response.OnStarting(FinishResponseAsync);
return Task.CompletedTask;
}
/// <summary>
/// Creates a new instance of the events instance.
/// </summary>
/// <returns>A new instance of the events instance.</returns>
protected override Task<object> CreateEventsAsync() => Task.FromResult<object>(new CookieAuthenticationEvents());
private Task<AuthenticateResult> EnsureCookieTicket()
{
// We only need to read the ticket once
if (_readCookieTask == null)
{
_readCookieTask = ReadCookieTicket();
}
return _readCookieTask;
}
private void CheckForRefresh(AuthenticationTicket ticket)
{
var currentUtc = Clock.UtcNow;
var issuedUtc = ticket.Properties.IssuedUtc;
var expiresUtc = ticket.Properties.ExpiresUtc;
var allowRefresh = ticket.Properties.AllowRefresh ?? true;
if (issuedUtc != null && expiresUtc != null && Options.SlidingExpiration && allowRefresh)
{
var timeElapsed = currentUtc.Subtract(issuedUtc.Value);
var timeRemaining = expiresUtc.Value.Subtract(currentUtc);
if (timeRemaining < timeElapsed)
{
RequestRefresh(ticket);
}
}
}
private void RequestRefresh(AuthenticationTicket ticket, ClaimsPrincipal replacedPrincipal = null)
{
var issuedUtc = ticket.Properties.IssuedUtc;
var expiresUtc = ticket.Properties.ExpiresUtc;
if (issuedUtc != null && expiresUtc != null)
{
_shouldRefresh = true;
var currentUtc = Clock.UtcNow;
_refreshIssuedUtc = currentUtc;
var timeSpan = expiresUtc.Value.Subtract(issuedUtc.Value);
_refreshExpiresUtc = currentUtc.Add(timeSpan);
_refreshTicket = CloneTicket(ticket, replacedPrincipal);
}
}
private AuthenticationTicket CloneTicket(AuthenticationTicket ticket, ClaimsPrincipal replacedPrincipal)
{
var principal = replacedPrincipal ?? ticket.Principal;
var newPrincipal = new ClaimsPrincipal();
foreach (var identity in principal.Identities)
{
newPrincipal.AddIdentity(identity.Clone());
}
var newProperties = new AuthenticationProperties();
foreach (var item in ticket.Properties.Items)
{
newProperties.Items[item.Key] = item.Value;
}
return new AuthenticationTicket(newPrincipal, newProperties, ticket.AuthenticationScheme);
}
private async Task<AuthenticateResult> ReadCookieTicket()
{
var cookie = Options.CookieManager.GetRequestCookie(Context, Options.Cookie.Name);
if (string.IsNullOrEmpty(cookie))
{
return AuthenticateResult.NoResult();
}
var ticket = Options.TicketDataFormat.Unprotect(cookie, GetTlsTokenBinding());
if (ticket == null)
{
return AuthenticateResult.Fail("Unprotect ticket failed");
}
if (Options.SessionStore != null)
{
var claim = ticket.Principal.Claims.FirstOrDefault(c => c.Type.Equals(SessionIdClaim));
if (claim == null)
{
return AuthenticateResult.Fail("SessionId missing");
}
_sessionKey = claim.Value;
ticket = await Options.SessionStore.RetrieveAsync(_sessionKey);
if (ticket == null)
{
return AuthenticateResult.Fail("Identity missing in session store");
}
}
var currentUtc = Clock.UtcNow;
var expiresUtc = ticket.Properties.ExpiresUtc;
if (expiresUtc != null && expiresUtc.Value < currentUtc)
{
if (Options.SessionStore != null)
{
await Options.SessionStore.RemoveAsync(_sessionKey);
}
return AuthenticateResult.Fail("Ticket expired");
}
CheckForRefresh(ticket);
// Finally we have a valid ticket
return AuthenticateResult.Success(ticket);
}
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
var result = await EnsureCookieTicket();
if (!result.Succeeded)
{
return result;
}
var context = new CookieValidatePrincipalContext(Context, Scheme, Options, result.Ticket);
await Events.ValidatePrincipal(context);
if (context.Principal == null)
{
return AuthenticateResult.Fail("No principal.");
}
if (context.ShouldRenew)
{
RequestRefresh(result.Ticket, context.Principal);
}
return AuthenticateResult.Success(new AuthenticationTicket(context.Principal, context.Properties, Scheme.Name));
}
private CookieOptions BuildCookieOptions()
{
var cookieOptions = Options.Cookie.Build(Context);
// ignore the 'Expires' value as this will be computed elsewhere
cookieOptions.Expires = null;
return cookieOptions;
}
protected virtual async Task FinishResponseAsync()
{
// Only renew if requested, and neither sign in or sign out was called
if (!_shouldRefresh || _signInCalled || _signOutCalled)
{
return;
}
var ticket = _refreshTicket;
if (ticket != null)
{
var properties = ticket.Properties;
if (_refreshIssuedUtc.HasValue)
{
properties.IssuedUtc = _refreshIssuedUtc;
}
if (_refreshExpiresUtc.HasValue)
{
properties.ExpiresUtc = _refreshExpiresUtc;
}
if (Options.SessionStore != null && _sessionKey != null)
{
await Options.SessionStore.RenewAsync(_sessionKey, ticket);
var principal = new ClaimsPrincipal(
new ClaimsIdentity(
new[] { new Claim(SessionIdClaim, _sessionKey, ClaimValueTypes.String, Options.ClaimsIssuer) },
Scheme.Name));
ticket = new AuthenticationTicket(principal, null, Scheme.Name);
}
var cookieValue = Options.TicketDataFormat.Protect(ticket, GetTlsTokenBinding());
var cookieOptions = BuildCookieOptions();
if (properties.IsPersistent && _refreshExpiresUtc.HasValue)
{
cookieOptions.Expires = _refreshExpiresUtc.Value.ToUniversalTime();
}
Options.CookieManager.AppendResponseCookie(
Context,
Options.Cookie.Name,
cookieValue,
cookieOptions);
await ApplyHeaders(shouldRedirectToReturnUrl: false, properties: properties);
}
}
protected async override Task HandleSignInAsync(ClaimsPrincipal user, AuthenticationProperties properties)
{
if (user == null)
{
throw new ArgumentNullException(nameof(user));
}
properties = properties ?? new AuthenticationProperties();
_signInCalled = true;
// Process the request cookie to initialize members like _sessionKey.
await EnsureCookieTicket();
var cookieOptions = BuildCookieOptions();
var signInContext = new CookieSigningInContext(
Context,
Scheme,
Options,
user,
properties,
cookieOptions);
DateTimeOffset issuedUtc;
if (signInContext.Properties.IssuedUtc.HasValue)
{
issuedUtc = signInContext.Properties.IssuedUtc.Value;
}
else
{
issuedUtc = Clock.UtcNow;
signInContext.Properties.IssuedUtc = issuedUtc;
}
if (!signInContext.Properties.ExpiresUtc.HasValue)
{
signInContext.Properties.ExpiresUtc = issuedUtc.Add(Options.ExpireTimeSpan);
}
await Events.SigningIn(signInContext);
if (signInContext.Properties.IsPersistent)
{
var expiresUtc = signInContext.Properties.ExpiresUtc ?? issuedUtc.Add(Options.ExpireTimeSpan);
signInContext.CookieOptions.Expires = expiresUtc.ToUniversalTime();
}
var ticket = new AuthenticationTicket(signInContext.Principal, signInContext.Properties, signInContext.Scheme.Name);
if (Options.SessionStore != null)
{
if (_sessionKey != null)
{
await Options.SessionStore.RemoveAsync(_sessionKey);
}
_sessionKey = await Options.SessionStore.StoreAsync(ticket);
var principal = new ClaimsPrincipal(
new ClaimsIdentity(
new[] { new Claim(SessionIdClaim, _sessionKey, ClaimValueTypes.String, Options.ClaimsIssuer) },
Options.ClaimsIssuer));
ticket = new AuthenticationTicket(principal, null, Scheme.Name);
}
var cookieValue = Options.TicketDataFormat.Protect(ticket, GetTlsTokenBinding());
Options.CookieManager.AppendResponseCookie(
Context,
Options.Cookie.Name,
cookieValue,
signInContext.CookieOptions);
var signedInContext = new CookieSignedInContext(
Context,
Scheme,
signInContext.Principal,
signInContext.Properties,
Options);
await Events.SignedIn(signedInContext);
// Only redirect on the login path
var shouldRedirect = Options.LoginPath.HasValue && OriginalPath == Options.LoginPath;
await ApplyHeaders(shouldRedirect, signedInContext.Properties);
Logger.AuthenticationSchemeSignedIn(Scheme.Name);
}
protected async override Task HandleSignOutAsync(AuthenticationProperties properties)
{
properties = properties ?? new AuthenticationProperties();
_signOutCalled = true;
// Process the request cookie to initialize members like _sessionKey.
await EnsureCookieTicket();
var cookieOptions = BuildCookieOptions();
if (Options.SessionStore != null && _sessionKey != null)
{
await Options.SessionStore.RemoveAsync(_sessionKey);
}
var context = new CookieSigningOutContext(
Context,
Scheme,
Options,
properties,
cookieOptions);
await Events.SigningOut(context);
Options.CookieManager.DeleteCookie(
Context,
Options.Cookie.Name,
context.CookieOptions);
// Only redirect on the logout path
var shouldRedirect = Options.LogoutPath.HasValue && OriginalPath == Options.LogoutPath;
await ApplyHeaders(shouldRedirect, context.Properties);
Logger.AuthenticationSchemeSignedOut(Scheme.Name);
}
private async Task ApplyHeaders(bool shouldRedirectToReturnUrl, AuthenticationProperties properties)
{
Response.Headers[HeaderNames.CacheControl] = HeaderValueNoCache;
Response.Headers[HeaderNames.Pragma] = HeaderValueNoCache;
Response.Headers[HeaderNames.Expires] = HeaderValueEpocDate;
if (shouldRedirectToReturnUrl && Response.StatusCode == 200)
{
// set redirect uri in order:
// 1. properties.RedirectUri
// 2. query parameter ReturnUrlParameter
//
// Absolute uri is not allowed if it is from query string as query string is not
// a trusted source.
var redirectUri = properties.RedirectUri;
if (string.IsNullOrEmpty(redirectUri))
{
redirectUri = Request.Query[Options.ReturnUrlParameter];
if (string.IsNullOrEmpty(redirectUri) || !IsHostRelative(redirectUri))
{
redirectUri = null;
}
}
if (redirectUri != null)
{
await Events.RedirectToReturnUrl(
new RedirectContext<CookieAuthenticationOptions>(Context, Scheme, Options, properties, redirectUri));
}
}
}
private static bool IsHostRelative(string path)
{
if (string.IsNullOrEmpty(path))
{
return false;
}
if (path.Length == 1)
{
return path[0] == '/';
}
return path[0] == '/' && path[1] != '/' && path[1] != '\\';
}
protected override async Task HandleForbiddenAsync(AuthenticationProperties properties)
{
var returnUrl = properties.RedirectUri;
if (string.IsNullOrEmpty(returnUrl))
{
returnUrl = OriginalPathBase + OriginalPath + Request.QueryString;
}
var accessDeniedUri = Options.AccessDeniedPath + QueryString.Create(Options.ReturnUrlParameter, returnUrl);
var redirectContext = new RedirectContext<CookieAuthenticationOptions>(Context, Scheme, Options, properties, BuildRedirectUri(accessDeniedUri));
await Events.RedirectToAccessDenied(redirectContext);
}
protected override async Task HandleChallengeAsync(AuthenticationProperties properties)
{
var redirectUri = properties.RedirectUri;
if (string.IsNullOrEmpty(redirectUri))
{
redirectUri = OriginalPathBase + OriginalPath + Request.QueryString;
}
var loginUri = Options.LoginPath + QueryString.Create(Options.ReturnUrlParameter, redirectUri);
var redirectContext = new RedirectContext<CookieAuthenticationOptions>(Context, Scheme, Options, properties, BuildRedirectUri(loginUri));
await Events.RedirectToLogin(redirectContext);
}
private string GetTlsTokenBinding()
{
var binding = Context.Features.Get<ITlsTokenBindingFeature>()?.GetProvidedTokenBindingId();
return binding == null ? null : Convert.ToBase64String(binding);
}
}
}