您的位置:首页 > 编程语言 > ASP

ASP.NET MVC 使用防伪造令牌来避免CSRF攻击

2019-06-24 20:43 1146 查看

本文转自这篇文章 

XSRF即在访问B站点的时候,执行了A站点的功能。 
比如: 
A站点登录后,可以修改用户的邮箱(接口:/Email/Modify?email=123),修改邮箱时只验证用户有没有登录,而且登录信息是保存在cookie中。 
用户登录A站点后,又打开一个窗口访问B站点,如果这时B站点内嵌入了一条链接http://www.A.com/Email/Modify?email=123,当用户点击这条链接时会直接修改A站点的用户邮箱。

对表达提交来说,要关注的就是安全问题。ASP.NET MVC 提供了探测某种攻击类型的机制,其中一个措施就是防伪造令牌。这种令牌包含服务器端和客户端组件,代码会在表单中插入一个隐藏域以保护用户特定的令牌:

[code]@Html.AntiForgeryToken()

在执行@Html.AntiForgeryToken()语句时,会在cookie中写入一个经过加密后的数据,并在页面中添加一个隐藏域并写入加密后的数据(默认名称为__RequestVerificationToken)。当执行IndexPost(前面示例)方法前,会判断cookie中的数据与隐藏域的数据是否相等。相等则验证通过。否则会抛出异常。(Post请求会自动把隐藏域传递到后台,如果是Get请求,就需要手动把隐藏域的值传递到后台)。 
待加密的数据是一个AntiForgeryToken对象。系统进行验证时,会先把加密的数据还原成AntiForgeryToken对象,对象有一个SecurityToken属性(用于填充随机序列),系统主要判断该字段的值是否相等。 
同一个会话期间,SecurityToken数据相同,所以即使开多个tab访问相同页面,数据验证也会通过。 
同一个会话期间cookie中的加密数据不会改变,因为访问页面时,cookie会传到后台,后台判断cookie中有加密数据,就不会重新生成cookie数据。但隐藏域的值每次都不同,因为每访问一次页面,都会重新加密一次,虽然AntiForgeryToken对象的值相同,但通过MachineKey的Protect加密后,每次加密的值都会不同。 
AntiForgery使用MachineKey进行加密,所以如果系统使用负载均衡,就需要配置MachineKey,否则不同服务器的MachineKey不同,导致无法解密。

[code][ValidateAntiForgeryToken]
public ActionResult Login(LoginModel model)
{
//ect.
}

在执行@Html.AntiForgeryToken()语句时,会调用GetHtml方法。GetHtml方法中会调用GetFormInputElement方法,该方法会在cookie中写入加密后的数据,并返回Html标签代码。该标签代码会写入到页面中. 

[code]        public static HtmlString GetHtml()
{
if (HttpContext.Current == null)
{
throw new ArgumentException(WebPageResources.HttpContextUnavailable);
}

TagBuilder retVal = _worker.GetFormInputElement(new HttpContextWrapper(HttpContext.Current));
return retVal.ToHtmlString(TagRenderMode.SelfClosing);
}

在GetFormInputElement方法中,首先通过GetCookieTokenNoThrow方法获取Cookie中AntiForgeryToken对象(第一访问页面该对象为空)。再通过GetTokens方法获取新的newCookieToken以及formToken(newCookieToken就是写入cookie的token,formToken就是写入隐藏域的token)。如果oldCookieToken不为空,那么newCookieToken就会为空,这样就不会重新写入cookie。所以同一个会话期间cookie值会相同。如果不为空就通过SaveCookieToken方法写入cookie。

[code]        public TagBuilder GetFormInputElement(HttpContextBase httpContext)
{
CheckSSLConfig(httpContext);

AntiForgeryToken oldCookieToken = GetCookieTokenNoThrow(httpContext);
AntiForgeryToken newCookieToken, formToken;
GetTokens(httpContext, oldCookieToken, out newCookieToken, out formToken);

if (newCookieToken != null)
{
// If a new cookie was generated, persist it.
_tokenStore.SaveCookieToken(httpContext, newCookieToken);
}

if (!_config.SuppressXFrameOptionsHeader)
{
// Adding X-Frame-Options header to prevent ClickJacking. See
// http://tools.ietf.org/html/draft-ietf-websec-x-frame-options-10
// for more information.
httpContext.Response.AddHeader("X-Frame-Options", "SAMEORIGIN");
}

// <input type="hidden" name="__AntiForgeryToken" value="..." />
TagBuilder retVal = new TagBuilder("input");
retVal.Attributes["type"] = "hidden";
retVal.Attributes["name"] = _config.FormFieldName;
retVal.Attributes["value"] = _serializer.Serialize(formToken);
return retVal;
}

SaveCookieToken方法先通过Serialize方法序列化,序列化的时候会对数据加密,再写入cookie  

