ASP.NET中的Json Web Token

一、简介

随着像AngularJs、Backbone、ReastJS等这种MVVC越来越流行,后端开发越来越脱离语言本身的渲染依赖,更甚者把部分业务直接转到前端来做;而后端变得更加的专注于数据处理,让前后端完全分离,这样后端可测试性变得更高、更简单。

针对这些MVVC现在最流行就是后端向前端提供Restful API接口,API应该是无状态,这意味者没有登陆、退出、当前用户会话,更不可能依赖Cookie;因为我们无法保证所有请求都来源于浏览器,有可能是移动端或者其他设备。

那么目前有两种方式来实现Token:

1、Basic Auth

如果按HTTP规范是指每一次请求时需要在Header带上凭证,包括用户名和密码。但一般不会这么做,我们可能会在登陆页面里根据提供的用户名和密码来获取一个Token,然后每一次请求带上Token做为钥匙来访问受限的API。

2、OAuth

相比较Basic Auth更安全,更标准。不过后端的开发难度也相对于高一点,在.NET里面目前有DotNetOpenAuth提供完整的开发资源。

其实不管是Basic Auth或OAuth他们都是为了更安全的获取Token。而JWT(Json Web Token)如果非要分应该算是Basic Auth的一种Token格式之一,其实OAuth 2.0的规范并没有明确规范Token的实现方式,那么如果你把JWT理解为OAuth的一种也不算是错的。

二、什么是JWT

JWT是一种用于认证的Token格式,这个Token可以帮助我们实现在两个系统之间以一种安全的方式传递信息。包含三部分:header、payload、signature。

1、header

用Base-64编码存放Token类型和编码方式。

{
    typ: 'JWT',
    alg: 'HS256' // 采用HMAC SHA-256算法
}
// result:e3R5cDogJ0pXVCcsYWxnOiAnSFMyNTYnfQ==

2、payload

用Base-64编码存放任意信息,比如:用户信息、权限信息等。

{
    name: 'asdf', // 用户名
    role: 'manage', // 权限
    exp: 1443925718155 // 过期时间
}
// result:e25hbWU6ICdhc2RmJyxyb2xlOiAnbWFuYWdlJyxleHA6IDE0NDM5MjU3MTgxNTV9

3、signature

包括header、payload和密钥的混合体,密钥当然是最重要的,要是泄露了那么你的Token也就被破解了。假定密钥为:1234567890,按 HmacSHA256(header+payload, 密钥) 所得到的Token为:b613679a0814d9ec772f95d778c35fc5ff1697c493715653c6c712144292c5ad。

最后将header、payload、signature合并起来就是Token了。

上面JSON体中的Key键JWT是有一个明确的结构体规范的,可以参考:JWT Claims

三、一个完整的Web API示例

以下我只能尽可能的简化去演示这个过程,如果你对JWT感兴趣可以查看完整的源代码

1、JWT编码和解码代码

代码并无特别之处,只不过将以上JWT三个步骤的具体实现。

/// <summary>
/// 只支持HS256算法
/// </summary>
public class JWT
{
    /// <summary>
    /// Token解码
    /// </summary>
    public T Decode<T>(string token, string key, bool noVerify = false)
    {
        var segments = token.Split('.');
        if (segments.Length != 3) throw new ArgumentException("Token格式不正确");

        var headerSeg = segments[0];
        var payloadSeg = segments[1];
        var signatureSeg = segments[2];

        var header = StringToJson<JWTHeader>(Base64Decrypt(headerSeg));
        var payload = StringToJson<T>(Base64Decrypt(payloadSeg));

        if (!noVerify)
        {
            if (!HmacSignature(headerSeg + "." + payloadSeg, key).Equals(signatureSeg))
                throw new Exception("无效Token");
        }

        return payload;
    }

    /// <summary>
    /// Token编码
    /// </summary>
    public string Encode<T>(T payload, string key, JWTHeader header = null)
    {
        var segments = new List<string>();
        // 1、header
        if (header == null) header = new JWTHeader();
        segments.Add(Base64Encrypt(JsonToString(header)));
        // 2、payload
        segments.Add(Base64Encrypt(JsonToString(payload)));
        // 3、signature
        segments.Add(HmacSignature(String.Join(".", segments), key));

        // 最终以.号拼接做为Token
        return String.Join(".", segments);
    }

