整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

ASP.NET MVC知识盘点:验证、授权与安全

ASP.NET MVC知识盘点:验证、授权与安全

验证

一般采用表单验证完成登陆验证,建议结合SSL使用。为限制控制器只能执行HTTPS,使用RequireHttpsAttribute

2 授权

对账户的权限的控制可以通过在控制器或控制器操作上加AuthorizeAttribute 属性。

扩展授权过滤器

扩展授权过滤器可以定义继承自AuthorizeAttribute的类,也可以定义同时继承自FilterAttribute, IAuthorizationFilter接口的类。

扩展AuthorizeAttribute

 [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited=true, AllowMultiple=true)]
 public class AuthorizeAttribute : FilterAttribute, IAuthorizationFilter
 {
 public AuthorizeAttribute(); 
 
 // 获取或设置有权访问控制器或操作方法的用户角色
 public string Roles { get; set; }
 
 //获取此特性的唯一标识符。
 public override object TypeId { get; }
 
 // 获取或设置有权访问控制器或操作方法的用户。
 public string Users { get; set; }
 
 //重写时,提供一个入口点用于进行自定义授权检查
 // 返回结果: 
 // 如果用户已经过授权,则为 true;否则为 false。
 // 异常: 
 // System.ArgumentNullException:
 // httpContext 参数为 null。
 protected virtual bool AuthorizeCore(HttpContextBase httpContext);
 
 //处理未能授权的 HTTP 请求。
 protected virtual void HandleUnauthorizedRequest(AuthorizationContext filterContext);
 
 //在过程请求授权时调用。
 // 异常: 
 // System.ArgumentNullException:
 // filterContext 参数为 null。
 public virtual void OnAuthorization(AuthorizationContext filterContext);
 
 //
 // 返回结果: 
 // 对验证状态的引用。
 //
 // 异常: 
 // System.ArgumentNullException:
 // httpContext 参数为 null。
 protected virtual HttpValidationStatus OnCacheAuthorization(HttpContextBase httpContext);
	}

AuthorizeAttribute提供了三个可重新的虚方法AuthorizeCore,HandleUnauthorizedRequest,OnAuthorization,那么在执行授权动作的过程中他们是如何被调用的呢?

看下源码的OnAuthorization方法,发现在这个方法中先调用AuthorizeCore,然后调用HandleUnauthorizedRequest被调用了。

public virtual void OnAuthorization(AuthorizationContext filterContext)
 {
 if (filterContext==null)
 {
 throw new ArgumentNullException("filterContext");
 }
//如果子操作的缓存处于活动状态,那么就抛出异常
 if (OutputCacheAttribute.IsChildActionCacheActive(filterContext))
 {
 throw new InvalidOperationException(MvcResources.AuthorizeAttribute_CannotUseWithinChildActionCache);
 }

//判断控制器或控制器操作是否允许匿名访问,如果可以就return

 bool skipAuthorization=filterContext.ActionDescriptor.IsDefined(typeof(AllowAnonymousAttribute), inherit: true)|| filterContext.ActionDescriptor.ControllerDescriptor.IsDefined(typeof(AllowAnonymousAttribute), inherit: true);
 
 if (skipAuthorization)
 {
 return;
 }
//进行权限验证
 if (AuthorizeCore(filterContext.HttpContext))
 {
 HttpCachePolicyBase cachePolicy=filterContext.HttpContext.Response.Cache;
 cachePolicy.SetProxyMaxAge(new TimeSpan(0));
 cachePolicy.AddValidationCallback(CacheValidateHandler, null /* data */);
 }
 else
 {//处理未通过权限验证的情形
 HandleUnauthorizedRequest(filterContext);
 }
 }

综合以上分析,扩展AuthorizeAttribute要注意:

1)在子类AuthorizeCore中,调用父类的AuthorizeCore方法

base.OnAuthorization(filterContext);

2)在子类的AuthorizeCore方法中验证用户的权限。

3)通过子类的构造函数传入用户的权限值

代码示例如下:

public class CustomAuthorizeAttribute : AuthorizeAttribute
 {
 private UserRole role;
 public CustomAuthorizeAttribute(UserRole role)
 {
 this.role=role;
 }
 protected override bool AuthorizeCore(HttpContextBase httpContext)
 {
 bool ret=false;
 
 //获得用户信息(从本地Session或分布式缓存中获取)
 var userInfo=......
 if(userInfo==null)
 {
 //信息为null,一般认为登陆超时或没有登陆
 }
 
 if(userInfo.Role==UserRole.Org)
 {
 ret=true;
 }
 else
 {
 //提示无权限
 }
 
 return ret;
 }
 protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
 {
 if (filterContext==null)
 {
 throw new ArgumentNullException("filterContext");
 }
 
 if (filterContext.HttpContext.Request.IsAjaxRequest())
 {//针对ajax请求进行处理
 
 }
 else
 {//非aiax进行处理
 
 //跳转到指定页面
 string strUrl=......;
 filterContext.Result=new RedirectResult(strUrl);
 }
 }
 public override void OnAuthorization(AuthorizationContext filterContext)
 {
 base.OnAuthorization(filterContext);
 }
 }
 public enum UserRole
 {
 Org=1,
 Vip=2,
 Guest=3
	}

3 安全

总的原则:

  • 所有层或各个子系统各自负责好自己的安全。
  • 任何用户数据和来自其他系统的数据都要经过检验。
  • 在满足需求的情况下,尽量缩小账户的权限。
  • 减少暴露的操作数量和操作参数。
  • 关闭服务器不需要的功能。

4 防范攻击

4.1跨站脚本攻击(XSS)

被动注入:用户的输入含有恶意脚本,而网站又能够不加检验地接受这样的输入,进而保存到数据库中。

主动注入:用户将含有恶意脚本的内容输入到页面文本框中,然后在屏幕上显示出来。

防御方法:

1)使用Razor语法输出的内容已经被编码,可以不做任何其他处理

例如:

<h4>@Model.Field</h4>

Html.ActionLink,Html.Action等方法会将路由参数编码输出

2)大部分的XSS攻击可通过对输入内容进行编码来阻止:Html.Encode,Html.AttributeEncode,Url.Encode

3)对Js进行编码

使用Ajax.JavaScriptStringEncode

4)将AntiXSS库作为默认的编码器(不建议使用,不灵活)

ASP.NET 4.5 集成Anti-XSS Library,可以通过配置来对整个网站的输出进行编码。

<system.web>
 <httpRuntime targetFramework="4.5" encoderType="System.Web.Security.AntiXss.AntiXssEncoder,System.Web"/>
 </system.web> 

4.2跨站请求伪造(CSRF/XSRF)

防御方法:

1)使用Html隐藏域存储用户令牌,令牌可以存储在Session里或者cookie里

2)在视图表单中使用@Html.AntiForgeryToken(),在控制器操作上添加属性[ValidateAntiForgeryToken],注意表单一定要使用@Html.BeginForm生成

实现机制:AntiForgeryToken方法向用户浏览器cookie中写入一个加密的数据,并在表单内插入一个隐藏栏位,每次刷新页面时隐藏栏位的值都不同,每次执行控制器操作前,都会验证隐藏栏位和浏览器cookie中的值是否相同,只有相同才允许执行控制器操作。

使用限制:

  • 客户端浏览器不能禁用cookie
  • 只对post请求有效
  • 若有XSS漏洞,则可轻易获取令牌
  • 对Ajax请求不能传递令牌,即对Ajax无效

3)使用幂等的Get请求,仅使用Post请求修改数据(仅仅是一定程度上限制这种攻击而已)

4)使用动作过滤器,验证UrlReferrer

扩展的动作过滤器:

public class CSRFFilter:AuthorizeAttribute
 {
 public override void OnAuthorization(AuthorizationContext filterContext)
 {
 if (filterContext.HttpContext==null)
 {
 throw new HttpException("请求无效");
 }
 
 if (filterContext.HttpContext.Request.UrlReferrer==null)
 {
 throw new HttpException("请求无效");
 }
 
 if (filterContext.HttpContext.Request.UrlReferrer.Host !="sit.com")
 {
 throw new HttpException("来自非法网站");
 }
 }
	}

4.3 cookie盗窃