[code]        public void SaveCookieToken(HttpContextBase httpContext, AntiForgeryToken token)
{
string serializedToken = _serializer.Serialize(token);
HttpCookie newCookie = new HttpCookie(_config.CookieName, serializedToken)
{
HttpOnly = true
};

// Note: don't use "newCookie.Secure = _config.RequireSSL;" since the default
// value of newCookie.Secure is automatically populated from the <httpCookies>
// config element.
if (_config.RequireSSL)
{
newCookie.Secure = true;
}

httpContext.Response.Cookies.Set(newCookie);
}

GetTokens方法,如果oldCookieToken不为空,就不重新生成newCookieToken。为空则通过GenerateCookieToken方法生成一个Token。再调用GenerateFormToken方法生成formToken 

[code]        private void GetTokens(HttpContextBase httpContext, AntiForgeryToken oldCookieToken, out AntiForgeryToken newCookieToken, out AntiForgeryToken formToken)
{
newCookieToken = null;
if (!_validator.IsCookieTokenValid(oldCookieToken))
{
// Need to make sure we're always operating with a good cookie token.
oldCookieToken = newCookieToken = _validator.GenerateCookieToken();
}

Contract.Assert(_validator.IsCookieTokenValid(oldCookieToken));
formToken = _validator.GenerateFormToken(httpContext, ExtractIdentity(httpContext), oldCookieToken);
}

GenerateCookieToken方法生成cookieToken,即创建一个新的AntiForgeryToken对象。AntiForgeryToken有个SecurityToken属性,类型为BinaryBlob。BianryBlob对象会通过RNGCryptoServiceProvider实例的GetBytes方法填充强随机序列。填充的序列就是用来验证的随机数。即随机数是在创建AntiForgeryToken对象时自动生成的   

[code]        public AntiForgeryToken GenerateCook
4000
ieToken()
{
return new AntiForgeryToken()
{
// SecurityToken will be populated automatically.
IsSessionToken = true
};
}

GenerateFormToken方法,就是把cookieToken的SecurityToken赋值给formToken。这样就会使得cookieToken与formToken的SecurityToken值相等 

[code]        public AntiForgeryToken GenerateFormToken(HttpContextBase httpContext, IIdentity identity, AntiForgeryToken cookieToken)
{
Contract.Assert(IsCookieTokenValid(cookieToken));

AntiForgeryToken formToken = new AntiForgeryToken()
{
SecurityToken = cookieToken.SecurityToken,
IsSessionToken = false
};

bool requireAuthenticatedUserHeuristicChecks = false;
// populate Username and ClaimUid
if (identity != null && identity.IsAuthenticated)
{
if (!_config.SuppressIdentityHeuristicChecks)
{
// If the user is authenticated and heuristic checks are not suppressed,
// then Username, ClaimUid, or AdditionalData must be set.
requireAuthenticatedUserHeuristicChecks = true;
}

formToken.ClaimUid = _claimUidExtractor.ExtractClaimUid(identity);
if (formToken.ClaimUid == null)
{
formToken.Username = identity.Name;
}
}

// populate AdditionalData
if (_config.AdditionalDataProvider != null)
{
formToken.AdditionalData = _config.AdditionalDataProvider.GetAdditionalData(httpContext);
}

if (requireAuthenticatedUserHeuristicChecks
&& String.IsNullOrEmpty(formToken.Username)
&& formToken.ClaimUid == null
&& String.IsNullOrEmpty(formToken.AdditionalData))
{
// Application says user is authenticated, but we have no identifier for the user.
throw new InvalidOperationException(String.Format(CultureInfo.CurrentCulture,
WebPageResources.TokenValidator_AuthenticatedUserWithoutUsername, identity.GetType()));
}

return formToken;
}

生成cookieToken和formToken后就会调用Serialize方法进行序列化。序列化的时候会调用MachineKey的Protect方法进行加密。每次加密后的值都不相同。如果使用了负载均衡,一定要配置MachineKey,而不能使用系统的值 

[code] public string Serialize(AntiForgeryToken token)
{
Contract.Assert(token != null);

using (MemoryStream stream = new MemoryStream())
{
using (BinaryWriter writer = new BinaryWriter(stream))
{
writer.Write(TokenVersion);
writer.Write(token.SecurityToken.GetData());
writer.Write(token.IsSessionToken);

if (!token.IsSessionToken)
{
if (token.ClaimUid != null)
{
writer.Write(true /* isClaimsBased */);
writer.Write(token.ClaimUid.GetData());
}
else
{
writer.Write(false /* isClaimsBased */);
writer.Write(token.Username);
}

writer.Write(token.AdditionalData);
}

writer.Flush();
return _cryptoSystem.Protect(stream.ToArray());
}
}
}

 

 

 

 

 

 

 

 

 

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: