使用 __RequestVerificationToken 对嵌套 ViewModel 进行远程验证
Remote Validation On Nested ViewModels With __RequestVerificationToken
我有一个名为 "LoginIndexViewModel" 的视图模型 class 用于登录剃须刀页面,包含登录、登录和忘记密码表单。它包含几个属性,每个属性都是单独的视图模型。这是 "LoginIndexViewModel" 视图模型:
public class LoginIndexViewModel
{
public LoginViewModel Login { get; set; }
public SignUpViewModel SignUp { get; set; }
public ForgetPasswordViewModel ForgetPassword { get; set; }
}
在 "SignUpViewModel" 上有一个具有远程验证的 属性,我想在操作方法调用之前检查防伪令牌。这是 "SignUpViewModel":
的正文
public class SignUpViewModel
{
.
.
.
[Display(Name = "Email *")]
[DataType(DataType.EmailAddress)]
[Required(ErrorMessage = "The Email Is Required.")]
[EmailAddress(ErrorMessage = "Invalid Email Address.")]
[RegularExpression("^[a-zA-Z0-9_\.-]+@([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,6}$", ErrorMessage = "Invalid Email Address.")]
[Remote("CheckUsername", "Account", ErrorMessage = "The Email Address Have Already Registered.", HttpMethod = "POST", **AdditionalFields = "__RequestVerificationToken")]
public string Username { get; set; }
.
.
.
}
我在操作方法 "CheckUsername" 和 @Html.AntiForgeryToken() 上面使用了 [ValidateAntiForgeryToken] 属性,如下所示:
[HttpPost]
[AjaxOnly]
[AllowAnonymous]
[ValidateAntiForgeryToken]
[OutputCache(Location = OutputCacheLocation.None, NoStore = true)]
public virtual async Task<JsonResult> CheckUsername([Bind(Prefix = "SignUp.UserName")]string userName)
{
return Json(await _userManager.CheckUsername(username), JsonRequestBehavior.AllowGet);
}
剃须刀视图:
@using (Html.BeginForm(MVC.Account.ActionNames.Register, MVC.Account.Name, FormMethod.Post, new { id = "RegisterForm", @class = "m-login__form m-form", role = "form" }))
{
@Html.AntiForgeryToken()
.
.
.
<div class="form-group">
@Html.TextBoxFor(m => m.SignUp.UserName, new { @class = "form-control", placeholder = "Email *", autocomplete = "off", @Value = "" })
<span class="helper">@Html.ValidationMessageFor(model => model.SignUp.UserName)</span>
</div>
.
.
.
}
问题是远程验证调用抛出异常:'The required anti-forgery form field "__RequestVerificationToken" is not present.'
根据 Mozilla 调试器工具,我发现 CheckUsername 使用 'SignUp.__RequestVerificationToken' 参数而不是 '__RequestVerificationToken' 调用,因此会引发异常。
有谁知道发生了什么以及为什么 __RequestVerificationToken 没有提供?
向 AdditionalFields
中定义的属性添加前缀是设计使然,由 jquery.validate.unobtrusive.js
添加。相关代码是在adapters.add("remote", ["url", "type", "additionalfields"], function (options) {
方法中添加前缀
有多种选择可以解决您的问题
根据现有源代码创建您自己的自定义(例如)[ValidateAntiForgeryTokenWithPrefix]
属性,但修改以从请求中删除任何前缀(不推荐)
进行您自己的 ajax 调用,而不是使用 [Remote]
属性 - 即处理文本框的 .blur()
事件以调用服务器方法,同时传递值和令牌,并在成功回调中更新 @Html.ValidationMessageFor()
生成的占位符,并处理 .keyup()
事件以清除任何消息。这确实具有性能优势,因为在初始验证之后,remote
规则会对每个 keyup()
事件进行 ajax 调用,因此它会导致大量服务器和数据库调用。
然而,最简单的解决方案是基于 LoginViewModel
、SignUpViewModel
和 ForgetPasswordViewModel
创建 3 个部分,然后使用 @Html.Partial()
从主视图调用,例如
_SignUpViewModel.cshtml
@model SignUpViewModel
....
@using (Html.BeginForm(MVC.Account.ActionNames.Register, MVC.Account.Name, FormMethod.Post, new { id = "RegisterForm", @class = "m-login__form m-form", role = "form" }))
{
@Html.AntiForgeryToken()
....
<div class="form-group">
@Html.TextBoxFor(m => m.UserName, new { @class = "form-control", placeholder = "Email *", autocomplete = "off" })
<span class="helper">
@Html.ValidationMessageFor(model => model.UserName)
</span>
</div>
....
}
并在主视图中
@model LoginIndexViewModel
....
@Html.Partial("_SignUpViewModel", Model.SignUp)
.... // ditto for Login and ForgetPassword properties
然后您可以从 CheckUsername()
方法中省略 [Bind(Prefix = "SignUp.UserName")]
。
或者,您可以根据 LoginViewModel
创建主视图,然后使用 @Html.Action()
调用 [ChildActionOnly]
方法,return 其他 2 种形式的部分视图。
话虽如此,用户只会使用一次 SignUp
表单,并且可能永远不会使用 ForgetPassword
表单,因此包含所有额外的 html 只会降低性能,并且最好有链接将它们重定向到 SignUp
和 ForgetPassword
的单独页面,或者如果您希望它们在同一页面中,然后使用 [=64= 按需为它们加载部分内容].
作为旁注,您不需要 return Json(...)
中的 JsonRequestBehavior.AllowGet
参数(它是 [HttpPost]
方法),并且您永远不应该设置 value
使用 HtmlHelper
方法时的属性。
我有一个名为 "LoginIndexViewModel" 的视图模型 class 用于登录剃须刀页面,包含登录、登录和忘记密码表单。它包含几个属性,每个属性都是单独的视图模型。这是 "LoginIndexViewModel" 视图模型:
public class LoginIndexViewModel
{
public LoginViewModel Login { get; set; }
public SignUpViewModel SignUp { get; set; }
public ForgetPasswordViewModel ForgetPassword { get; set; }
}
在 "SignUpViewModel" 上有一个具有远程验证的 属性,我想在操作方法调用之前检查防伪令牌。这是 "SignUpViewModel":
的正文public class SignUpViewModel
{
.
.
.
[Display(Name = "Email *")]
[DataType(DataType.EmailAddress)]
[Required(ErrorMessage = "The Email Is Required.")]
[EmailAddress(ErrorMessage = "Invalid Email Address.")]
[RegularExpression("^[a-zA-Z0-9_\.-]+@([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,6}$", ErrorMessage = "Invalid Email Address.")]
[Remote("CheckUsername", "Account", ErrorMessage = "The Email Address Have Already Registered.", HttpMethod = "POST", **AdditionalFields = "__RequestVerificationToken")]
public string Username { get; set; }
.
.
.
}
我在操作方法 "CheckUsername" 和 @Html.AntiForgeryToken() 上面使用了 [ValidateAntiForgeryToken] 属性,如下所示:
[HttpPost]
[AjaxOnly]
[AllowAnonymous]
[ValidateAntiForgeryToken]
[OutputCache(Location = OutputCacheLocation.None, NoStore = true)]
public virtual async Task<JsonResult> CheckUsername([Bind(Prefix = "SignUp.UserName")]string userName)
{
return Json(await _userManager.CheckUsername(username), JsonRequestBehavior.AllowGet);
}
剃须刀视图:
@using (Html.BeginForm(MVC.Account.ActionNames.Register, MVC.Account.Name, FormMethod.Post, new { id = "RegisterForm", @class = "m-login__form m-form", role = "form" }))
{
@Html.AntiForgeryToken()
.
.
.
<div class="form-group">
@Html.TextBoxFor(m => m.SignUp.UserName, new { @class = "form-control", placeholder = "Email *", autocomplete = "off", @Value = "" })
<span class="helper">@Html.ValidationMessageFor(model => model.SignUp.UserName)</span>
</div>
.
.
.
}
问题是远程验证调用抛出异常:'The required anti-forgery form field "__RequestVerificationToken" is not present.'
根据 Mozilla 调试器工具,我发现 CheckUsername 使用 'SignUp.__RequestVerificationToken' 参数而不是 '__RequestVerificationToken' 调用,因此会引发异常。
有谁知道发生了什么以及为什么 __RequestVerificationToken 没有提供?
向 AdditionalFields
中定义的属性添加前缀是设计使然,由 jquery.validate.unobtrusive.js
添加。相关代码是在adapters.add("remote", ["url", "type", "additionalfields"], function (options) {
方法中添加前缀
有多种选择可以解决您的问题
根据现有源代码创建您自己的自定义(例如)[ValidateAntiForgeryTokenWithPrefix]
属性,但修改以从请求中删除任何前缀(不推荐)
进行您自己的 ajax 调用,而不是使用 [Remote]
属性 - 即处理文本框的 .blur()
事件以调用服务器方法,同时传递值和令牌,并在成功回调中更新 @Html.ValidationMessageFor()
生成的占位符,并处理 .keyup()
事件以清除任何消息。这确实具有性能优势,因为在初始验证之后,remote
规则会对每个 keyup()
事件进行 ajax 调用,因此它会导致大量服务器和数据库调用。
然而,最简单的解决方案是基于 LoginViewModel
、SignUpViewModel
和 ForgetPasswordViewModel
创建 3 个部分,然后使用 @Html.Partial()
从主视图调用,例如
_SignUpViewModel.cshtml
@model SignUpViewModel
....
@using (Html.BeginForm(MVC.Account.ActionNames.Register, MVC.Account.Name, FormMethod.Post, new { id = "RegisterForm", @class = "m-login__form m-form", role = "form" }))
{
@Html.AntiForgeryToken()
....
<div class="form-group">
@Html.TextBoxFor(m => m.UserName, new { @class = "form-control", placeholder = "Email *", autocomplete = "off" })
<span class="helper">
@Html.ValidationMessageFor(model => model.UserName)
</span>
</div>
....
}
并在主视图中
@model LoginIndexViewModel
....
@Html.Partial("_SignUpViewModel", Model.SignUp)
.... // ditto for Login and ForgetPassword properties
然后您可以从 CheckUsername()
方法中省略 [Bind(Prefix = "SignUp.UserName")]
。
或者,您可以根据 LoginViewModel
创建主视图,然后使用 @Html.Action()
调用 [ChildActionOnly]
方法,return 其他 2 种形式的部分视图。
话虽如此,用户只会使用一次 SignUp
表单,并且可能永远不会使用 ForgetPassword
表单,因此包含所有额外的 html 只会降低性能,并且最好有链接将它们重定向到 SignUp
和 ForgetPassword
的单独页面,或者如果您希望它们在同一页面中,然后使用 [=64= 按需为它们加载部分内容].
作为旁注,您不需要 return Json(...)
中的 JsonRequestBehavior.AllowGet
参数(它是 [HttpPost]
方法),并且您永远不应该设置 value
使用 HtmlHelper
方法时的属性。