cookie有两种形式

1)会话cookie:存储在浏览器内存中,浏览器每次请求通过Http头进行传递

2)持久性cookie:存储在硬盘上,同样通过Http头进行传递

二者的区别:会话cookie常在会话结束时失效,而持久性cookie在下一次访问站点时仍然有效。

被窃取的原因:依赖于XSS漏洞,注入一段恶意脚本就能窃取。

防御方法:

1)在web.config对cookie进行设置

<httpCookies httpOnlyCookies="true"/>,httpOnlyCookies指定为true表达仅服务器可以访问,浏览器无法访问

2)在编写代码时为每个cookie单独设置

Response.Cookies["cok"].Value=Guid.NewGuid().ToString();

Response.Cookies["cok"].HttpOnly=true;

4.4重复提交

防御方法:

1)使用bind特性,设置想要绑定的属性来,防止这种攻击。也可以设置不要绑定的字属性,但优先选择设置要绑定的属性。

例:

可以指定多个字段,用逗号分隔

public ActionResult TestViewData([Bind(Include="Field,Field1,Field1")]ModelF mf)
{
	......
}

2)使用UpdateModel或TryUpdateModel

3)使用ViewModel,明确规定View使用的数据模型

4.5开放重定向

防御方法:

使用Url.IsLocalUrl检测是否为本地url

4.6 SQL注入攻击

防御方法:

通过参数注入非法获得或修改网站数据。

使用参数化查询来防止SQL注入攻击。

这篇文章中,Web开发人员和DZone MVB介绍了如何通过验证数组来启用和禁用一组元素的技术。

今天,我们介绍如何通过验证数组来启用和禁用一组元素的技术。

当涉及到前端的复杂业务逻辑时,Web应用程序变得越来越难以编写。

根据复杂性,可能需要您在提交之前接受一组输入并验证这些元素。

我提交的最近一个屏幕需要多个DOM元素在启用提交或“操作”按钮之前处于特定状态。

当一组DOM元素处于特定状态时,我们希望触发其他元素。

我知道你在想什么。为什么不使用jQuery进行验证?

原因如下:

  1. 对于这个任务,我们需要jQuery和不显眼的验证库。这是我们真正不需要的两个大型图书馆。

  2. 借助最新的JavaScript特性,您会惊讶于浏览器中已有多少功能,以及您甚至可能不再需要jQuery。

  3. 我想要一个快速和整洁的解决方案来解决这个问题。

据说,我着手寻找一种更简单的方式来有条件地验证DOM元素组。

一个简单的例子

让我们用一个网格来设置一个简单的场景。

Views/Home/Index.cshtml

@model EventViewModel

@{

ViewData["Title"]="Grouped Validations";

}

<style>

th:nth-child(1) {

width: 20px

}

</style>

<h2>Events</h2>

@using (Html.BeginForm())

{

<div class="btn-toolbar mb-3" role="toolbar" aria-label="Toolbar with button groups">

<div class="btn-group mr-2" role="group" aria-label="First group">

<button id="enabled-button" type="button" disabled="disabled" class="btn btn-info disabled">Enable</button>

<button id="disabled-button" type="button" disabled="disabled" class="btn btn-info disabled">Disable</button>

</div>

</div>

<table class="table table-condensed table-bordered table-striped">

<thead>

<tr>

<th><input type="checkbox" id="select-all" /></th>

<th>Event</th>

<th>Begin Date</th>

<th>End Date</th>

</tr>

</thead>

<tbody>

@foreach (var eventModel in Model.Events)

{

<tr>

<td><input name="select" class="event-checkbox" type="checkbox" value="@eventModel.EventId" /></td>

<td><a title="Go to @eventModel.Title" href="@eventModel.Url.AbsoluteUri">@eventModel.Title</a></td>

<td>@eventModel.BeginDate.ToShortDateString()</td>

<td>@eventModel.EndDate.ToShortDateString()</td>

</tr>

}

</tbody>

</table>

}

这个网格在左边有复选框,带有批按钮来启用和禁用事件。点击标题中的复选框将选择所有这些选项。

首先,我们需要受用户操作影响的元素。

this.enabledButton=document.getElementById("enabled-button");