    #region helper

    private string Base64Encrypt(string str)
    {
        return Convert.ToBase64String(Encoding.UTF8.GetBytes(str));
    }

    private string Base64Decrypt(string str)
    {
        return Encoding.UTF8.GetString(Convert.FromBase64String(str));
    }

    private string HmacSignature(string secret, string value)
    {
        var secretBytes = Encoding.UTF8.GetBytes(secret);
        var valueBytes = Encoding.UTF8.GetBytes(value);
        string signature;

        using (var hmac = new HMACSHA256(secretBytes))
        {
            var hash = hmac.ComputeHash(valueBytes);
            signature = Convert.ToBase64String(hash);
        }
        return signature;
    }

    private string JsonToString<T>(T obj)
    {
        return Newtonsoft.Json.JsonConvert.SerializeObject(obj);
    }

    private T StringToJson<T>(string value)
    {
        if (String.IsNullOrEmpty(value)) return default(T);
        return Newtonsoft.Json.JsonConvert.DeserializeObject<T>(value);
    }

    #endregion
}

2、获取Token

第一件事自然是通过用户名和密码来获取Token。

[HttpGet, HttpPost, Route("login")]
public TokenResult Login(string username, string password)
{
    // 为了简化不做用户名和密码验证
    // 有效期:7天
    var expire = DateTime.UtcNow.AddDays(7);
    var res = new TokenResult()
    {
        expires = expire
    };
    var payloadObj = new JWTPayload()
    {
        name = username,
        role = "manage",
        exp = expire
    };
    res.token = new JWT().Encode<JWTPayload>(payloadObj, PassportConfig.SecretKey);

    return res;
}

当我们请求 passport/login?username=asdf&password=1 后,会返回一个JSON结果:

{
token: "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiMSIsInJvbGUiOiJtYW5hZ2UiLCJleHAiOjE0NDQ1NDQxMzU0MTl9.zUbqWY/vBerzM3BjzSFnFxEbWlcW1g92pNsmStt5xXc=",
expires: 1444544135419
}

token 表示于下如果我们需要访问受限API时,只需要带上这个Token即可。

expires 表示过期时间。

3、验证Token

Web API我们可以通过OWIN做一个中间件,每一次请求时需要先通过中间件来验证Token是否有效。但这里我没打算这么写,写一个OWin的成本高于一个属性。所以这里我采用 AuthorizationFilterAttribute 属性。

public override void OnAuthorization(HttpActionContext actionContext)
{
    if (actionContext == null) throw new Exception("actionContext");

    // 过滤带有AllowAnonymous
    if (SkipAuthorization(actionContext)) return;

    // 分别从Query、Body、Header获取Token字符串
    const string key = "access_token";
    string token = "";
    // header
    if (actionContext.Request.Headers.Contains(key))
        token = actionContext.Request.Headers.GetValues(key).First();
    // query
    // body

    if (String.IsNullOrEmpty(token))
        actionContext.Response = actionContext.ControllerContext.Request.CreateErrorResponse(HttpStatusCode.Forbidden, "请先登录。");

    try
    {
        var payload = new JWT().Decode<JWTPayload>(token, PassportConfig.SecretKey);
        // 检查token是否已经过期
        if (payload.exp < DateTime.UtcNow)
            actionContext.Response = actionContext.ControllerContext.Request.CreateErrorResponse(HttpStatusCode.Forbidden, "Token已经过期。");
    }
    catch (Exception ex)
    {
        actionContext.Response = actionContext.ControllerContext.Request.CreateErrorResponse(HttpStatusCode.Forbidden, ex.Message);
    }
}

四、总结

JWT这种JSON Token格式相比较我们自建的安全认证更简单,而安全上面也能够得到保障。首先对于公司产品而言,同一个Token能否在不同业务线上能够快速、有效的认证非常重要;其次本身Token已经附带着用户信息,虽然他几乎接近明文存放在Token中间,但这和安全并不冲突。

ASP.NET MVC数据绑定

一、介绍

深入了解 ASP.NET MVC 数据绑定非常重要,特别是对于像富客户端越来越流行,复杂对象需求度也越来越高。我会写一些示例来帮助了解MVC数据绑定的是怎么回事,这对于工作也会事半功倍。

