目录
详解Asp.Net Core中的Cookie策略
这篇主要介绍Microsoft.AspNetCore.CookiePolicy这个类库的作用。
功能介绍
- 实现
IResponseCookies
接口,添加、删除cookie时加入自定义控制方法,并支持全局cookie属性设置。 - 实现
CookieOptions.IsEssential
的功能,该属性标识当前属性是否必须的或是否绕过ITrackingConsentFeature
的检查。 - 实现
ITrackingConsentFeature
接口,该接口主要是向cookie中添加并检索用户确认设置。
使用Cookie策略
Asp.Net Core是一个高度组件化的框架,很多功能比如授权,认证,回话状态等都是通过中间件的方式引入的,而Microsoft.AspNetCore.CookiePolicy扩展也是通过中间件的方式引入的。
在项目的Startup中添加如下代码:
public class Startup
{
public void Configure(IApplicationBuilder app)
{
...
//cookie策略提供了UseCookiePolicy的两个重载版本
app.UseCookiePolicy();
//app.UseCookiePolicy(new CookiePolicyOptions
//{
// CheckConsentNeeded = _ => true,
// HttpOnly = Microsoft.AspNetCore.CookiePolicy.HttpOnlyPolicy.None,
// MinimumSameSitePolicy = SameSiteMode.Strict,
// Secure = CookieSecurePolicy.SameAsRequest,
// OnAppendCookie = (context) =>
// {
// context.IssueCookie = true;
// },
// OnDeleteCookie = (context) =>
// {
// }
//});
...
app.UseMvc();
}
}
从UseCookiePolicy方法入手
打开CookiePolicyAppBuilderExtensions文件,可以看到有两个UseCookiePolicy
方法的重载版本,我们先从这个无参的UseCookiePolicy
开始介绍,在无参方法中通过UseMiddleware方法添加了一个CookiePolicyMiddleware
中间件,如下代码:
//C#扩展类,可以为已有类库添加扩展方法
public static class CookiePolicyAppBuilderExtensions
{
//为IApplicationBuilder添加扩展方法
public static IApplicationBuilder UseCookiePolicy(this IApplicationBuilder app)
{
...
//为http管道添加中间件
return app.UseMiddleware<CookiePolicyMiddleware>();
}
}
下面我们看中间件CookiePolicyMiddleware做了些什么。详解中间件
通过context.Features.Set
方法,分别设置了IResponseCookiesFeature,ITrackingConsentFeature的实现类
public class CookiePolicyMiddleware
{
public Task Invoke(HttpContext context)
{
var feature = context.Features.Get<IResponseCookiesFeature>() ?? new ResponseCookiesFeature(context.Features);
var wrapper = new ResponseCookiesWrapper(context, Options, feature, _logger);
//这个类中我们主要看下面这两个Set
context.Features.Set<IResponseCookiesFeature>(new CookiesWrapperFeature(wrapper));
//实现gdrp
context.Features.Set<ITrackingConsentFeature>(wrapper);
return _next(context);
}
//IResponseCookiesFeature实现
private class CookiesWrapperFeature : IResponseCookiesFeature
{
public CookiesWrapperFeature(ResponseCookiesWrapper wrapper)
{
Cookies = wrapper;
}
public IResponseCookies Cookies { get; }
}
}
大家可能注意到context.Features
这个对象,如果不明白请移步详解Features。
通过Set方法将IReponseCookiesFeature
的实现设置成了CookiesWrapperFeature
,CookiesWrapperFeature
有一个参数类型为RespnseCookiesWrapper的构造函数,而这个类实现了两个接口,IResponseCookies,ITrackingConsentFeature ,接下来我们来介绍这两个接口的作用。
实现IResponseCookies接口
通过IReponseCookies
接口ResponseCooiesWraper
重写了Append、Delete方法,接下来我们看看它是如何实现的:
internal class ResponseCookiesWrapper : IResponseCookies, ITrackingConsentFeature
{
private CookiePolicyOptions Options { get; }
public ResponseCookiesWrapper(HttpContext context, CookiePolicyOptions options, IResponseCookiesFeature feature, ILogger logger)
{
...
Options = options;
...
}
public void Append(string key, string value)
{
//如果我们绑定了OnAppendCookie事件,则每次添加cookie的时候都会触发该事件
if (CheckPolicyRequired() || Options.OnAppendCookie != null)
{
Append(key, value, new CookieOptions());
}
else
{
Cookies.Append(key, value);
}
}
public void Append(string key, string value, CookieOptions options)
{
if (options == null)
{
throw new ArgumentNullException(nameof(options));
}
//使用全局cookie配置修改options
if (ApplyAppendPolicy(ref key, ref value, options))
{
Cookies.Append(key, value, options);
}
else
{
_logger.CookieSuppressed(key);
}
}
//这个方法判断如果cookie不能被跟踪、设置了相同站点要求、设置了cookie只读、设置cookie安全等任何一个选项则采用Cookie策略的实现
private bool CheckPolicyRequired()
{
return !CanTrack
|| Options.MinimumSameSitePolicy != SameSiteMode.None
|| Options.HttpOnly != HttpOnlyPolicy.None
|| Options.Secure != CookieSecurePolicy.None;
}
private bool ApplyAppendPolicy(ref string key, ref string value, CookieOptions options)
{
//如果启用了跟踪或这个cookie是必须的
var issueCookie = CanTrack || options.IsEssential;
//这里去修改options的配置为全局cookies配置
ApplyPolicy(key, options);
//如果我们绑定了添加cookie事件
if (Options.OnAppendCookie != null)
{
var context = new AppendCookieContext(Context, options, key, value)
{
IsConsentNeeded = IsConsentNeeded,
HasConsent = HasConsent,
IssueCookie = issueCookie,
};
//这里执行我们在statup中绑定的方法
Options.OnAppendCookie(context);
key = context.CookieName;
value = context.CookieValue;
//通过设置context.IssueCookie为true可以将cookie写入到浏览器cookie
issueCookie = context.IssueCookie;
}
return issueCookie;
}
//Delete方法的实现与Append方法相同,具体请查看Append方法中的注释
public void Delete(string key)
{
...
}
public void Delete(string key, CookieOptions options)
{
...
}
//使用全局cookie配置替换options参数对应的属性
private void ApplyPolicy(string key, CookieOptions options)
{
switch (Options.Secure)
{
case CookieSecurePolicy.Always:
if (!options.Secure)
{
options.Secure = true;
_logger.CookieUpgradedToSecure(key);
}
break;
case CookieSecurePolicy.SameAsRequest:
// Never downgrade a cookie
if (Context.Request.IsHttps && !options.Secure)
{
options.Secure = true;
_logger.CookieUpgradedToSecure(key);
}
break;
case CookieSecurePolicy.None:
break;
default:
throw new InvalidOperationException();
}
switch (Options.MinimumSameSitePolicy)
{
case SameSiteMode.None:
break;
case SameSiteMode.Lax:
if (options.SameSite == SameSiteMode.None)
{
options.SameSite = SameSiteMode.Lax;
_logger.CookieSameSiteUpgraded(key, "lax");
}
break;
case SameSiteMode.Strict:
if (options.SameSite != SameSiteMode.Strict)
{
options.SameSite = SameSiteMode.Strict;
_logger.CookieSameSiteUpgraded(key, "strict");
}
break;
default:
throw new InvalidOperationException($"Unrecognized {nameof(SameSiteMode)} value {Options.MinimumSameSitePolicy.ToString()}");
}
switch (Options.HttpOnly)
{
case HttpOnlyPolicy.Always:
if (!options.HttpOnly)
{
options.HttpOnly = true;
_logger.CookieUpgradedToHttpOnly(key);
}
break;
case HttpOnlyPolicy.None:
break;
default:
throw new InvalidOperationException($"Unrecognized {nameof(HttpOnlyPolicy)} value {Options.HttpOnly.ToString()}");
}
}
}
通过代码,我们可以看出,给CookiePolicyOptions
对象的OnAppendCookie
、OnDeleteCookie
的属性设置方法可以实现在每次添加、删除cookie时触发改方法的调用,通常我们可以在这里面做一些全局的过滤,配置什么,最终我们需要设置CookiePolicyOptions.IssueCookie
来确认是否要将改cookie发送到浏览器中。
实现ITrackingConsentFeature接口
该接口定义了cookie在什么情况才会被跟踪,并将在浏览器中设置对应的cookie值。
public interface ITrackingConsentFeature
{
bool IsConsentNeeded { get; }
bool HasConsent { get; }
bool CanTrack { get; }
void GrantConsent();
void WithdrawConsent();
string CreateConsentCookie();
}
internal class ResponseCookiesWrapper : IResponseCookies, ITrackingConsentFeature
{
//是否需要用户确认显式确认
public bool IsConsentNeeded
{
get
{
if (!_isConsentNeeded.HasValue)
{
_isConsentNeeded = Options.CheckConsentNeeded == null ? false
//从CookiePolicyOptions的CheckConsentNeeded方法获取是否需要用户确认操作
: Options.CheckConsentNeeded(Context);
_logger.NeedsConsent(_isConsentNeeded.Value);
}
return _isConsentNeeded.Value;
}
}
//判断用户之前是否开启了确认
public bool HasConsent
{
get
{
if (!_hasConsent.HasValue)
{
//从我们之前设置的cookie中获取确认cookie的值
var cookie = Context.Request.Cookies[Options.ConsentCookie.Name];
_hasConsent = string.Equals(cookie, ConsentValue, StringComparison.Ordinal);
_logger.HasConsent(_hasConsent.Value);
}
return _hasConsent.Value;
}
}
//能否跟踪,如果浏览器没有开启用户确认或者浏览器已经确认过了则允许跟踪
public bool CanTrack => !IsConsentNeeded || HasConsent;
//向cookie中写入跟踪启用标识
public void GrantConsent()
{
if (!HasConsent && !Context.Response.HasStarted)
{
var cookieOptions = Options.ConsentCookie.Build(Context);
Append(Options.ConsentCookie.Name, ConsentValue, cookieOptions);
_logger.ConsentGranted();
}
_hasConsent = true;
}
//撤销之前cookie中写入的跟踪启用标识
public void WithdrawConsent()
{
if (HasConsent && !Context.Response.HasStarted)
{
var cookieOptions = Options.ConsentCookie.Build(Context);
//删除之前的cookie确认信息
Delete(Options.ConsentCookie.Name, cookieOptions);
_logger.ConsentWithdrawn();
}
_hasConsent = false;
}
//返回跟踪cookie的字符串值
public string CreateConsentCookie()
{
var key = Options.ConsentCookie.Name;
var value = ConsentValue;
var options = Options.ConsentCookie.Build(Context);
ApplyAppendPolicy(ref key, ref value, options);
var setCookieHeaderValue = new Net.Http.Headers.SetCookieHeaderValue(
Uri.EscapeDataString(key),
Uri.EscapeDataString(value))
{
Domain = options.Domain,
Path = options.Path,
Expires = options.Expires,
MaxAge = options.MaxAge,
Secure = options.Secure,
SameSite = (Net.Http.Headers.SameSiteMode)options.SameSite,
HttpOnly = options.HttpOnly
};
return setCookieHeaderValue.ToString();
}
}
CookiePolicyOptions类的功能
还记得UseCookiePolicy
方法有一个参数的重载版本吗?没错,这个参数就是CookiePolicyOptions类型。
通过配置CookiePolicyOptions
我们可以设置一些全局的cookie约定信息,并允在每次添加、删除cookie时触发指定的方法已完成一些特殊的cookie配置。CookiePolicyOptions
代码如下:
public class CookiePolicyOptions
{
//设置全局cookie通用配置
public SameSiteMode MinimumSameSitePolicy { get; set; } = SameSiteMode.Lax;
public HttpOnlyPolicy HttpOnly { get; set; } = HttpOnlyPolicy.None;
public CookieSecurePolicy Secure { get; set; } = CookieSecurePolicy.None;
//设置gdpr在浏览器中保存的cookie信息
public CookieBuilder ConsentCookie { get; set; } = new CookieBuilder()
{
Name = ".AspNet.Consent",
Expiration = TimeSpan.FromDays(365),
IsEssential = true,
};
//是否需要用户确认才能将部分cookie发送到客户端
public Func<HttpContext, bool> CheckConsentNeeded { get; set; }
//当添加cookie时触发该事件
public Action<AppendCookieContext> OnAppendCookie { get; set; }
//当删除cookie时触发该事件
public Action<DeleteCookieContext> OnDeleteCookie { get; set; }
}
该类是Microsoft.AspNetCore.CookiePolicy中的一个重要类,我需要的cookie修改监控,gdrp配置等都需要靠该类实现。
总结
- cookie策略通过继承
IResponseCookies
接口,可以实现添加、删除的功能 - 通过
CookiePolicyOptions
类,我们可以修改cookie的全局配置,并在添加、删除cookie时接受到通知,然后做一些你希望做的任何事情 - cookie策略通过继承
ITrackingConsentFeature
接口,可以实现检索、设置cookie的跟踪配置,改配置主要用于GDPR