前言

最近在看JSON Web Token(Jwt)相關(guān)的東西,但是發(fā)現(xiàn)在Nancy中直接使用Jwt的組件比較缺乏,所以就在空閑時(shí)間寫了一個(gè)。

這個(gè)組件是開源的,不過目前只支持.NET Core,后續(xù)有時(shí)間再考慮兼容,歡迎Start和提Issue。組件也已經(jīng)上傳到NuGet了,可以直接安裝使用。

項(xiàng)目地址:https://github.com/hwqdt/Nancy.Authentication.JwtBearer

NuGet地址:https://www.nuget.org/packages/Nancy.Authentication.JwtBearer/

前面也寫過在ASP.NET Core中使用的Jwt的博文,只是因?yàn)楫?dāng)時(shí)為了練習(xí)Middleware ,所以是用Middleware來處理的,實(shí)際使用是不需要那么麻煩的!

畢竟是一個(gè)Action就可以搞定的事,希望沒有誤導(dǎo)大家。

下面簡單介紹一下如何使用這個(gè)組件以及這個(gè)組件是怎么實(shí)現(xiàn)的。

簡單使用

第一步 , 用VS創(chuàng)建一個(gè)空的ASP.NET Core Web Application

大學(xué)生就業(yè)培訓(xùn),高中生培訓(xùn),在職人員轉(zhuǎn)行培訓(xùn),企業(yè)團(tuán)訓(xùn)

第二步 , 安裝相關(guān)的NuGet包

通過命令在Package Manager Console執(zhí)行安裝下面的包,也可以用圖形界面來完成這一步操作。

Install-Package Microsoft.AspNetCore.Owin -Version 1.1.2 Install-Package Nancy -PreInstall-Package Nancy.Authentication.JwtBearer

其中,Microsoft.AspNetCore.Owin和Nancy是基礎(chǔ)包,Nancy.Authentication.JwtBearer是等下要用到的組件包。

第三步 , 修改Startup,添加對(duì)Nancy的支持。

public class Startup{        
    public void Configure(IApplicationBuilder app)    {            
        app.UseOwin(x=>x.UseNancy());
    }
}

第四步 , 添加一個(gè)Module來驗(yàn)證Nancy是否可以正常使用

public class MainModule : NancyModule{    public MainModule()    {
        Get("/",_=> 
        {            return "test";
        });
    }
}

正常情況下,這個(gè)時(shí)候運(yùn)行項(xiàng)目是OK的,大致效果如下:

大學(xué)生就業(yè)培訓(xùn),高中生培訓(xùn),在職人員轉(zhuǎn)行培訓(xùn),企業(yè)團(tuán)訓(xùn)

下面一步就是添加一個(gè)Bootstrapper用于啟用JwtBearer驗(yàn)證。

public class DemoBootstrapper :  DefaultNancyBootstrapper{    protected override void ApplicationStartup(TinyIoCContainer container, IPipelines pipelines)    {        base.ApplicationStartup(container, pipelines);        var keyByteArray = Encoding.ASCII.GetBytes("Y2F0Y2hlciUyMHdvbmclMjBsb3ZlJTIwLm5ldA==");        var signingKey = new SymmetricSecurityKey(keyByteArray);        var tokenValidationParameters = new TokenValidationParameters
        {            // The signing key must match!
            ValidateIssuerSigningKey = true,
            IssuerSigningKey = signingKey,            // Validate the JWT Issuer (iss) claim
            ValidateIssuer = true,
            ValidIssuer = "http://www.cnblogs.com/catcher1994",            // Validate the JWT Audience (aud) claim
            ValidateAudience = true,
            ValidAudience = "Catcher Wong",            // Validate the token expiry
            ValidateLifetime = true,
            ClockSkew = TimeSpan.Zero
        };        var configuration = new JwtBearerAuthenticationConfiguration
        {
            TokenValidationParameters = tokenValidationParameters
        };        
        //enable the JwtBearer authentication
        pipelines.EnableJwtBearerAuthentication(configuration);
    }
}

如果使用過Nancy項(xiàng)目自帶的其他認(rèn)證方式(Basic,Forms和Stateless),就會(huì)發(fā)現(xiàn)下面的才是關(guān)鍵,其他的只是用于JwtBearer認(rèn)證的配置參數(shù)。