二、键值对

首先我们要明确MVC默认只支持键值对数据绑定(自定义绑定规则除外),了解这一点非常重要,因为对于MVC而言提交时是通过表单。而表单默认就以键值对形式(即 Content-Type:application/x-www-form-urlencoded);当然我们可能提交是带有文件二进制流的 Content-Type:multipart/form-data 形式,我们也可以理解为键值对,只不过编码不同而已。这两种对MVC而言都是一样的。

文本键值对的特点是请求参数都是会以 [name]=[value]& 的形式串连一个字符串。

看到这不必太着急,我相信很多人更想要了解的是通过AJAX来提交一个复杂对象是怎么一回事,但必须先了解键值对,否则脑子很难转过弯来。

三、示例

我们假如以下 Model 是我们遇到的相对复杂的结构,我相信已经足够应付绝大多数环境了。

public class Student
{
    public string Name { get; set; }
    public int Age { get; set; }
    public bool IsCCP { get; set; }
    public byte Status { get; set; }
    public DateTime Created { get; set; }

    /// <summary>
    /// 家庭成员情况
    /// </summary>
    public List<Family> Families { get; set; }

    /// <summary>
    /// 年度分类情况,key:年份
    /// </summary>
    public Dictionary<int, Score> Scores { get; set; }
}

public class Family
{
    public string Name { get; set; }

    public string Mobile { get; set; }
}

public class Score
{
    public int Avg { get; set; }

    public int Highest { get; set; }

    public int Lowest { get; set; }
}

根据Model出现了基础类型、List、Dictionary几乎涵盖所有我们日常需要的类型了,以下相应的前端HTML代码如下:

@using (Html.BeginForm("Index", "Home", FormMethod.Post, new { name = "nForm", id = "nForm", @class = "form-horizontal" }))
{
    <fieldset>
        <legend>基本信息</legend>
        <div class="form-group">
            @Html.LabelFor(model => model.Name, new { @class = "col-sm-2 control-label" })
            <div class="col-sm-10">
                @Html.TextBoxFor(model => model.Name, new { @class = "form-control" })
            </div>
        </div>
        <div class="form-group">
            @Html.LabelFor(model => model.Age, new { @class = "col-sm-2 control-label" })
            <div class="col-sm-10">
                @Html.TextBoxFor(model => model.Age, new { type = "number", @class = "form-control" })
            </div>
        </div>
        <div class="form-group">
            <div class="col-sm-10 col-sm-offset-2">
                @Html.CheckBoxFor(model => model.IsCCP)
                @Html.LabelFor(model => model.IsCCP, new { @class = "" })
            </div>
        </div>
        <div class="form-group">
            @Html.LabelFor(model => model.Status, new { @class = "col-sm-2 control-label" })
            <div class="col-sm-10">
                @Html.DropDownListFor(w => w.Status, new List<SelectListItem>() {
            new SelectListItem(){Text="请选择",Value="0"},
            new SelectListItem(){Text="正常",Value="1"},
            new SelectListItem(){Text="退学",Value="2"}
           }, new { @class = "form-control" })
            </div>
        </div>
        <div class="form-group">
            @Html.LabelFor(model => model.Created, new { @class = "col-sm-2 control-label" })
            <div class="col-sm-10">
                @Html.TextBoxFor(model => model.Created, new { type = "date", @class = "form-control" })
            </div>
        </div>
    </fieldset>
    <fieldset>
        <legend>家庭情况</legend>
        @for (var i = 0; i < Model.Families.Count; i++)
        {
            <div class="form-group">
                @Html.LabelFor(model => Model.Families[i].Name, new { @class = "col-sm-2 control-label" })
                <div class="col-sm-10">
                    @Html.TextBoxFor(model => Model.Families[i].Name, new { @class = "form-control" })
                </div>
            </div>
            <div class="form-group">
                @Html.LabelFor(model => Model.Families[i].Mobile, new { @class = "col-sm-2 control-label" })
                <div class="col-sm-10">
                    @Html.TextBoxFor(model => Model.Families[i].Mobile, new { type = "number", @class = "form-control" })
                </div>
            </div>
            <hr />
        }
    </fieldset>
    <fieldset>
        <legend>分数情况</legend>
        @{int scorePoint = 0;}
        @foreach (var item in Model.Scores)
        {
            <div class="form-group">
                <div class="col-sm-2">
                    <input name="Scores[@scorePoint].Key" value="@item.Key" type="number" class="form-control" />
                </div>
                <div class="col-sm-10">
                    <div class="form-group">
                        @Html.LabelFor(model => item.Value.Highest, new { @class = "col-sm-2 control-label" })
                        <div class="col-sm-10">
                            <input name="Scores[@scorePoint].Value.Highest" value="@item.Value.Highest" type="number" class="form-control" />
                        </div>
                    </div>
                    <div class="form-group">
                        @Html.LabelFor(model => item.Value.Lowest, new { @class = "col-sm-2 control-label" })
                        <div class="col-sm-10">
                            <input name="Scores[@scorePoint].Value.Lowest" value="@item.Value.Lowest" type="number" class="form-control" />
                        </div>
                    </div>
                    <div class="form-group">
                        @Html.LabelFor(model => item.Value.Avg, new { @class = "col-sm-2 control-label" })
                        <div class="col-sm-10">
                            <input name="Scores[@scorePoint].Value.Avg" value="@item.Value.Avg" type="number" class="form-control" />
                        </div>
                    </div>
                </div>
            </div>
            <hr />
            ++scorePoint;
        }
    </fieldset>
    <div class="form-group">
        <div class="col-sm-10 col-sm-offset-2">
            <button type="submit" class="btn btn-primary">Submit</button>
        </div>
    </div>
}

1、基础类型

基本信息都是基础类型,相应的@Html都是非常简单的,我们可以跟踪一下生成的HTML及请求时所提交的相应参数信息:

<input class="form-control" id="Name" name="Name" type="text" value="cipchk">
<input class="form-control" id="Age" name="Age" type="number" value="18">

非常简单的将Name和Age组装成:Name=cipchk&Age=18,我们可以通过 Request.Form 来确认这一点。

2、List类型

家庭情况以List对象存在,相应的@Html只需要通过 for 循环,最后生成的HTML就像:

<!-- 1 -->
<input class="form-control" name="Families[0].Name" type="text" value="父亲">
<input class="form-control" name="Families[0].Mobile" type="number" value="111">
<!-- 2 -->
<input class="form-control" name="Families[1].Name" type="text" value="母亲">
<input class="form-control" name="Families[1].Mobile" type="number" value="222">

和基本类型相比较,重点的name值,会以对象名+下标的形式出现,且这个下标必须是从0逐一递增。

注:原则上我们也可以用foreach来循环,但这出于@Html的运行机制中会以传递的 model => Model.Families[i].Name 做为name值,而这种方式跟我们所需要的键值对名称的约定有出入,所以采用 for 循环方式更符合实际。因为我们可以把以下这句话做为@Html的约束。

对于前端@Html的循环,永远都以 for 来完成。

而最后提交到会组装成数据如:Families[0].Name=父亲&Families[0].Mobile=111&Families[1].Name=母亲&Families[1].Mobile=222

2、Dictionary类型

分数以字典形式出现,其实他也是一个List对象,只不过看起来他只有 Key、Value 而已,所以我们依然可以用 List 的形式来表现,只不过他的表单 name 会更加奇怪一点,像:

<!-- 1 -->
<input name="Scores[0].Key" value="2014" type="number" class="form-control">
<input name="Scores[0].Value.Highest" value="100" type="number" class="form-control">
<input name="Scores[0].Value.Lowest" value="100" type="number" class="form-control">
<input name="Scores[0].Value.Avg" value="100" type="number" class="form-control">
<!-- 2 -->
<input name="Scores[1].Key" value="2015" type="number" class="form-control">
<input name="Scores[1].Value.Highest" value="100" type="number" class="form-control">
<input name="Scores[1].Value.Lowest" value="100" type="number" class="form-control">
<input name="Scores[1].Value.Avg" value="100" type="number" class="form-control">

所需要的表单name和List类型相象,我就不加与赘述。

四、回归HTML标准

查询w3对form描述,键值对这种形和.Net的数据类型之间的互转换原来对于基础类型来说非常简单,我们可以理解为form中的text、number、date等等(大部分是HTML5)这一些都是和我们编程语言的数据类型相之对应,而对于复杂类型对象、List、字典等等,本质也是采用键值对,只不过不同语言对于键的表现形式不同而已。

