ASP.NET MVC 使用防伪造令牌来避免CSRF攻击
本文转自这篇文章
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()); } } }
- 浅谈ASP.NET MVC 防止跨站请求伪造(CSRF)攻击的实现方法
- 我要学ASP.NET MVC 3.0(十三): MVC 3.0 防止跨站点请求伪造 (CSRF) 攻击
- ASP.NET MVC 3.0(十三): MVC 3.0 防止跨站点请求伪造 (CSRF) 攻击
- 我要学ASP.NET MVC 3.0(十三): MVC 3.0 防止跨站点请求伪造 (CSRF) 攻击
- ASP.NET MVC 防止跨站请求伪造(CSRF)攻击的方法
- ASP.NET MVC 防止CSRF攻击
- asp.net MVC中防止跨站请求攻击(CSRF)的ajax用法
- aspnet mvc使用@Html.AntiForgeryToken()防止跨站攻击
- ASP.NET Core 防止跨站请求伪造(XSRF/CSRF)攻击
- ASP.NET MVC4/5 - Ajax 防止 CSRF攻击
- 什么是CSRF攻击,如何在ASP.NET MVC网站中阻止这种攻击?
- 【MVC整理】4.Asp.net MVC 如何防止CSRF攻击
- 如何在asp.net mvc框架及django框架下面避免CSRF
- Asp.net MVC 如何防止CSRF攻击
- ASP.NET Core 防止跨站请求伪造(XSRF\/CSRF)攻击
- ASP.NET MVC中防止跨站请求攻击(CSRF)
- 图文并茂的讲解怎么在asp.net mvc里使用structureMap
- 使用Asp.net mvc + Linq + mvc_scaffold_gen_setup.exe 生成一个完整的家庭帐册大管家程序 之二
- ASP.NET MVC中使用ASP.NET AJAX异步访问WebService
- 使用Jquery应用到Asp.net ajax中时3个误区应该避免