pipelines.EnableJwtBearerAuthentication(configuration);

下面簡單介紹一下配置參數(shù)。

配置參數(shù)主要有兩個(gè),一個(gè)是TokenValidationParameters , 一個(gè)是Challenge 。

其中最主要的參數(shù)TokenValidationParameters,這是用來驗(yàn)證客戶端傳過來的token是否合法的!

它位于Microsoft.IdentityModel.Tokens這個(gè)命名空間下面。

Challenge參數(shù)則是用于指定在Unauthorized時(shí)Http響應(yīng)頭中WWW-Authenticate的值。它的默認(rèn)值是Bearer

注:Challenge參數(shù)是從Microsoft.AspNetCore.Authentication.JwtBearer項(xiàng)目借鑒過來的。

到這里, 我們已經(jīng)完成了對(duì)JwtBearer認(rèn)證的配置和啟用,下面還要驗(yàn)證這個(gè)配置是否已經(jīng)生效了!

創(chuàng)建一個(gè)新的Module,并在這個(gè)Module中使用RequiresAuthentication。

public class SecurityModule : NancyModule{    public SecurityModule() : base("/demo")    {        //important
        this.RequiresAuthentication();

        Get("/",_=> 
        {            return "JwtBearer authentication";
        });
    }    
}

注: 這里需要引用Nancy.Security這個(gè)命名空間

到這里,驗(yàn)證的代碼也已經(jīng)寫好了,當(dāng)我們訪問 http://yourdomain.com/demo 的時(shí)候

瀏覽器會(huì)提示我們The requested resource requires user authentication , 并且在響應(yīng)頭中我們可以看到WWW-Authenticate對(duì)應(yīng)的值是Bearer。

大學(xué)生就業(yè)培訓(xùn),高中生培訓(xùn),在職人員轉(zhuǎn)行培訓(xùn),企業(yè)團(tuán)訓(xùn)

我們創(chuàng)建一個(gè)合法的token值,然后通過Fiddler再發(fā)起一次請(qǐng)求,看看能否正常返回我們要的結(jié)果。

下面的代碼是生成一個(gè)測試token用的,其中的JwtSecurityToken對(duì)象應(yīng)當(dāng)與前面的配置一樣,才能確保token是有效的。

private string GetJwt(){    var now = DateTime.UtcNow;    var claims = new Claim[]
    {        new Claim(JwtRegisteredClaimNames.Sub, "demo"),        new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),        new Claim(JwtRegisteredClaimNames.Iat, now.ToUniversalTime().ToString(), ClaimValueTypes.Integer64)
    };    
    //must the same as your setting in your boostrapper class
    var symmetricKeyAsBase64 = "Y2F0Y2hlciUyMHdvbmclMjBsb3ZlJTIwLm5ldA==";    var keyByteArray = Encoding.ASCII.GetBytes(symmetricKeyAsBase64);    var signingKey = new SymmetricSecurityKey(keyByteArray);    var jwt = new JwtSecurityToken(
        issuer: "http://www.cnblogs.com/catcher1994",
        audience: "Catcher Wong",
        claims: claims,
        notBefore: now,
        expires: now.Add(TimeSpan.FromMinutes(10)),
        signingCredentials: new SigningCredentials(signingKey, SecurityAlgorithms.HmacSha256));    var encodedJwt = new JwtSecurityTokenHandler().WriteToken(jwt);    var response = new
    {
        access_token = encodedJwt,
        expires_in = (int)TimeSpan.FromMinutes(10).TotalSeconds
    };    return JsonConvert.SerializeObject(response, new JsonSerializerSettings { Formatting = Formatting.Indented });
}

通過Fiddler執(zhí)行這個(gè)帶上了token的請(qǐng)求,大致結(jié)果如下 :

大學(xué)生就業(yè)培訓(xùn),高中生培訓(xùn),在職人員轉(zhuǎn)行培訓(xùn),企業(yè)團(tuán)訓(xùn)

可以看到成功取到了相應(yīng)的內(nèi)容!

然后是本次測試用的token值相關(guān)的信息:

大學(xué)生就業(yè)培訓(xùn),高中生培訓(xùn),在職人員轉(zhuǎn)行培訓(xùn),企業(yè)團(tuán)訓(xùn)

注:用Fiddler發(fā)起請(qǐng)求的時(shí)候,記得要在請(qǐng)求頭部加上Authorization,它的值是Bearer+空格+token值

到這里,已經(jīng)展示了如何使用這個(gè)JwtBearer認(rèn)證的組件。

下面就介紹一下是怎么實(shí)現(xiàn)的這個(gè)組件!

如何實(shí)現(xiàn)

在繼續(xù)下面的內(nèi)容之前,我假設(shè)大家對(duì)Nancy的Pipelines有所了解,如果不了解的可以參考我以前的下面的鏈接

因?yàn)槠渲械腂eforePipeliine和AfterPipeline是實(shí)現(xiàn)這個(gè)認(rèn)證組件的重要切入點(diǎn)。

另外,實(shí)現(xiàn)上還用了Nancy項(xiàng)目的代碼風(fēng)格去編寫的代碼,所以你可能會(huì)發(fā)現(xiàn)與其自帶的Basic認(rèn)證等寫法差不多。

從我們上面的例子使用來說明內(nèi)部實(shí)現(xiàn)。

在上面例子的啟動(dòng)器(Bootstrapper)中,我們有一行啟用JwtBearer認(rèn)證的入口。這個(gè)入口是IPipelines的一個(gè)擴(kuò)展方法。

/// <summary>/// Module requires JwtBearer authentication/// </summary>/// <param name="pipeline">Bootstrapper to enable</param>/// <param name="configuration">JwtBearer authentication configuration</param>public static void EnableJwtBearerAuthentication(this IPipelines pipeline, JwtBearerAuthenticationConfiguration configuration){
    JwtBearerAuthentication.Enable(pipeline, configuration);
}

在這個(gè)擴(kuò)展方法中,調(diào)用了JwtBearerAuthentication這個(gè)靜態(tài)類的Enable方法,同時(shí)傳遞了當(dāng)前的pipeline和JwtBearer認(rèn)證的參數(shù)給這個(gè)方法。

下面是Enable方法的具體實(shí)現(xiàn)。

/// <summary>/// Enables JwtBearer authentication for the application/// </summary>/// <param name="pipelines">Pipelines to add handlers to (usually "this")</param>/// <param name="configuration">JwtBearer authentication configuration</param>public static void Enable(IPipelines pipelines, JwtBearerAuthenticationConfiguration configuration){    if (pipelines == null)
    {        throw new ArgumentNullException("pipelines");
    }    if (configuration == null)
    {        throw new ArgumentNullException("configuration");
    }

    pipelines.BeforeRequest.AddItemToStartOfPipeline(GetLoadAuthenticationHook(configuration));
    pipelines.AfterRequest.AddItemToEndOfPipeline(GetAuthenticationPromptHook(configuration));
}

以BeforeRequest為例,我們把一個(gè)委托對(duì)象加入到了請(qǐng)求之前要處理的一個(gè)集合中去。這樣在每次請(qǐng)求之前都會(huì)去處理這個(gè)委托。

所以這里有兩個(gè)部分。

  • 請(qǐng)求處理之前的token認(rèn)證

  • 請(qǐng)求處理之后的響應(yīng)

先來看看請(qǐng)求處理之前的token認(rèn)證如何處理

private static Func<NancyContext, Response> GetLoadAuthenticationHook(JwtBearerAuthenticationConfiguration configuration)
{    return context => 
    {
        Validate(context,configuration);        return null;
    };
}

這里也是一個(gè)空殼,用于返回AddItemToStartOfPipeline方法需要的委托對(duì)象。

真正處理token的還是Validate這個(gè)方法。認(rèn)證的處理還借助了System.IdentityModel.Tokens.Jwt命名空間下面的JwtSecurityTokenHandler類。

private static void Validate(NancyContext context, JwtBearerAuthenticationConfiguration configuration){            
    //get the token from request header
    var jwtToken = context.Request.Headers["Authorization"].FirstOrDefault() ?? string.Empty;   
    //whether the token value start with Bearer
    if (jwtToken.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase))
    {
        jwtToken = jwtToken.Substring("Bearer ".Length);
    }    else
    {                                
        return;
    }    
    //verify the token
    if (!string.IsNullOrWhiteSpace(jwtToken))
    {        try
        {
            SecurityToken validatedToken;            var tokenHandler = new JwtSecurityTokenHandler();            var validatedClaims = tokenHandler.ValidateToken(jwtToken, configuration.TokenValidationParameters, out validatedToken);            //var jwtSecurityToken = validatedToken as JwtSecurityToken;
            context.CurrentUser = validatedClaims;
        }        catch (Exception)
        {                                  
        }                               
    }
}