所以实际我们更应该清楚不同语言在键的表述形式上的理解,那么我相信对于数据绑定这个课题上不会再纠结。

五、AJAX问题

我相信如果了解问题本质以后,这个点就会不存在了,不相信我们改学以上示例,用AJAX提交,只需要这样:

$('form').submit(function () {
    $.ajax({
        url: $(this).attr('action'),
        type: $(this).attr('method'),
        data: $(this).serialize(),
        success: function (res) {

        }
    });
    return false;
});

资料

ASP.NET MVC 验证码:Captcha MVC

Captcha MVC 是一个开源的ASP.NET MVC验证码库,首先我先上两张图,比较明白:
MathCaptchaMvc
PlainCaptchaMvc

第一张为数学验证码,第二张是文本验证码,包括样式效果是不是很漂亮。

一、安装

一方面可以通过官网(见参考资料),另一方面可以通过NuGet关键词Captcha MVC直接安装,分别有MVC3/4/5三个版本。

二、使用

在View所在页需要先引入命令空间 @using CaptchaMvc.HtmlHelpers。 这样我们可以通过HTML扩展方法直接创建数学或文本验证码。

不管是哪一个扩展方法,都带有 params ParameterModel[],通过他可以完成做任意验证码。

ParameterModel参数

  • InputTextAttribute 比如图片中的:Enter the result you see above,String类型。
  • RefreshTextAttribute 比如图片中的:Try another,String类型。
  • IsRequiredAttribute 是否为必填项,bool类型。
  • RequiredMessageAttribute 必填项提醒文本。
  • LengthAttribute 文本有效,字符长度,int类型。
  • ErrorAttribute 验证失败时提醒文本,默认:The captcha is not valid。
  • PartialViewNameAttribute 自定义布局的部分视图名称。
  • PartialViewDataAttribute 自定义布局的部分视图的ViewData参数。
  • ScriptPartialViewNameAttribute 自定义布局的部分视图的Script节点名称。

扩展方法

扩展方法实际是对默认属性值的一个初始化,虽然分别有 Html.CaptchaHtml.MathCaptcha 两种验证类型的扩展方法,但实际都是由上列属性表组合而来。

比如我们创建一个数字验证码,可以这样子:

@{
    CaptchaMvc.Models.ParameterModel[] catchaParams = new CaptchaMvc.Models.ParameterModel[] { 
        new CaptchaMvc.Models.ParameterModel(CaptchaMvc.Infrastructure.DefaultCaptchaManager.MathCaptchaAttribute, true),
        new CaptchaMvc.Models.ParameterModel(CaptchaMvc.Infrastructure.DefaultCaptchaManager.PartialViewNameAttribute, "_Captcha"),
        new CaptchaMvc.Models.ParameterModel(CaptchaMvc.Infrastructure.DefaultCaptchaManager.RefreshTextAttribute, "刷新文本"),
        new CaptchaMvc.Models.ParameterModel(CaptchaMvc.Infrastructure.DefaultCaptchaManager.IsRequiredAttribute, true),
        new CaptchaMvc.Models.ParameterModel(CaptchaMvc.Infrastructure.DefaultCaptchaManager.RequiredMessageAttribute, "必填项提醒文本")
    };
}
@Html.Captcha(6, catchaParams)

检查验证码是否有效

有两种方式可以验证。

使用属性:

        [CaptchaMvc.Attributes.CaptchaVerify("Captcha is not valid")]
        [HttpPost]
        public ActionResult MathCaptcha(string empty)
        {
            if (ModelState.IsValid)
            {
                TempData["Message"] = "Message: captcha is valid.";
                return View();
            }

            TempData["ErrorMessage"] = "Error: captcha is not valid.";
            return View();
        }

使用扩展方法:

        [HttpPost]
        public ActionResult Index(string empty)
        {
            if (this.IsCaptchaValid("Captcha is not valid"))
            {
                TempData["Message"] = "Message: captcha is valid.";
                return View();
            }

            TempData["ErrorMessage"] = "Error: captcha is not valid.";
            return View();
        }

三、自定义布局

