上一篇介紹了JWT身份認證的原理及.net core webapi中如何使用JWT。
本篇繼續介紹如何在客戶端設置JWT認證的Token信息以及Web服務器如何去解析Token中的內容並正確識別出用戶身份。
注:這里的客戶端可以是瀏覽器、桌面應用、手機APP、小程序等。
本項目中的認證流程是這樣的:
1. 用戶訪問登錄接口API(http://localhost:52384/api/users/login) → 2. 得到 JWT Token → 3. 后續API訪問的請求Header中加上此 Token
一、客戶端調用登錄接口生成Token字符串,代碼如下:
1 [Route("login")] 2 [HttpGet] 3 public ContentResult LoginUser() 4 { 5 string acc = Request.Query["acc"]; //接收查詢參數acc 6 string pwd = Request.Query["pwd"]; //接收查詢參數pwd 7 8 //如果參數為空返回提示信息 9 if (string.IsNullOrEmpty(acc) || string.IsNullOrEmpty(pwd)) 10 { 11 return Content("{'result':'account or password is empty!'}"); 12 } 13 14 //生成 Token 信息 15 string token = GenerateJwtToken(); 16 17 //將 Token 信息返回給客戶端 18 return Content(token); 19 } 20 21 private string GenerateJwtToken() 22 { 23 // 1. 設置加密算法 24 string algorithm = SecurityAlgorithms.HmacSha256; 25 26 // 2. 生成簽名證書,注意密鑰長度至少為16位,否則會報錯 27 SymmetricSecurityKey key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("abcdefghijklmn1234567890")); 28 SigningCredentials signing = new SigningCredentials(key, algorithm); 29 30 // 3. 構造Claims(任意添加幾個值,實際項目根據需要來設置要傳遞的信息) 31 Claim[] claims = new[] 32 { 33 new Claim(JwtRegisteredClaimNames.Sub, "aaa000"), 34 new Claim(ClaimTypes.Name, "ccc333"), 35 }; 36 37 // 4. 生成令牌 38 string issuer = null; 39 string audience = null; 40 DateTime notBefore = new DateTime(2020, 2, 1); 41 DateTime expires = new DateTime(2020, 3, 1); 42 JwtSecurityToken jwtToken = new JwtSecurityToken(issuer, audience, claims, notBefore, expires, signing); 43 44 // 5. 將令牌實例轉換成字符串 45 string strToken = new JwtSecurityTokenHandler().WriteToken(jwtToken); 46 47 return strToken; 48 }
說明:
1 . [Route("login")] 表示訪問 LoginUser( ) 這個終結點時的路徑應該是 http://www.xxx.com/api/uses/login ;
2 . 用戶的賬號密碼是以查詢字符串的形式傳遞的 ,如 ?acc=123&pwd=456 ;
3 . 為了方便,這里僅判斷了賬號/密碼不為空,實際項目中可能需要和數據庫中的值做比較 ;
4 . 編碼時要添加 System.IdentityModel.Tokens.Jwt 和 System.Security.Claims 這兩個引用 ;
二、在終結點上加上屬性 [Authorize] 啟用身份認證,代碼如下:
1 [HttpGet] 2 [Authorize] 3 public ContentResult ManageUsers() 4 { 5 //... 6 //... 7 }
注:UsersController這個控制器中還有多個終結點,為了避免逐一添加的麻煩,可以將屬性 [Authorize] 加在類名上面,
使身份驗證的功能對該控制器中的每個終結點都生效,因 LoginUser( ) 這個終結點是不需要身份驗證的,
故加上屬性 [AllowAnonymous] 就可以了,代碼如下:
1 [Route("api/[controller]")] 2 [ApiController] 3 [Authorize] 4 public class UsersController : ControllerBase 5 { 6 private ILogger<UsersController> _logger; 7 private IUserDao _userDao; 8 public UsersController(ILogger<UsersController> logger, IUserDao userDao) 9 { 10 _logger = logger; 11 _userDao = userDao; 12 } 13 14 15 16 [Route("login")] 17 [HttpGet] 18 [AllowAnonymous] 19 public ContentResult LoginUser() 20 { 21 string acc = Request.Query["acc"]; //接收查詢參數acc 22 string pwd = Request.Query["pwd"]; //接收查詢參數pwd 23 24 //... 25 //... 26 } 27 28 }
三、在Startup.cs的 ConfigureServices( ) 方法中設置身份認證的參數,代碼如下:
1 public void ConfigureServices(IServiceCollection services) 2 { 3 services.AddControllers(); 4 5 services.AddScoped<IUserDao, MySqlUserDao>(); 6 7 services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer( 8 //設置Token驗證的參數 9 options => 10 { 11 options.TokenValidationParameters = new TokenValidationParameters() 12 { 13 ValidateIssuer = false, //不驗證 issuer,因為 GenerateJwtToken() 方法生成的Token中issuer = null 14 ValidateAudience = false, //同上 15 16 //如果希望 Token 在規定時間后失效請設置成 true 17 //本例中 GenerateJwtToken()方法設置的值是 new DateTime(2020, 3, 1) 18 ValidateLifetime = false, 19 20 //需要驗證簽名key, 同時IssuerSigningKey的值與 GenerateJwtToken() 方法中的key值保持一致 21 ValidateIssuerSigningKey = true, 22 IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("abcdefghijklmn1234567890")) 23 }; 24 } 25 ); 26 }
四、測試 LoginUser( ) 和 ManageUsers( ) 這兩個終結點。
4.1 重新編譯整個項目后打開POSTMAN訪問網址 http://localhost:52384/api/users/login,結果如下:
可以訪問且顯示賬號或密碼為空的信息。
4.2 加上查詢字符串后訪問網址:http://localhost:52384/api/users/login?acc=111&pwd=222,結果如下:
如我們所願,生成了一個 JWT token字符串。
4.3 訪問網址 http://localhost:52384/api/users ,結果如下:
無法訪問,提示401沒有權限的信息。
4.4 在請求頭中加上4.2中生成的Token字符串后重新訪問,結果如下:
可以正常訪問並得到期望的結果。
注:請求頭中加的 KEY 名稱是 Authorization ,VALUE 值是 Bearer + 空格 + token字符串, 空格個數不限,
但 Authorization 和 Bearer 是關鍵字,不能寫錯。
========================================== 分割線 ==========================================
補充:
1 . Claim[ ]數組中添加元素用JwtRegisteredClaimNames和ClaimTypes的區別
訪問網址 https://jwt.io/ , 在頁面中輸入4.2中生成的 token 解析出來的值如下:
JwtRegisteredClaimNames.Sub 生成的key名和屬性名保持一致,
ClaimTypes.Name生成的key帶了一個網址前綴。
2 . 在其他終結點中如何得到 "ccc333"這個值
添加終結點 DecodeToken( ) 並設置路由屬性為 [Route("decode")](對應網址為http://localhost:52384/api/users/decode),代碼如下:
1 [Route("decode")] 2 [HttpGet] 3 public ContentResult DecodeToken() 4 { 5 //讀取 token 的值 6 string token = Request.Headers["Authorization"]; 7 token = token.Replace(" ","").Substring(6); 8 9 //得到 token 中payload部分的值 10 JwtSecurityTokenHandler handler = new JwtSecurityTokenHandler(); 11 JwtSecurityToken jwtToken = handler.ReadJwtToken(token); 12 JwtPayload payload = jwtToken.Payload; 13 14 //1. 官方字段可直接取值 15 string sub = payload.Sub; 16 string nbf = payload.Nbf.ToString(); 17 string exp = payload.Exp.ToString(); 18 19 //2. 非官方字段反序列化成字典對象后取值 20 Dictionary<string,object> dic = JsonSerializer.Deserialize<Dictionary<string, object>>(payload.SerializeToJson()); 21 string name = dic["http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name"].ToString(); 22 23 //輸出 24 string jwt1 = "@@@@ " + payload.SerializeToJson() + Environment.NewLine; 25 string jwt2 = "@@@@ sub=" + sub + "; nbf=" + nbf + "; exp=" + exp + "; name=" + name + Environment.NewLine; 26 27 return Content("result:" + Environment.NewLine + jwt1 + jwt2); 28 }
訪問網址 http://localhost:52384/api/users/decode , 結果如下:
實際項目中可以將讀取Token內容的功能封裝成一個Utility類,方便在其他方法中調用。