MVC万无一失的意思验证问题,怎么解决

public int OrderId { }
public System.DateTime OrderDate { }
public string Username { }
public string FirstName { }
public string LastName { }
public string Address { }
public string City { }
public string State { }
public string PostalCode { }
public string Country { }
public string Phone { }
public string Email { }
public decimal Total { }
public List&OrderDetail& OrderDetails { }
       应用程序使用 HTML 辅助方法 EditorForModel 来构建结算页面,下面是视图中的代码:
&fieldset&
&legend&Shipping Information&/legend&
@Html.EditorForModel()
&/fieldset&
       EditForModel 辅助方法为对象的每个属性构建一个它认为合适的编辑器(这类型的编程做法适用于快速开发且对页面布局和美观度要求不高的内部小项目)。这个表单存在一些明显的问题。比如 OderId 和 OderDate 编辑器,这些值并不需要用户填写,应用程序会在服务器端设置。FirstName 属性对程序员有意义,而客户会认为难道少数如一个空格?(指正确写法是 First Name)
验证注解的使用
       数据注解特性定义在命名空间 System.ComponentModel.DataAnnotations 中(并不全是,下面会讲到),它们提供了服务器端的验证功能,当在模型的属性上使用这些特性时,框架也支持客户端验证。
Required:当属性为 null 或者为空时,Required 特性会引发一个验证错误。客户的姓氏和名字都是必需的,所以可以在模型上添加此属性。
[Required]
public string FirstName { }
[Required]
public string LastName { }
       所有内置的验证特性既传递服务器端验证逻辑也传递客户端验证逻辑,即使客户端关闭了 JavaScript,服务器端也会引发验证。
       StringLength:如果试图向数据库插入一个超过最大长度的字符串,就会引发异常,这就是 StringLength 的用武之地。
[Required]
[StringLength(160)]
public string FirstName { }
[Required]
[StringLength(160)]
public string LastName { }
       MinimumLength 是一个可选参数,如果设置了,意味着该字符串的验证具有一个长度的范围区间。
[Required]
[StringLength(160, MinimumLength = 3)]
public string FirstName { }
       RegularExpression:一些属性要求的验证并非是简单的非空或长度范围,例如 Email 属性需要一个有效可用的 Email 地址(至少看上去是一个有效的地址)。这里我们使用正则表达式来使验证输入的 Email 地址:
[RegularExpression(@&[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}&)]
public string Email { }
       Range:该特性用来指定数值类型的最大值和最小值。
[Range(18,70)]
public int Age { }
System.Web.Mvc 下的验证特性
       ASP.NET MVC 框架还会应用程序在命名空间 System.Web.Mvc 中额外添加了两个验证特性。
       Remote:可以利用服务器端的回调函数执行客户端的验证逻辑。例如,系统中不允许两个用户具有相同的 UserName 值,但客户端验证是很难做到这一点的,使用 Remote 特性可以把 UserName 的值传到服务器(可以指定操作名和控制器名),然后在服务器端进行验证:
[Remote(&CheckUserName&,&Account&)]
public string Username { }
// Account 控制器中的验证方法
public JsonResult CheckUserName(string username)
var result = Membership.FindUsersByName(username).Count == 0;
return Json(result, JsonRequestBehavior.AllowGet);
       Compare:它用来确保模型对象的两个属性拥有相同的值。例如购物时,要求用户输入两次 Email 地址以确保用户输入无误:
[RegularExpression(@&[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}&)]
public string Email { }
[System.Web.Mvc.Compare(&Email&)]
public string EmailConfirm { }
自定义错误提示消息及本地化
       每个验证特性都允许传递一个带有自定义错误提示消息的参数。例如 Email 的原本错误提示消息是一个正则表达式,在客户看来就像是一串乱码,此时自定义错误消息就派上了用处:
[RegularExpression(@&[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,4}&,
ErrorMessage = &Email 格式不正确!&)]
public string Email { }
[System.Web.Mvc.Compare(&Email&, ErrorMessage = &两次输入的密码不同!&)]
public string EmailConfirm { }
       自定义的错误提示消息在字符串中也带有一个格式项。内置特性使用友好的属性显示名称格式化错误提示消息字符串,注意,是属性显示名称(后面会讲到),若无,则直接使用属性名填充字符串占位符。
[Required(ErrorMessage=&Your {0} is required.&)]
[StringLength(160, MinimumLength = 3)]
public string LastName { }
注解的后台原理
       ASP.NET MVC 的验证特性是由模型绑定器、模型元数据、模型验证器、模型状态组成的协调系统的一部分。默认情况下,ASP.NET MVC 框架在模型绑定时执行验证逻辑,当操作方法带有参数时,就会隐式的执行模型绑定,当然,也可以调用 UpdateModel 或 TryUpdateModel 方法显式执行模型绑定。
模型绑定器一旦使用新值完成对模型属性的更新,就会利用当前的模型元数据获得模型的所有验证器。
ASP.NET MVC 运行时提供了一个验证器 DataAnnotationsModelValidator 来于数据注解一同工作,它会找到所有的验证特性并执行验证逻辑。
模型绑定器捕获所有失败的验证规则并把它们放入模型状态中。
       模型绑定主要的副产品是模型状态 ModelState,模型状态不仅包含了用户想放入模型属性里的值,也包括与每个属性相关联的所有错误。假设用户在没有填写 LastName 值的情况下提交了表单,由于设置了 Required 验证注解特性,因此模型绑定之后,下面所有的表达式都将返回 false:
flag = ModelState.IsV
flag = ModelState.IsValidField(&LastName&);
flag = ModelState[&LastName&].Errors.Count & 0;
       也可以在模型状态中查看与失败验证相关的错误提示消息:
var errorMsg = ModelState[&LastName&].Errors[0].ErrorM
       很少会编写代码来查看特定的错误消息。之前的 HTML 辅助方法中也介绍过,辅助方法可以利用模型状态(和模型状态中出现的错误)来改变模型在视图中的显示。例如,ValidationMessage 辅助方法可以通过查看模型状态来显示与特定部分视图数据相关的错误消息:
@Html.ValidationMessageFor(m =& m.LastName)
控制器操作和验证错误
       控制器决定了在模型验证失败和成功时的执行流程。当验证成功时,操作通常会执行必要的步骤来保存和更新客户的信息;当验证失败时,操作一般会重新渲染提交模型值的视图。这样就可以让用户看到所有的验证错误提示消息,并按照提示进行更正或补填遗漏的字段信息。
[HttpPost]
public ActionResult AddressAndPayment(Order newOrder)
if (ModelState.IsValid)
newOrder.Username = User.Identity.N
newOrder.OrderDate = DateTime.N
// Store DB and do something...
return RedirectToAction(&Complete&, new { id = newOrder.OrderId });
return View(newOrder);
       注意,如果模型状态无效,操作就会重新渲染 AddressAndPayment 视图,给用户一个修正错误并重新提交表单的机会。
自定义验证逻辑
       数据注解特性给验证带来了简易性和透明性,但也不可能满足程序中可能遇到的所有验证场合。
       ASP.NET MVC 框架的扩展性意味着实现自定义验证逻辑有着很大的可行性。这里重点介绍两个核心应用方法:
将验证逻辑封装在自定义的数据注解中。
将验证逻辑封装在模型对象中。
       封装在自定义数据注解中,则可以轻松实现在多个模型中重用逻辑,当然,这需要在特性内部编写代码以应对不同类型的模型,但一旦实现,新的注解就可以在多处重用。
       将验证逻辑直接放入模型对象中,就意味着验证逻辑可以很容易的编码实现,因为这样只需关心一种模型对象的验证逻辑,但不利于重用。
自定义注解
       假设要限制客户输入姓氏中单词的数量,并让这种验证在其他模型中重用,那就可以考虑将验证逻辑封装在一个可重用的特性中。所有的验证注解特性最终都派生自基类 ValidationAttribute,它是个抽象类,在命名空间 System.ComponentModel.DataAnnotations 中定义,程序员的验证逻辑也必须派生自该类。
using System.ComponentModel.DataA
namespace MvcMusicStore.ExtendValidationAttribute
public class MaxWordsAttribute : ValidationAttribute
/// &summary&
/// 为了实现验证逻辑,至少需要重新基类中 IsValid 方法的其中一个版本。
/// 重写 IsValid 方法时可以利用的 ValidationContext 参数提供了很多可在 IsValid
/// 内部使用的信息,如模型类型、模型对象实例、用来验证属性的人性化显示名称以及其他一些信息
/// &/summary&
/// &param name=&value&&&/param&
/// &returns&&/returns&
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
return ValidationResult.S
       方法的第一个参数是要验证的对象的值,如果这个值有效,就可以返回一个成功的验证结果。在本例中,在判断它是否有效时,需要知道欲限制单词数量的上限,要获得这个上限,可通过创建构造函数要求顾客把最大单词数作为一个参数传递给它:
public class MaxWordsAttribute : ValidationAttribute
private readonly int _maxW
public MaxWordsAttribute(int maxWords)
this._maxWords = maxW
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
if (value != null)
var valueString = value.ToString();
if (valueString.Split(' ').Length & this._maxWords)
return new ValidationResult(&Too many words!&);
return ValidationResult.S
       现在已经实现了验证的逻辑,但上述代码的问题在于那行返回硬编码错误消息的代码。使用数据注解的开发人员希望可以使用 ValidationAttribute 的 ErrorMessage 属性来自定义错误消息,同时还要与其它验证特性一样,提供一个默认的错误提示消息(在开发人员没有提供自定义的错误提示消息时使用),并且还要利用验证的属性名称生成错误提示消息:
public class MaxWordsAttribute : ValidationAttribute
private readonly int _maxW
public MaxWordsAttribute(int maxWords)
: base(&{0} has too many words.&)
// 设置默认值,如果特性不指明错误提示消息的话
// 如果特性显式的传递了错误消息,那么上面的字符串会被替换
// 注意,显式传递错误消息也是带有占位符的,这样下述语句
// FormatErrorMessage(validationContext.DisplayName)
// 才会把 DisplayName 传递至占位符
this._maxWords = maxW
protected override ValidationResult IsValid(object value, ValidationContext validationContext)
if (value != null)
var valueString = value.ToString();
if (valueString.Split(' ').Length & this._maxWords)
// 基于发生错误的数据字段对错误消息应用格式设置
var errorMessage = FormatErrorMessage(validationContext.DisplayName);
return new ValidationResult(errorMessage);
return ValidationResult.S
       代码作了两处改动,首先向基类构造函数传递了默认的错误提示消息,并带有一个占位符;调用继承自基类的 FormatErrorMessage 方法会自动使用 DisplayName 来格式化这个字符串!至此,该特性可以灵活(灵活即:可指定参数值和错误消息)重用。
// 因为在扩展时创建了新的文件夹存放自定义验证类便于管理
// 因此,使用时需要引用该命名空间
using MvcMusicStore.ExtendValidationA
[Required]
[StringLength(160)]
[MaxWords(10, ErrorMessage = &There are too many words in {0}&)]
public string LastName { }
IValidatableObject
       自验证(self - validating)模型指一个知道如何验证自身的模型对象,该模型对象可以通过实现 IValidatableObject 接口来实现对自身的验证。
       例如,下面在 Order 模型中直接实现对 LastName 字段中单词个数的检查:
public partial class Order : IValidatableObject
public IEnumerable&ValidationResult& Validate(ValidationContext validationContext)
if (LastName != null && LastName.Split(' ').Length & 10)
yield return new ValidationResult(&The last name has too many words!&, new[] { &LastName& });
// rest of Order implementation and properties
       这种方式与特性版有明显的不同点:
MVC 运行时为执行验证而调用的方法名称是 Validate 而不是 IsValid,更重要的是返回类型和参数也不同。
Validate 返回类型是 IEnumerable&ValidationResult&,而不是单独的 ValidationResult 对象。因为从表面上看,内部的验证逻辑验证的是整个模型,因此可能返回多个验证错误。
没有 value 参数传递给 Validate 方法,因为该方法是一个模型实例方法,因此肯定可以看到当前模型对象自有的属性值。
       上面的代码使用了 C# yield return 语法来构建枚举返回值,同时代码还需要显式的告知 ValidationResult 与其关联的字段名称,ValidationResult 构造函数最后一个参数是 String 数组,因为这样可以使验证的结果与多个属性相关联(一组属性都执行这一验证,返回相同的错误提示消息)。
显示和编辑注解
       和验证特性一样,模型元数据提供器会收集下面的显示(和编辑)注解信息,以供 HTML 辅助方法和 ASP.NET MVC 运行时的其他组件使用,HTML 辅助方法可以使用任何可用的元数据来改变模型的显示和编辑 UI。
Display:为模型属性设置友好的“显示名称”:[Display(Name = &First Name&)];
ScaffoldColumn:该特性可以隐藏 HTML 辅助方法如 EditorForModel 和 DisplayForModel 渲染的一些特性:[ScaffoldColumn(false)]
DisplayFormat:通过命名参数来处理属性的各种格式化选项。下面代码可格式化为货币:
[DisplayFormat(ApplyFormatInEditMode = true, DataFormatString = &{0:c}&)]
public decimal Total { }
       ApplyFormatInEditMode 参数指示数据字段处于编辑模式时,是否应用属性指定的格式设置字符串。