在说这个之前得先了解一下大概的原理。所先当调用 Html.Captcha 后会产生一个 Token,对于刷新时需要一并提交 Token,否则只会返回一个黑框的图片。明白这一点后,以下是官网示例看起来会变得很容易懂,而且对于自定义布局来满足我们页面需求非常重要。

自定义文本验证码布局

@model CaptchaMvc.Models.DefaultBuildInfoModel
<img id="@Model.ImageElementId" src="@Model.ImageUrl" />
@Html.Hidden(Model.TokenElementId, Model.TokenValue)
<br />
@{
    string id = Guid.NewGuid().ToString("N");
    string functionName = string.Format("______{0}________()", Guid.NewGuid().ToString("N"));
    <script type="text/javascript">

        $(function () {
            $('#@id').show();
    });


    function @functionName {
            $('#@id').hide();
        $.post("@Model.RefreshUrl", { @Model.TokenParameterName: $('#@Model.TokenElementId').val() }, 
            function () {
                $('#@id').show();
            });
        return false;
    }
    </script>

    <a href="#@Model.InputElementId" id="@id" onclick="@functionName" style="display: none;">@Model.RefreshButtonText</a> 
}

<br />
@Model.InputText
<br />
@if (Model.IsRequired)
{
    @Html.TextBox(Model.InputElementId, null, new Dictionary<string, object>
                                                  {
                                                      {"data-val", "true"},
                                                      {"data-val-required", Model.RequiredMessage}
                                                  })
}
else
{
    @Html.TextBox(Model.InputElementId)
}
@Html.ValidationMessage(Model.InputElementId)

自定义数学验证码布局

@model CaptchaMvc.Models.MathBuildInfoModel

<img id="@Model.ImageElementId" src="@Model.ImageUrl" />
@Html.Hidden(Model.TokenElementId, Model.TokenValue)
<br />
@{
    string id = Guid.NewGuid().ToString("N");
    string functionName = string.Format("______{0}________()", Guid.NewGuid().ToString("N"));
   <script type="text/javascript">

    $(function () {
        $('#@id').show();
    });


    function @functionName {
        $('#@id').hide();
        $.post("@Model.RefreshUrl", { @Model.TokenParameterName: $('#@Model.TokenElementId').val(), 
@Model.MathParamterName: "0" }, 
            function () {  $('#@id').show();
        });
        return false;
    }
</script>

<a href="#@Model.InputElementId" id="@id" onclick="@functionName" style="display: none;">@Model.RefreshButtonText</a> 
}

<br />
@Model.InputText
<br />
@if (Model.IsRequired)
{
    @Html.TextBox(Model.InputElementId, null, new Dictionary<string, object>
                                                  {
                                                      { "data-val", "true" }, 
                                                      { "data-val-required", Model.RequiredMessage }
                                                  })
}
else
{
    @Html.TextBox(Model.InputElementId)   
}
@Html.ValidationMessage(Model.InputElementId)

这里还要注意的是,对于不同的验证码的 @model 也是不同。

四、Ajax问题

由于每一次验证的token都会不一样,所以如果form是ajax发送请求的话,那么我们还需要将在验证失败后,重新生成一个token,并将结果返回到客户端。这并不复杂,我们只需要调用 CaptchaHelper.GenerateCaptchaValue 来获取 IUpdateInfoModel 对象,这里面就有几个属性,其中 TokenElementIdTokenValue 就是新生成的 token。

var buildInfo = CaptchaMvc.HtmlHelpers.CaptchaHelper.GenerateCaptchaValue(controller, 6, GetParams());
return new
{
    TokenElementId = buildInfo.TokenElementId,
    TokenValue = buildInfo.TokenValue
};

这里的 GetParams() 是我自己封装的参数集合,上面已经很详细说明了。

参考资料

什么是pjax

Ajax是Web开发非常重要的工具,只不过有一点单纯拿AJAX来请求一个页面时特别是单页应用时,无法正常使用浏览器历史记录、后退、刷新时同时替换文档标题。

而pjax就是用来解决这个问题,使用的是jQuery ajax 和 pushState(查看支持pushState浏览器版本情况)。

一、pushState、replaceState

pushState往历史堆栈的顶部添加一条记录,replaceState更改当前历史记录,包括三个参数:

  • data为一个可以被解析的对象或null,不能够是一个jQuery对象。在触发popstate事件时,会做为state参数传递过去;
  • title为页面的标题;
  • url为页面的URL,不写则为当前页。