this.disabledButton=document.getElementById("disabled-button");

接下来,我们需要我们的验证。这些验证存储在包含以下项目的数组中:

  1. 要启用/禁用的元素。

  2. 一个简单的条件。

  3. 条件为真时的lambda。

  4. 条件为false时的lambda。

我们的验证数组看起来像这样:

// Custom validations for enabling/disabling DOM elements based on conditions.

this.validations=[

{

// When should the enable button be active?

element: this.enabledButton,

condition: ()=> {

var checkedCheckboxes=areChecked();

var valid=(

// Do we have even one checkbox selected?

checkedCheckboxes.length > 0

);

return valid;

},

trueAction: (elem)=> {

elem.removeAttribute("disabled");

elem.classList.remove("disabled");

},

falseAction: (elem)=> {

elem.setAttribute("disabled", "disabled");

elem.classList.add("disabled");

}

},

// Second validation

{

// When should the disable button be active?

element: this.disabledButton,

condition: ()=> {

var checkedCheckboxes=areChecked();

var valid=(

// No checkboxes are available, time to disable.

checkedCheckboxes.length > 0

);

return valid;

},

trueAction: (elem)=> {

elem.removeAttribute("disabled");

elem.classList.remove("disabled");

},

falseAction: (elem)=> {

elem.setAttribute("disabled", "disabled");

elem.classList.add("disabled");

}

}

];

我们基本上建立了一个“编码阵列”。这个数组拥有我们编写数据驱动代码所需的一切。

如果您发现 trueAction并且 falseAction,我们正在建立的拉姆达接收的元素。一旦我们收到元素,我们就会将某些属性应用到DOM元素,当它是真或假操作时。

我们的条件可以像我们需要的那样复杂,以启用或禁用某些DOM元素。

启用/禁用元素

为了使我们的DOM元素可用或禁用,我们需要遍历我们的验证并对其执行操作; 所以,我们将使用该 map功能。

function updateValidations() {

Array.from(validations).map( (item, index, array)=> {

if (item.condition()) {

item.trueAction(item.element);

} else {

item.falseAction(item.element);

}

});

}

一旦我们有我们的功能来启用或禁用DOM元素,我们需要将我们的事件附加到复选框。

// Set the "select all" checkbox.

var checkAll=document.getElementById("select-all");

checkAll.addEventListener("change", checkAllCheckbox);

我们的变化事件 checkAllCheckbox当然会称为我们的 updateValidations();功能。

function checkAllCheckbox() {

var allCheckbox=document.getElementById("select-all");

var eventCheckboxes=Array.from(

document.getElementsByClassName("event-checkbox")

);

eventCheckboxes.map( (elem, index, array)=> {

elem.checked=allCheckbox.checked;

});

updateValidations();

}

所有这些都不使用jQuery或验证库。

浏览器阻断

当然,总会遇到一些......呃......某些浏览器。

一些用户报告了复选框单击不与其他复选框一起工作的问题。

我想知道=>旧版浏览器的arrow()语法。

所以我去了caniuse.com以确认这是否可以与其他浏览器一起使用。

你不知道吗?我去了箭头语法,发现这个:caniuse.com/#search=arrow

由于我们不能使用箭头语法,所以修复非常简单。

而不是这样做:

{

// When should the enable button be active?

element: this.enabledButton,

condition: ()=> {

var checkedCheckboxes=areChecked();

var valid=(

// Do we have even one checkbox selected?

checkedCheckboxes.length > 0

);

return valid;

},

trueAction: (elem)=> {

elem.removeAttribute("disabled");

elem.classList.remove("disabled");

},

falseAction: (elem)=> {

elem.setAttribute("disabled", "disabled");

elem.classList.add("disabled");

}

},

我们做得到:

{

// When should the enable button be active?

element: this.enabledButton,

condition: function () {

var checkedCheckboxes=areChecked();

var valid=(

// Do we have even one checkbox selected?

checkedCheckboxes.length > 0

);

return valid;

},

trueAction: function (elem) {

elem.removeAttribute("disabled");

elem.classList.remove("disabled");

},

falseAction: function (elem) {

elem.setAttribute("disabled", "disabled");

elem.classList.add("disabled");

}

},

