JWT 是一個老牌的套件,從 nuget 上來看他,甚至還比 System.IdentityModel.Tokens.Jwt 還要資深,使用起來也相當的簡單
以下是我的使用過程分享
開發環境
- VS 2017.15.9.4
- Install-Package JWT
- 專案建置步驟請參考 [Web API] 使用 OWIN 進行測試
使用手冊
官網介紹的蠻清楚的,就不再多寫了
https://github.com/jwt-dotnet/jwt
這需要閱讀一下 JWT 的協議,才知道他在做甚麼
產生JWT
JwtManager.cs
這有兩個方法,產生 Token、驗證 Token
https://github.com/yaochangyu/sample.dotblog/blob/master/WebAPI/JWT/jwt-dotnet/Server/JwtManager.cs
我把 exp、nbf,這兩個 claim 依賴 Now 屬性,等會要由測試程式注入時間模擬過期
public static string GenerateToken(string userName, int expireMinutes = 20) { var symmetricKey = Convert.FromBase64String(Secret); IJwtAlgorithm algorithm = new HMACSHA256Algorithm(); IJsonSerializer serializer = new JsonNetSerializer(); IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder(); IJwtEncoder encoder = new JwtEncoder(algorithm, serializer, urlEncoder); var now = (DateTimeOffset) Now.Value; var expired = now.AddMinutes(expireMinutes).ToUnixTimeSeconds(); var notBefore = now.ToUnixTimeSeconds(); var payload = new Dictionary<string, object> { {"name", userName}, {"exp", expired}, {"nbf", notBefore} }; var token = encoder.Encode(payload, symmetricKey); return token; }
驗證Token
這裡用到時間的驗證,我自己實作 IDateTimeProvider,這樣才能注入時間,模擬過期,通過驗證則回傳 ClaimsPrincipal
public static bool TryValidateToken(string token, out ClaimsPrincipal principal) { var symmetricKey = Convert.FromBase64String(Secret); principal = null; var result = false; try { IJsonSerializer serializer = new JsonNetSerializer(); IDateTimeProvider provider = new DateTimeProvider {Now = Now}; IJwtValidator validator = new JwtValidator(serializer, provider); IBase64UrlEncoder urlEncoder = new JwtBase64UrlEncoder(); IJwtDecoder decoder = new JwtDecoder(serializer, validator, urlEncoder); var payload = decoder.DecodeToObject(token, symmetricKey, true); List<Claim> claims = new List<Claim>(); foreach (var item in payload) { if (item.Value == null) { continue; } var key = item.Key; var value = item.Value.ToString(); if (key.ToLower() == "name") { claims.Add(new Claim(ClaimTypes.Name, value)); } else if (key.ToLower() == "role") { claims.Add(new Claim(ClaimTypes.Role, value)); } else { claims.Add(new Claim(key, value)); } } var identity = new ClaimsIdentity(claims, "JWT"); principal = new ClaimsPrincipal(identity); result = true; } catch (TokenExpiredException) { Console.WriteLine("Token has expired"); } catch (SignatureVerificationException) { Console.WriteLine("Token has invalid signature"); } catch (Exception ex) { Console.WriteLine(ex.Message); } return result;
AuthorizeAttribute
這裡我用 AuthorizeAttribute 進行驗證
驗證 Token,並拿到 ClaimsPrincipal 塞給 Thread.CurrentPrincipal 和 RequestContext
public class JwtAuthorizeAttribute : AuthorizeAttribute { protected override bool IsAuthorized(HttpActionContext actionContext) { var authorization = actionContext.Request.Headers.Authorization; if (authorization != null && authorization.Scheme == "Bearer") { var token = authorization.Parameter; if (JwtManager.TryValidateToken(token, out var principal)) { Thread.CurrentPrincipal = principal; actionContext.Request.GetRequestContext().Principal = principal; } } return base.IsAuthorized(actionContext); } }
頒發授權 API
public class TokenController : ApiController { // POST api/token [AllowAnonymous] public IHttpActionResult Post(LoginData loginData) { if (this.CheckUser(loginData.UserName, loginData.Password)) { var token = JwtManager.GenerateToken(loginData.UserName); return new ResponseMessageResult(new HttpResponseMessage { StatusCode = HttpStatusCode.OK, Content = new StringContent(token, Encoding.UTF8) }); } throw new HttpResponseException(HttpStatusCode.Unauthorized); } public bool CheckUser(string username, string password) { // should check in the database return true; } public class LoginData { public string UserName { get; set; } public string Password { get; set; } } }
保護資源
把剛剛的 AuthorizeAttribute 加進 Filter,套用在整個應用程式
public static class WebApiConfig { public static void Register(HttpConfiguration config) { // Web API configuration and services config.Filters.Add(new JwtAuthorizeAttribute()); ..... } }
這樣 api/Value 就需要授權才能訪問了
public class ValueController : ApiController { public IHttpActionResult Get() { return new ResponseMessageResult(new HttpResponseMessage(HttpStatusCode.OK) { Content = new StringContent("value") }); } }
當訪問資源沒有帶入 JWT,就會得到401
[TestMethod] public void Access_Resource_HttpStatus_Should_Be_Unauthorized() { var queryUrl = "api/value"; var queryResponse = s_client.GetAsync(queryUrl).Result; Assert.AreEqual(HttpStatusCode.Unauthorized, queryResponse.StatusCode); }
訪問資源之前先取得 Token,再帶入 Header,便能順利的訪問 api/value
[TestMethod] public void Access_Resource_Should_Be_Value() { var loginUrl = "api/token"; var queryUrl = "api/value"; var tokenResponse = s_client.PostAsJsonAsync(loginUrl, new LoginData { UserName = "yao", Password = "1234" }) .Result; Assert.AreEqual(HttpStatusCode.OK, tokenResponse.StatusCode); var token = tokenResponse.Content.ReadAsStringAsync().Result; s_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); var queryResponse = s_client.GetAsync(queryUrl).Result; Assert.AreEqual(HttpStatusCode.OK, queryResponse.StatusCode); var result = queryResponse.Content.ReadAsStringAsync().Result; Assert.AreEqual("value", result); }
Validation 'exp' (Expiration Time)
接下來,我要驗證這個 JWT 套件所頒發的 Token,當過期時會不會驗證失敗,所以我注入了兩次時間 JwtManager.Now,第一次是過期的時間,第二次是目前的時間。
[TestMethod] public void Token_Expired_Should_Be_Unauthorized() { var loginUrl = "api/token"; var queryUrl = "api/value"; //注入期望時間 JwtManager.Now = DateTime.SpecifyKind(new DateTime(2000, 1, 1), DateTimeKind.Utc); var tokenResponse = s_client.PostAsJsonAsync(loginUrl, new LoginData { UserName = "yao", Password = "1234" }) .Result; Assert.AreEqual(HttpStatusCode.OK, tokenResponse.StatusCode); var token = tokenResponse.Content.ReadAsStringAsync().Result; s_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); //模擬時間經過30分鐘 JwtManager.Now = JwtManager.Now.Value.AddMinutes(30); var queryResponse = s_client.GetAsync(queryUrl).Result; Assert.AreEqual(HttpStatusCode.Unauthorized, queryResponse.StatusCode); JwtManager.Now = null; }
Validation 'nbf' (Not Before)
還有一個跟時間有關係的 claim,這個是在什麼時間之前,該 token 不能使用,同樣的我也注入兩次時間,第一次是設定能使用的時間,第二次是模擬目前時間
[TestMethod] public void Token_NotBefore_Should_Be_Unauthorized() { var loginUrl = "api/token"; var queryUrl = "api/value"; //注入Token可以使用的時間 JwtManager.Now = DateTime.SpecifyKind(new DateTime(2000, 1, 2), DateTimeKind.Utc); var tokenResponse = s_client.PostAsJsonAsync(loginUrl, new LoginData { UserName = "yao", Password = "1234" }) .Result; Assert.AreEqual(HttpStatusCode.OK, tokenResponse.StatusCode); var token = tokenResponse.Content.ReadAsStringAsync().Result; s_client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token); //注入目前時間 JwtManager.Now = DateTime.SpecifyKind(new DateTime(2000, 1, 1), DateTimeKind.Utc); var queryResponse = s_client.GetAsync(queryUrl).Result; Assert.AreEqual(HttpStatusCode.Unauthorized, queryResponse.StatusCode); JwtManager.Now = null; }
範例位置:
https://github.com/yaochangyu/sample.dotblog/tree/master/WebAPI/JWT/jwt-dotnet
若有謬誤,煩請告知,新手發帖請多包涵
Microsoft MVP Award 2010~2017 C# 第四季
Microsoft MVP Award 2018~2022 .NET