二、pjax应用

pjax并不是完全自动的,需要自己调用。

首先:需要给页面设定一下被替换的位置,例:

<!DOCTYPE html>
<html>
<head>
  <!-- styles, scripts, etc -->
</head>
<body>
  <h1>My Site</h1>
  <div class="container" id="pjax-container">
    Go to <a href="/page/2">next page</a>.
  </div>
</body>
</html>

如果我们希望点击 /page/2 链接,会将 /page/2 HTML代码替换到 #pjax-container 容器中,则还需要告知pjax监听 a 标签。

$(document).pjax('a', '#pjax-container')

以上是一个完整的例子;如果你希望跟Unobtrusive JavaScript兼容,那么可以尝试使用符合Unobtrusive JavaScript写法,当然并不能完整的利用data-ajax-loading、data-ajax-mode、data-ajax-update属性。

每个PJAX请求都会自动加入一个 X-PJAX 请求头,以便于我们可以来判断是否需要引入 layout 文件,这里我是通过一个Filter来判断:

    public class PJaxFilter : ActionFilterAttribute
    {
        public override void OnActionExecuting(ActionExecutingContext filterContext)
        {
            base.OnActionExecuting(filterContext);
            filterContext.Controller.ViewBag.layout = "~/Views/Shared/_Layout.cshtml";
            string pjaxHeader = filterContext.RequestContext.HttpContext.Request.Headers["X-PJAX"];
            if (!String.IsNullOrEmpty(pjaxHeader)) filterContext.Controller.ViewBag.layout = null;
        }
    }

cshtml页面对于布局需要根据 @{ Layout = ViewBag.layout; } 来,这样当我们直接通过 /page/2 请求页面时也能够正常显示。

页面标题可以通过在HTML中输出 <title>页面标题</title>,pjax会自动获取。

pjax也提供一系列事件,比如 pjax:sendpjax:complete 来处理加载状态:

$(document).on('pjax:send', function() {
  $('#loading').show()
})
$(document).on('pjax:complete', function() {
  $('#loading').hide()
})

二、pjax原理

pjax的AJAX请求是基于jquery的ajax,所以pjax的options参数大部分才是jquery ajax请求的原样参数。

1、每次PAJX发生请求时会先通过 pushState 将请求页压入历史堆栈的顶部。

  if (xhr.readyState > 0) {
    if (options.push && !options.replace) {
      // Cache current container element before replacing it
      cachePush(pjax.state.id, context.clone().contents())
      window.history.pushState(null, "", stripPjaxParam(options.requestUrl))
    }
    fire('pjax:start', [xhr, options])
    fire('pjax:send', [xhr, options])
  }

2、请求成功后会:替换当前历史记录页、修改页面标题、把HTML写入相应容器当中。

    if (options.push || options.replace) {
      window.history.replaceState(pjax.state, container.title, container.url)
    }
    // Clear out any focused controls before inserting new page contents.
    try {
      document.activeElement.blur()
    } catch (e) { }
    // 这里的页面标题,会依次按<head><title /></head>、按Response片断根节点的title属性、Response带有<title />。
    if (container.title) document.title = container.title
    fire('pjax:beforeReplace', [container.contents, options], {
      state: pjax.state,
      previousState: previousState
    })
    context.html(container.contents)

这里要特意提一下pjax对脚本的处理方式。

因为 .html() 是基于innerHTML来改变容器代码,所以如果在页面使用 document.write 变成很不可取的方式,因为不同版本浏览器解析会有不可预知的情况发生,参考

还有针对 <script src="" /> 的处理是将未加载过的脚本写入到 <head> 中。

这里pjax的顺序是先写入HTML,再加载script,这好处就是避免DOM渲染和脚本资源请求受阻,倒置页面假死。

所以这里要特别注意加载顺序问题,如果某个页面每次请求都需要引用外部脚本,建议用 getScript。

3、监控window.onpopstate事件,后退时重新发送AJAX请求。

pjax会先从缓存中获取数据,而缓存并非无限制的,可以通过调用 pjax 时指定 maxCacheLength 参数大小(默认:20);和重新发送请求。