要對(duì)token進(jìn)行驗(yàn)證,首先要知道token是從那里來的。常規(guī)情況下,都是將這個(gè)token放到請(qǐng)求頭的Authorization中。

所以第一步是要從請(qǐng)求頭中取出Authorization的值。這個(gè)值是必須以Bearer開頭的一個(gè)字符串。注意是Bearer加一個(gè)空格!

而我們要驗(yàn)證的部分是去掉開頭這部分之后的內(nèi)容。只需要構(gòu)造一個(gè)JwtSecurityTokenHandler實(shí)例并調(diào)用這個(gè)實(shí)例的ValidateToken方法,并把要驗(yàn)證的token值和我們的配置傳進(jìn)去即可。

驗(yàn)證成功后,最為主要的一步是將ValidateToken方法的返回值賦給當(dāng)前Nancy上下文的CurrentUser??!

當(dāng)驗(yàn)證失敗的時(shí)候,ValidateToken方法會(huì)拋出一個(gè)異常,這里只catch了這個(gè)異常,并沒有進(jìn)行其他額外的處理。要處理無非也就是記錄日記,可以在這里trace一下,配合Diagnostics的使用。但是目前并沒有這樣做。

到這里,Before已經(jīng)OK了,現(xiàn)在要處理After了。

當(dāng)然對(duì)于After,也是只處理401(Unauthorized)的情況。主要是告訴客戶端 “當(dāng)前請(qǐng)求的資源需要用戶認(rèn)證”,并告訴客戶端當(dāng)前請(qǐng)求的資源需要那種認(rèn)證類型。

private static Action<NancyContext> GetAuthenticationPromptHook(JwtBearerAuthenticationConfiguration configuration){    return context =>
    {        if (context.Response.StatusCode == HttpStatusCode.Unauthorized)
        {            //add a response header 
            context.Response.WithHeader(JwtBearerDefaults.WWWAuthenticate, configuration.Challenge);
        }
    };
}

一個(gè)簡單的判斷加上響應(yīng)頭部的處理。

到這里,這個(gè)JwtBearer認(rèn)證的組件已經(jīng)ok了。

當(dāng)然這里只介紹了Pipeline的實(shí)現(xiàn),還有一個(gè)是基于NancyModule的實(shí)現(xiàn),本質(zhì)還是pipeline的處理,所以這里就不累贅了。

寫在最后

雖然簡單介紹了如何全使用和實(shí)現(xiàn)Nancy.Authentication.JwtBearer這個(gè)組件,相信大家對(duì)token(access_token)的使用是沒有太大疑問的??赡艽蠹矣幸蓡柕氖?strong style="margin: 0px; padding: 0px;">refresh_token的使用。

但是,對(duì)于refresh_token的使用,可以說因場景而異,也因人而異。只需要記住一點(diǎn)即可:refresh_token是用來換取一個(gè)新的并且可用的access_token。

本文已同步到Nancy之大雜燴

如果您認(rèn)為這篇文章還不錯(cuò)或者有所收獲,可以點(diǎn)擊右下角的【推薦】按鈕,因?yàn)槟愕闹С质俏依^續(xù)寫作,分享的最大動(dòng)力!

作者:Catcher ( 黃文清 )

來源:http://catcher1994.cnblogs.com/

聲明: 本文版權(quán)歸作者和博客園共有,歡迎轉(zhuǎn)載,但未經(jīng)作者同意必須保留此段聲明,且在文章頁面明顯位置給出原文連接,否則保留追究法律責(zé)任的權(quán)利。如果您發(fā)現(xiàn)博客中出現(xiàn)了錯(cuò)誤,或者有更好的建議、想法,請(qǐng)及時(shí)與我聯(lián)系?。∪绻胝椅宜较陆涣?,可以私信或者加我QQ。

http://www.cnblogs.com/catcher1994/p/7223618.htm