前言
最近在看JSON Web Token(Jwt)相關的東西,但是發現在Nancy中直接使用Jwt的組件比較缺乏,所以就在空閑時間寫了一個。
這個組件是開源的,不過目前只支持.NET Core,后續有時間再考慮兼容,歡迎Start和提Issue。組件也已經上傳到NuGet了,可以直接安裝使用。
項目地址:https://github.com/hwqdt/Nancy.Authentication.JwtBearer
NuGet地址:https://www.nuget.org/packages/Nancy.Authentication.JwtBearer/
前面也寫過在ASP.NET Core中使用的Jwt的博文,只是因為當時為了練習Middleware ,所以是用Middleware來處理的,實際使用是不需要那么麻煩的!
畢竟是一個Action就可以搞定的事,希望沒有誤導大家。
下面簡單介紹一下如何使用這個組件以及這個組件是怎么實現的。
簡單使用
第一步 , 用VS創建一個空的ASP.NET Core Web Application
第二步 , 安裝相關的NuGet包
通過命令在Package Manager Console執行安裝下面的包,也可以用圖形界面來完成這一步操作。
Install-Package Microsoft.AspNetCore.Owin -Version 1.1.2
Install-Package Nancy -Pre
Install-Package Nancy.Authentication.JwtBearer
其中,Microsoft.AspNetCore.Owin和Nancy是基礎包,Nancy.Authentication.JwtBearer是等下要用到的組件包。
第三步 , 修改Startup,添加對Nancy的支持。
public class Startup
{
public void Configure(IApplicationBuilder app)
{
app.UseOwin(x=>x.UseNancy());
}
}
第四步 , 添加一個Module來驗證Nancy是否可以正常使用
public class MainModule : NancyModule
{
public MainModule()
{
Get("/",_=>
{
return "test";
});
}
}
正常情況下,這個時候運行項目是OK的,大致效果如下:
下面一步就是添加一個Bootstrapper用於啟用JwtBearer驗證。
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項目自帶的其他認證方式(Basic,Forms和Stateless),就會發現下面的才是關鍵,其他的只是用於JwtBearer認證的配置參數。
pipelines.EnableJwtBearerAuthentication(configuration);
下面簡單介紹一下配置參數。
配置參數主要有兩個,一個是TokenValidationParameters , 一個是Challenge 。
其中最主要的參數TokenValidationParameters,這是用來驗證客戶端傳過來的token是否合法的!
它位於Microsoft.IdentityModel.Tokens這個命名空間下面。
Challenge參數則是用於指定在Unauthorized時Http響應頭中WWW-Authenticate的值。它的默認值是Bearer
注:Challenge參數是從Microsoft.AspNetCore.Authentication.JwtBearer項目借鑒過來的。
到這里, 我們已經完成了對JwtBearer認證的配置和啟用,下面還要驗證這個配置是否已經生效了!
創建一個新的Module,並在這個Module中使用RequiresAuthentication。
public class SecurityModule : NancyModule
{
public SecurityModule() : base("/demo")
{
//important
this.RequiresAuthentication();
Get("/",_=>
{
return "JwtBearer authentication";
});
}
}
注: 這里需要引用Nancy.Security這個命名空間
到這里,驗證的代碼也已經寫好了,當我們訪問 http://yourdomain.com/demo 的時候
瀏覽器會提示我們The requested resource requires user authentication , 並且在響應頭中我們可以看到WWW-Authenticate對應的值是Bearer。
我們創建一個合法的token值,然后通過Fiddler再發起一次請求,看看能否正常返回我們要的結果。
下面的代碼是生成一個測試token用的,其中的JwtSecurityToken對象應當與前面的配置一樣,才能確保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執行這個帶上了token的請求,大致結果如下 :
可以看到成功取到了相應的內容!
然后是本次測試用的token值相關的信息:
注:用Fiddler發起請求的時候,記得要在請求頭部加上Authorization,它的值是
Bearer+空格+token值
到這里,已經展示了如何使用這個JwtBearer認證的組件。
下面就介紹一下是怎么實現的這個組件!
如何實現
在繼續下面的內容之前,我假設大家對Nancy的Pipelines有所了解,如果不了解的可以參考我以前的下面的鏈接
因為其中的BeforePipeliine和AfterPipeline是實現這個認證組件的重要切入點。
另外,實現上還用了Nancy項目的代碼風格去編寫的代碼,所以你可能會發現與其自帶的Basic認證等寫法差不多。
從我們上面的例子使用來說明內部實現。
在上面例子的啟動器(Bootstrapper)中,我們有一行啟用JwtBearer認證的入口。這個入口是IPipelines的一個擴展方法。
/// <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);
}
在這個擴展方法中,調用了JwtBearerAuthentication這個靜態類的Enable方法,同時傳遞了當前的pipeline和JwtBearer認證的參數給這個方法。
下面是Enable方法的具體實現。
/// <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為例,我們把一個委托對象加入到了請求之前要處理的一個集合中去。這樣在每次請求之前都會去處理這個委托。
所以這里有兩個部分。
- 請求處理之前的token認證
- 請求處理之后的響應
先來看看請求處理之前的token認證如何處理
private static Func<NancyContext, Response> GetLoadAuthenticationHook(JwtBearerAuthenticationConfiguration configuration)
{
return context =>
{
Validate(context,configuration);
return null;
};
}
這里也是一個空殼,用於返回AddItemToStartOfPipeline方法需要的委托對象。
真正處理token的還是Validate這個方法。認證的處理還借助了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)
{
}
}
}
要對token進行驗證,首先要知道token是從那里來的。常規情況下,都是將這個token放到請求頭的Authorization中。
所以第一步是要從請求頭中取出Authorization的值。這個值是必須以Bearer
開頭的一個字符串。注意是Bearer加一個空格!
而我們要驗證的部分是去掉開頭這部分之后的內容。只需要構造一個JwtSecurityTokenHandler實例並調用這個實例的ValidateToken方法,並把要驗證的token值和我們的配置傳進去即可。
驗證成功后,最為主要的一步是將ValidateToken方法的返回值賦給當前Nancy上下文的CurrentUser!!
當驗證失敗的時候,ValidateToken方法會拋出一個異常,這里只catch了這個異常,並沒有進行其他額外的處理。要處理無非也就是記錄日記,可以在這里trace一下,配合Diagnostics的使用。但是目前並沒有這樣做。
到這里,Before已經OK了,現在要處理After了。
當然對於After,也是只處理401(Unauthorized)的情況。主要是告訴客戶端 “當前請求的資源需要用戶認證”,並告訴客戶端當前請求的資源需要那種認證類型。
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);
}
};
}
一個簡單的判斷加上響應頭部的處理。
到這里,這個JwtBearer認證的組件已經ok了。
當然這里只介紹了Pipeline的實現,還有一個是基於NancyModule的實現,本質還是pipeline的處理,所以這里就不累贅了。
寫在最后
雖然簡單介紹了如何全使用和實現Nancy.Authentication.JwtBearer這個組件,相信大家對token(access_token)的使用是沒有太大疑問的。可能大家有疑問的是refresh_token的使用。
但是,對於refresh_token的使用,可以說因場景而異,也因人而異。只需要記住一點即可:refresh_token是用來換取一個新的並且可用的access_token。
本文已同步到Nancy之大雜燴