ReadOnly:确保默认的模型绑定器不使用请求中的新值来更新属性。如在 TotalPrice(该值为计算得到,而不是用户请求来赋值) 上设置。
DataType:这是一个枚举值,为运行时提供属性的特定用途信息。如 string 类型的属性可应用于很多场合:email、URL、密码等。
[DataType(DataType.Password)]
[Display(Name = &新密码&)]
public string NewPassword { }
UIHint:给 ASP.NET MVC 运行时提供了一个模版名称,以备调用模板辅助方法(DisplayFor、EditorFor)渲染输出时使用。
HiddenInput:渲染一个隐藏的文本域,隐藏文本域有时非常好用。(但并非万无一失,恶意用户可篡改提交的表单值)上篇文章说到,如果没有看到上篇博客,建议看完再来看这个。
通过园友们的讨论,以及我自己查了些资料,然后对接口安全做一个相对完善的总结,承诺给大家写个demo,今天一并放出。
对于安全也是相对的,下面我来根据安全级别分析
1.完全开放的接口
有没有这样的接口,谁都可以调用,谁都可以访问,不受时间空间限制,只要能连上互联网就能调用,毫无安全可言。
实话说,这样的接口我们天天都在接触,你查快递,你查天气预报,你查飞机,火车班次等,这些都是有公共的接口。
我把这称之为裸奔时代。代码如下:
/// &summary&
/// 接口对外公开
/// &/summary&
/// &returns&&/returns&
[Route("NoSecure")]
public HttpResponseMessage NoSecure(int age)
var result = new ResultModel&object&()
ReturnCode = 0,
Message = string.Empty,
Result = string.Empty
var dataResult = stulist.Where(T =& T.Age == age).ToList();
result.Result = dataR
return GetHttpResponseMessage(result);
2.接口参数加密(基础加密)
&你写个接口,你只想让特定的调用方使用,你把这些调用的人叫到一个小屋子,给他们宣布说我这里有个接口只打算给你们用,我给你们每人一把钥匙,你们用的时候拿着这把钥匙即可。
这把钥匙就是我上文说到的参数加密规则,有了这个规则就能调用。
这有安全问题啊,这里面的某个成员如果哪个不小心丢了钥匙或者被人窃取,掌握钥匙的人是不是也可以来掉用接口了呢?而且他可以复制很多钥匙给不明不白的人用。
相当于有人拿到了你的请求链接,如果业务没有对链接唯一性做判断(实际上业务逻辑通常不会把每次请求的加密签名记录下来,所以不会做唯一性判断),就会被重复调用,有一定安全漏洞,怎么破?先看这个场景的代码,然后继续往下看!
/// &summary&
/// 接口加密
/// &/summary&
/// &returns&&/returns&
[Route("SecureBySign")]
public HttpResponseMessage SecureBySign([FromUri]int age, long _timestamp, string appKey, string _sign)
var result = new ResultModel&object&()
ReturnCode = 0,
Message = string.Empty,
Result = string.Empty
#region 校验签名是否合法
var param = new SortedDictionary&string, string&(new AsciiComparer());
param.Add("age", age.ToString());
param.Add("appKey", appKey);
param.Add("_timestamp", _timestamp.ToString());
string currentSign = SignHelper.GetSign(param, appKey);
if (_sign != currentSign)
result.ReturnCode = -2;
result.Message = "签名不合法";
return GetHttpResponseMessage(result);
#endregion
var dataResult = stulist.Where(T =& T.Age == age).ToList();
result.Result = dataR
return GetHttpResponseMessage(result);
3.接口参数加密+接口时效性验证(一般达到这个级别已经非常安全了)
继上一步,你发现有不明不白的人调用你的接口,你很不爽,随即把真正需要调用接口的人又叫来,告诉他们每天给他们换一把钥匙。和往常一样,有个别伙伴的钥匙被小偷偷走了,小偷煞费苦心,经过数天的踩点观察,准备在一个月黑风高的夜晚动手。拿出钥匙,捣鼓了半天也无法开启你的神圣之门,因为小偷不知道你天天都在换新钥匙。
小偷不服,经过一段时间琢磨,小偷发现了你们换钥匙的规律。在一次获得钥匙之后,不加思索,当天就动手了,因为他知道他手里的钥匙在第二天你更换钥匙后就失效了。
结果,小偷如愿。怎么破?先看这个场景的代码,然后继续往下看!
/// &summary&
/// 接口加密并根据时间戳判断有效性
/// &/summary&
/// &returns&&/returns&
[Route("SecureBySign/Expired")]
public HttpResponseMessage SecureBySign_Expired([FromUri]int age, long _timestamp, string appKey, string _sign)
var result = new ResultModel&object&()
ReturnCode = 0,
Message = string.Empty,
Result = string.Empty
#region 判断请求是否过期---假设过期时间是20秒
DateTime requestTime = GetDateTimeByTicks(_timestamp);
if (requestTime.AddSeconds(20) & DateTime.Now)
result.ReturnCode = -1;
result.Message = "接口过期";
return GetHttpResponseMessage(result);
#endregion
#region 校验签名是否合法
var param = new SortedDictionary&string, string&(new AsciiComparer());
param.Add("age", age.ToString());
param.Add("appKey", appKey);
param.Add("_timestamp", _timestamp.ToString());
string currentSign = SignHelper.GetSign(param, appKey);
if (_sign != currentSign)
result.ReturnCode = -2;
result.Message = "签名不合法";
return GetHttpResponseMessage(result);
#endregion
var dataResult = stulist.Where(T =& T.Age == age).ToList();
result.Result = dataR
return GetHttpResponseMessage(result);
4.接口参数加密+时效性验证+私钥(达到这个级别安全性固若金汤)
&继上一步,你发现道高一尺魔高一丈,仍然有偷盗事情发生。咋办呢?你打算下血本,给每个人配一把钥匙的基础上,再给每个人发个暗号,即使钥匙被小偷弄去了,小偷没有暗号,任然无法如愿。即使小偷真正的如愿,这样也很容易定位是谁的暗号泄漏问题,找到问题根源,只需要给当前这个人换下钥匙就行了,不用大动干戈。
但这个并不是万无一失的,因为钥匙和暗号毕竟还有可能被小偷搞到。代码如下:
/// &summary&
/// 接口加密并根据时间戳判断有效性而且带着私有key校验
/// &/summary&
/// &returns&&/returns&
[Route("SecureBySign/Expired/KeySecret")]
public HttpResponseMessage SecureBySign_Expired_KeySecret([FromUri]int age, long _timestamp, string appKey, string _sign)
//key集合,这里随便弄两个测试数据
//如果调用方比较多,需要审核授权,根据一定的规则生成key把这些数据存放在数据库中,如果功能扩展开来,可以针对不同的调用方做不同的功能权限管理
//在调用接口时动态从库里取,每个调用方在调用时带上他的key,调用方一般把自己的key放到网站配置中
Dictionary&string, string& keySecretDic = new Dictionary&string, string&();
keySecretDic.Add("key_zhangsan", "D9U7YY5D7FF2748AED89E90HJ88881E6");//张三的key,
keySecretDic.Add("key_lisi", "I9O6ZZ3D7FF2748AED89E90ZB7732M9");//李四的key
var result = new ResultModel&object&()
ReturnCode = 0,
Message = string.Empty,
Result = string.Empty
#region 判断请求是否过期---假设过期时间是20秒
DateTime requestTime = GetDateTimeByTicks(_timestamp);
if (requestTime.AddSeconds(20) & DateTime.Now)
result.ReturnCode = -1;
result.Message = "接口过期";
return GetHttpResponseMessage(result);
#endregion
#region 根据appkey获取key值
string secret = keySecretDic.Where(T =& T.Key == appKey).FirstOrDefault().V
#endregion
#region 校验签名是否合法
var param = new SortedDictionary&string, string&(new AsciiComparer());
param.Add("age", age.ToString());
param.Add("appKey", appKey);
param.Add("appSecret", secret);//把secret加入进行加密
param.Add("_timestamp", _timestamp.ToString());
string currentSign = SignHelper.GetSign(param, appKey);
if (_sign != currentSign)
result.ReturnCode = -2;
result.Message = "签名不合法";
return GetHttpResponseMessage(result);
#endregion
var dataResult = stulist.Where(T =& T.Age == age).ToList();
result.Result = dataR
return GetHttpResponseMessage(result);
5.接口参数加密+时效性验证+私钥+Https(我把这个级别称之为金钟罩,世间最安全莫过于此)
继上一步,我们给传输机制改为Https,这下小偷彻底懵逼了。那么问题来了,Https咋玩儿呢?可以在本地搭个环境,参考此文:
另:本文的接口是用的MVC WebAPI写的,完全基于RESTful标准。如对此不是特别了解可以参考此文:
注:demo不能直接运行,需要把两个web项目配置到iis中,api代表接口提供方,他的主域需要配置到business的webconfig中,在浏览器地址栏分别请求business中的各个调用接口方法来实现接口调用。
1、如果想验证参数错误,需要在请求接口时打个断点把接口url取出,篡改url参数,然后在浏览器中模拟请求
2、如果想验证接口超时,需要在请求接口时打个断点把接口url取出,然后等到了超时时间,然后在浏览器中模拟请求
3、如果想验证私钥错误,需要在请求接口时打个断点把接口url取出,然后修改business的私钥配置,然后在浏览器中模拟请求
阅读(...) 评论()

我要回帖

更多关于 万无一失 的文章

 

随机推荐