请记住,拉姆达只是匿名代表。用一个简单的函数替换箭头语法可以实现向后兼容性的奇迹。

对于项目来源,请查看我的GitHub存储库。

结论

一旦建立起来,我就可以在多个页面上使用这个功能,以便“引导”用户在屏幕上可以或不可以执行的操作。当一个DOM元素被启用时,它触发另外两个元素供用户在屏幕上进行交互。

这可以很容易地修改为使用提交按钮。如果满足所有这些条件,请启用提交按钮。如果不是,则禁用它直到满足所有条件。

再次,您可以根据需要将其设置为复杂或简单。

、点击引用右键,管理NuGet程序包,输入NPIO,选择红框选中的下载

2、创建一个 ExcelController 控制器,将Index 方法生成视图

3、批量导入excel的页面源码:

<h2>批量导入excel</h2>

<div>

@using (Html.BeginForm("Import", "Excel", FormMethod.Post, new { enctype="multipart/form-data" }))

{

<input name="files" type="file" multiple="multiple" id="=file" />

<br />

<br />

<input name="submit" id="submit" type="submit" value="批量导入" />

}

</div>

这里需要注意:file 的multiple属性是能多个文件选中,enctype="multipart/form-data" 是上传必须的表单属性

4、在ExcelController 控制器里面添加方法

[HttpPost]

public ActionResult Import(IEnumerable<HttpPostedFileBase> files)

{

// var fileName=file.FileName;

var filePath=Server.MapPath(string.Format("~/{0}", "Files"));


foreach (var file in files)

{

if (file !=null && file.ContentLength > 0)

{

var path=Path.Combine(filePath, file.FileName);

file.SaveAs(path);

DataTable excelTable=new DataTable();

excelTable=ImportExcel.GetExcelDataTable(path);

DataTable dbdata=new DataTable();

dbdata.Columns.Add("ids");

dbdata.Columns.Add("users");

dbdata.Columns.Add("area");

dbdata.Columns.Add("school");

dbdata.Columns.Add("classes");

dbdata.Columns.Add("name");

dbdata.Columns.Add("phone");

dbdata.Columns.Add("integration");

dbdata.Columns.Add("states");

dbdata.Columns.Add("createDate");

dbdata.Columns.Add("refreshDate");

for (int i=0; i < excelTable.Rows.Count; i++)

{

DataRow dr=excelTable.Rows[i];

DataRow dr_=dbdata.NewRow();

dr_["ids"]=dr["ID"];

dr_["users"]=dr["用户"];

dr_["area"]=dr["区域"];

dr_["school"]=dr["学校"];

dr_["classes"]=dr["班级"];

dr_["name"]=dr["姓名"];

dr_["phone"]=dr["手机"];

dr_["integration"]=dr["积分"];

dr_["states"]=dr["状态"];

dr_["createDate"]=dr["创建时间"];

dr_["refreshDate"]=dr["更新时间"];

dbdata.Rows.Add(dr_);

}

RemoveEmpty(dbdata);

string constr=System.Configuration.ConfigurationManager.AppSettings["cool"];

SqlBulkCopyByDatatable(constr, "student", dbdata);

}

}


return RedirectToAction("Index", "DataRefsh");

}

/// <summary>

/// 大数据插入

/// </summary>

/// <param name="connectionString">目标库连接</param>

/// <param name="TableName">目标表</param>

/// <param name="dtSelect">来源数据</param>

public static void SqlBulkCopyByDatatable(string connectionString, string TableName, DataTable dtSelect)

{

using (SqlConnection conn=new SqlConnection(connectionString))

{

using (SqlBulkCopy sqlbulkcopy=new SqlBulkCopy(connectionString, SqlBulkCopyOptions.UseInternalTransaction))

{

try

{

sqlbulkcopy.DestinationTableName=TableName;

sqlbulkcopy.BatchSize=20000;

sqlbulkcopy.BulkCopyTimeout=0;//不限时间

for (int i=0; i < dtSelect.Columns.Count; i++)

{

sqlbulkcopy.ColumnMappings.Add(dtSelect.Columns[i].ColumnName, dtSelect.Columns[i].ColumnName);

}

sqlbulkcopy.WriteToServer(dtSelect);

}

catch (System.Exception ex)

{

throw ex;

}

}

}

}

