我想在MVC中创建一个ETag过滤器。 问题是我无法控制Response.OutputStream,如果我能够这样做,我只会根据结果流计算ETag。 我之前在WCF中做过这件事,但在MVC中找不到任何简单的想法。
我希望能够写出类似的东西
[ETag]
public ActionResult MyAction()
{
var myModel = Factory.CreateModel();
return View(myModel);
}
有什么想法吗?
答案 0 :(得分:27)
这是我能想到的最好的,我真的不明白你的意思是你无法控制Response.OutputStream。
using System;
using System.IO;
using System.Security.Cryptography;
using System.Web.Mvc;
public class ETagAttribute : ActionFilterAttribute
{
private string GetToken(Stream stream) {
MD5 md5 = MD5.Create();
byte [] checksum = md5.ComputeHash(stream);
return Convert.ToBase64String(checksum, 0, checksum.Length);
}
public override void OnResultExecuted(ResultExecutedContext filterContext)
{
filterContext.HttpContext.Response.AppendHeader("ETag", GetToken(filterContext.HttpContext.Response.OutputStream));
base.OnResultExecuted(filterContext);
}
}
这应该有效,但事实并非如此。
显然Microsoft覆盖了System.Web.HttpResponseStream.Read(Byte []缓冲区,Int32偏移量,Int32计数),因此它返回“不支持指定方法。”,不知道为什么他们会这样做,因为它继承了System.IO.Stream基类......
这是以下资源的混合,Response.OutputStream是一个只写流,所以我们必须使用Response.Filter类来读取输出流,有点古怪,你必须使用过滤器过滤器,但它的工作原理=)
http://bytes.com/topic/c-sharp/answers/494721-md5-encryption-question-communication-java
http://www.codeproject.com/KB/files/Calculating_MD5_Checksum.aspx
http://blog.gregbrant.com/post/Adding-Custom-HTTP-Headers-to-an-ASPNET-MVC-Response.aspx
http://www.infoq.com/articles/etags
http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html
<强>更新强>
经过多次战斗,我终于能够让它发挥作用了:
using System;
using System.IO;
using System.Security.Cryptography;
using System.Web;
using System.Web.Mvc;
public class ETagAttribute : ActionFilterAttribute {
public override void OnActionExecuting(ActionExecutingContext filterContext) {
try {
filterContext.HttpContext.Response.Filter = new ETagFilter(filterContext.HttpContext.Response);
} catch (System.Exception) {
// Do Nothing
};
}
}
public class ETagFilter : MemoryStream {
private HttpResponseBase o = null;
private Stream filter = null;
public ETagFilter (HttpResponseBase response) {
o = response;
filter = response.Filter;
}
private string GetToken(Stream stream) {
byte[] checksum = new byte[0];
checksum = MD5.Create().ComputeHash(stream);
return Convert.ToBase64String(checksum, 0, checksum.Length);
}
public override void Write(byte[] buffer, int offset, int count) {
byte[] data = new byte[count];
Buffer.BlockCopy(buffer, offset, data, 0, count);
filter.Write(data, 0, count);
o.AddHeader("ETag", GetToken(new MemoryStream(data)));
}
}
更多资源:
http://authors.aspalliance.com/aspxtreme/sys/Web/HttpResponseClassFilter.aspx
http://forums.asp.net/t/1380989.aspx/1
答案 1 :(得分:14)
非常感谢,这正是我想要的。 只是对ETagFilter做了一个小修复,如果内容没有改变,它将处理304
public class ETagAttribute : ActionFilterAttribute
{
public override void OnActionExecuting(ActionExecutingContext filterContext)
{
filterContext.HttpContext.Response.Filter = new ETagFilter(filterContext.HttpContext.Response, filterContext.RequestContext.HttpContext.Request);
}
}
public class ETagFilter : MemoryStream
{
private HttpResponseBase _response = null;
private HttpRequestBase _request;
private Stream _filter = null;
public ETagFilter(HttpResponseBase response, HttpRequestBase request)
{
_response = response;
_request = request;
_filter = response.Filter;
}
private string GetToken(Stream stream)
{
byte[] checksum = new byte[0];
checksum = MD5.Create().ComputeHash(stream);
return Convert.ToBase64String(checksum, 0, checksum.Length);
}
public override void Write(byte[] buffer, int offset, int count)
{
byte[] data = new byte[count];
Buffer.BlockCopy(buffer, offset, data, 0, count);
var token = GetToken(new MemoryStream(data));
string clientToken = _request.Headers["If-None-Match"];
if (token != clientToken)
{
_response.Headers["ETag"] = token;
_filter.Write(data, 0, count);
}
else
{
_response.SuppressContent = true;
_response.StatusCode = 304;
_response.StatusDescription = "Not Modified";
_response.Headers["Content-Length"] = "0";
}
}
}
答案 2 :(得分:2)
有很多有希望的答案。但它们都不是完整的解决方案。它也不是问题的一部分,没有人提到它。但 ETag应该用于缓存验证。因此,它应与Cache-Control标头一起使用。因此,客户端甚至不必在缓存过期之前调用服务器(可能是非常短的时间段取决于您的资源)。当缓存过期时,客户端使用ETag发出请求并验证它。有关缓存see this article的详细信息。
这是我的带有ETags的CacheControl属性解决方案。它可以改进,例如启用公共缓存等... 但是我强烈建议您了解缓存并仔细修改它。如果您使用HTTPS并且端点是安全的,则此设置应该没问题。
/// <summary>
/// Enables HTTP Response CacheControl management with ETag values.
/// </summary>
public class ClientCacheWithEtagAttribute : ActionFilterAttribute
{
private readonly TimeSpan _clientCache;
private readonly HttpMethod[] _supportedRequestMethods = {
HttpMethod.Get,
HttpMethod.Head
};
/// <summary>
/// Default constructor
/// </summary>
/// <param name="clientCacheInSeconds">Indicates for how long the client should cache the response. The value is in seconds</param>
public ClientCacheWithEtagAttribute(int clientCacheInSeconds)
{
_clientCache = TimeSpan.FromSeconds(clientCacheInSeconds);
}
public override async Task OnActionExecutedAsync(HttpActionExecutedContext actionExecutedContext, CancellationToken cancellationToken)
{
if (!_supportedRequestMethods.Contains(actionExecutedContext.Request.Method))
{
return;
}
if (actionExecutedContext.Response?.Content == null)
{
return;
}
var body = await actionExecutedContext.Response.Content.ReadAsStringAsync();
if (body == null)
{
return;
}
var computedEntityTag = GetETag(Encoding.UTF8.GetBytes(body));
if (actionExecutedContext.Request.Headers.IfNoneMatch.Any()
&& actionExecutedContext.Request.Headers.IfNoneMatch.First().Tag.Trim('"').Equals(computedEntityTag, StringComparison.InvariantCultureIgnoreCase))
{
actionExecutedContext.Response.StatusCode = HttpStatusCode.NotModified;
actionExecutedContext.Response.Content = null;
}
var cacheControlHeader = new CacheControlHeaderValue
{
Private = true,
MaxAge = _clientCache
};
actionExecutedContext.Response.Headers.ETag = new EntityTagHeaderValue($"\"{computedEntityTag}\"", false);
actionExecutedContext.Response.Headers.CacheControl = cacheControlHeader;
}
private static string GetETag(byte[] contentBytes)
{
using (var md5 = MD5.Create())
{
var hash = md5.ComputeHash(contentBytes);
string hex = BitConverter.ToString(hash);
return hex.Replace("-", "");
}
}
}
用法例如:1分钟客户端缓存:
[ClientCacheWithEtag(60)]
答案 3 :(得分:1)
这是我为解决这个问题而创建的代码 - 我从gzip继承,因为我也想要gzip流(你总是可以使用常规流) 不同之处在于,我计算了所有响应的etag,而不仅仅是它的大块。
public class ETagFilter : GZipStream
{
private readonly HttpResponseBase m_Response;
private readonly HttpRequestBase m_Request;
private readonly MD5 m_Md5;
private bool m_FinalBlock;
public ETagFilter(HttpResponseBase response, HttpRequestBase request)
: base(response.Filter, CompressionMode.Compress)
{
m_Response = response;
m_Request = request;
m_Md5 = MD5.Create();
}
protected override void Dispose(bool disposing)
{
m_Md5.Dispose();
base.Dispose(disposing);
}
private string ByteArrayToString(byte[] arrInput)
{
var output = new StringBuilder(arrInput.Length);
for (var i = 0; i < arrInput.Length; i++)
{
output.Append(arrInput[i].ToString("X2"));
}
return output.ToString();
}
public override void Write(byte[] buffer, int offset, int count)
{
m_Md5.TransformBlock(buffer, 0, buffer.Length, null, 0);
base.Write(buffer, 0, buffer.Length);
}
public override void Flush()
{
if (m_FinalBlock)
{
base.Flush();
return;
}
m_FinalBlock = true;
m_Md5.TransformFinalBlock(new byte[0], 0, 0);
var token = ByteArrayToString(m_Md5.Hash);
string clientToken = m_Request.Headers["If-None-Match"];
if (token != clientToken)
{
m_Response.Headers["ETag"] = token;
}
else
{
m_Response.SuppressContent = true;
m_Response.StatusCode = 304;
m_Response.StatusDescription = "Not Modified";
m_Response.Headers["Content-Length"] = "0";
}
base.Flush();
}
}