详见 onPjaxPopstate 函数。

4、重新加载布局

我们可以重新指定Response的请求头pjax版本号,来强制重新刷新页面。

if request.headers['X-PJAX']
  response.headers['X-PJAX-Version'] = "v123"
end

或html代码加入:

<meta http-equiv="x-pjax-version" content="v123">

其实pjax和模块化编程(例:require.js)真的是天生一对,之前有好几个小项目已经做了大量开发,效率、用户体验真的上升一个档次。

以上。

asp.net mvc 调用数据、视图几点浅析

Dynamic类型

在说数据之前得先了解一下什么是Dynamic类型。根据微软MSDN的解释是说绝大部分情况下和Object是一样的,唯一的差别是Object在编译会解析或类型检查,而Dynamic不会,所以最学出错的是一但对Dynamic不正确类型转换只能等待异常。

另一点是网上很多文章都是在将Dynamic和var进行比较,那都是狗屎,这分明两个完全不一样的东西。

Collection向Value传值

包括ViewBag、ViewData、Model三种类型,至于ViewTemp采用是Session作为传递,不加于考虑差别。

ViewData:实际是一个 Dictionary<string, object> 数据集合。

ViewBag:数据依然存入ViewData,只不过当需要从ViewBag获取数据时并转化之,其核心是Dynamic;换句话说动态的将ViewData中的数据转换成Dynamic对象,倒置我们可以语句更优美罢了。

另:ViewData["now"] = DateTime.Now; 同样在View端可以直接用 ViewBag.now。故而他们是同一个数据源。

Model:并无特别之处,只是将数据做为强类型使用,不过是最优美的编码风格。推荐之。

视图调用

这里会有四种调用方式,Html.Partial、Html.RenderPartial、Html.Action、Html.RenderAction。理论上这四种是没有太大差别的,只不过在操作性上感觉比较自由而已。

为何这么讲,其原因是不管采用哪种方式,你大可直接调用静态方法来处理逻辑处理,所以我才会说没有太大差别。虽然有点鸡蛋里挑骨头。

asp.net mvc 获取网站缩略图

这是很早以前一个项目的外挂,但是今天有时间就做一下整理。核心功能是基于《Get ASP.NET C# 2.0 Website Thumbnail Screenshot》其原理也非常简单,利用WebBrowser加载网页,完成调用DrawToBitmap即可,故而需要引用:System.Windows.Forms程序集。

线程问题

虽然利用WebBrowser在Win Form下没有什么问题,但如果在ASP.NET下就不行,因为这涉及到单线程和多线程问题。我们说利用WebBrowser加载网页,但前提是必须等待DocumentCompleted事件被调用。

这里的办法是重新强制创建一个新单元线程SetApartmentState(ApartmentState.STA)

范例

1、引用 (ClassWSThumb)[http://pan.baidu.com/s/1eQIaKCU]。

2、在HomeController创建2个Action:Index、GetThumb

以下是Index View的核心代码

<input type="url" id="J_Url" value="http://www.baidu.com" />
<button type="button" id="J_GetThumb">获取缩略图</button><br />
<p id="J_Status"></p>
<img id="J_Priview" />
<script>
    $("#J_GetThumb").click(function () {
        $("#J_Status").text("加载中...");
        $("#J_Priview").fadeOut(1000);
        $("#J_Priview").attr("src", "/home/GetThumb?Url=" + $("#J_Url").val()).load(function () {
            $("#J_Status").text("完成");
            $("#J_Priview").fadeIn(1000);
        });
    });
</script>

以下是Index View的核心代码

        public ActionResult GetThumb(string Url)
        {
            Bitmap _Bitmap = ClassWSThumb.GetWebSiteThumbnail(Url, 1024, 768, 300, 300);
            MemoryStream ms = new MemoryStream();
            if (_Bitmap != null)
                _Bitmap.Save(ms, System.Drawing.Imaging.ImageFormat.Jpeg);
            else
                System.Drawing.Image.FromFile(Server.MapPath("~/NoPriview.jpg")).Save(ms, System.Drawing.Imaging.ImageFormat.Jpeg);
            return File(ms.ToArray(), "image/jpg");
        }

源码下载

© 2017 卡片机色彩 沪ICP备13032872号-3

Theme by cipchk

to top