protected void RemoveEmpty(DataTable dt)

{

List<DataRow> removelist=new List<DataRow>();

for (int i=0; i < dt.Rows.Count; i++)

{

bool IsNull=true;

for (int j=0; j < dt.Columns.Count; j++)

{

if (!string.IsNullOrEmpty(dt.Rows[i][j].ToString().Trim()))

{

IsNull=false;

}

}

if (IsNull)

{

removelist.Add(dt.Rows[i]);

}

}

for (int i=0; i < removelist.Count; i++)

{

dt.Rows.Remove(removelist[i]);

}

}

这里需要注意:IEnumerable<HttpPostedFileBase> 是读取多个文件的名称,HttpPostedFileBase只能读取一个,还有就是files 是file 控件name 相对应的

5、string constr=System.Configuration.ConfigurationManager.AppSettings["cool"]; 需要在web.config 里面的appSettings 里面添加数据库连接字符串的配置

6、控制器import 方法里面有用到GetExcelDataTable 和GetCellValue 方法

这里创建一个ImportExcel 类,创建以下两个方法

using System;

using System.Collections.Generic;

using System.Linq;

using System.Web;

using NPOI;

using System.Data;

using NPOI.SS.UserModel;

using NPOI.HSSF.UserModel;

using NPOI.XSSF.UserModel;

using System.IO;

namespace Cool{

public class ImportExcel

{

public static DataTable GetExcelDataTable(string filePath)

{

IWorkbook Workbook;

DataTable table=new DataTable();

try

{

using (FileStream fileStream=new FileStream(filePath, FileMode.Open, FileAccess.Read))

{

//XSSFWorkbook 使用XLSX格式,HSSFWorkbook 使用XLS格式

string fileExt=Path.GetExtension(filePath).ToLower();

if (fileExt==".xls")

{

Workbook=new HSSFWorkbook(fileStream);

}

else if (fileExt==".xlsx")

{

Workbook=new XSSFWorkbook(fileStream);

}

else

{

Workbook=null;

}

}

}

catch (Exception ex)

{

throw ex;

}

//定位在第一个sheet

ISheet sheet=Workbook.GetSheetAt(0);

//第一行为标题行

IRow headerRow=sheet.GetRow(0);

int cellCount=headerRow.LastCellNum;

int rowCount=sheet.LastRowNum;

//循环添加标题列

for (int i=headerRow.FirstCellNum; i < cellCount; i++)

{

DataColumn column=new DataColumn(headerRow.GetCell(i).StringCellValue);

table.Columns.Add(column);

}

//数据

for (int i=(sheet.FirstRowNum + 1); i <=rowCount; i++)

{

IRow row=sheet.GetRow(i);

DataRow dataRow=table.NewRow();

if (row !=null)

{

for (int j=row.FirstCellNum; j < cellCount; j++)

{

if (row.GetCell(j) !=null)

{

dataRow[j]=GetCellValue(row.GetCell(j));

}

}

}

table.Rows.Add(dataRow);

}

return table;

}

private static string GetCellValue(ICell cell)

{

if (cell==null)

{

return string.Empty;

}

switch (cell.CellType)

{

case CellType.Blank:

return string.Empty;

case CellType.Boolean:

return cell.BooleanCellValue.ToString();

case CellType.Error:

return cell.ErrorCellValue.ToString();

case CellType.Numeric:

case CellType.Unknown:

default:

return cell.ToString();

case CellType.String:

return cell.StringCellValue;

case CellType.Formula:

try

{

HSSFFormulaEvaluator e=new HSSFFormulaEvaluator(cell.Sheet.Workbook);

e.EvaluateInCell(cell);

return cell.ToString();

}

catch

{

return cell.NumericCellValue.ToString();

}

}

}

}}

这里需要注意:NPOI 只支持93-2007的offcie 的excel文件,如果是高版本的excel文件,需要降级,打开excel文件,另存为93-2007 的.xls 文件即可


这样就可以批量把文件数据导入到数据库中了!!!