前言 在上一篇 化繁為簡:02 後端篇(一) 中,我們最後製作一個可以添加內容安全策略 (Content Security Policy ) 的 Middleware,其實還有很多事情我們可以集中到 Middleware 處理,例如操作紀錄,我們在 Controllers 內僅能了解功能面的存取狀況,但是拉到 Middleware 來紀錄時,就可以了解至整個作業面的資料存取是否合理,如果發現異常就可以加入還名單並終止使用者權限(中斷他的請求),而這些集中化的紀錄還可以拿來分析優化系統,說了這些,我們今天卻不是要實作它,重要的是要說明我們為了加速開發而在弱化整個系統架構時,其實後續事可以逐步強化回來,讓系統回覆該有的嚴謹度,在某種程度上與敏捷式開發 有著同樣的目標。
雖著智慧手機的興起,大量的程式設計師透入 App 開發,以往系統間的資料交換首選幾乎都是以嚴謹聞名的 XML 格式,如今雖著 App 的迅速發展,有著輕量化的 JSON 資料格式已經幾乎取代了 XML 的地位,而隨著 JSON 的流行 JWT (JSON Web Tokens ) 也變成很普及的身分驗證方式,今天我們就來弱化 JWT。
JSON Web Tokens JWT 主要的流程就是當我們登入(驗證)成功時,系統會產生一組可以代表我們身分的 Token ,後續發出請求(Request)時只要夾帶這組 Token ,後端系統就可以藉此來驗證他的身分,這樣可以避免發出請求時需要夾帶敏感的身分資料。
下面程式碼是一個簡單產生 Token 的作法,我們可在 Token 內夾帶一些其他資訊,例如:有效期限、有效簽證,但是使用者的敏感資料就避免存放。
AuthController.cs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 using System;using System.IdentityModel.Tokens.Jwt;using System.Security.Claims;using System.Text;using Microsoft.AspNetCore.Mvc;using Microsoft.Extensions.Configuration;using Microsoft.IdentityModel.Tokens;namespace WebAPI.Controllers { [Route("api/[controller]" ) ] [ApiController ] public class AuthController : ControllerBase { private readonly IConfiguration Config; public AuthController (IConfiguration config ) { Config = config; } private string BuildToken (Models.SysUser user, string tokenUid, string companyName ) { var claims = new [] { new Claim(JwtRegisteredClaimNames.Sid, tokenUid), new Claim(JwtRegisteredClaimNames.UniqueName, user.Name), new Claim(JwtRegisteredClaimNames.Email, user.Email), new Claim(JwtRegisteredClaimNames.FamilyName, companyName) }; var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Config["Tokens:IssuerSigningKey" ])); var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256Signature); var token = new JwtSecurityToken( issuer: Config["Tokens:ValidIssuer" ], audience: Config["Tokens:ValidAudience" ], claims: claims, expires: DateTime.Now.AddDays(1 ), signingCredentials: creds ); return new JwtSecurityTokenHandler().WriteToken(token); } } }
這常情況下我們會在 Token 存放可以代表使用者的資訊,例如:User Id
,不過筆者會建立一個資料表(log_login
),每次登入驗證成功之後系統會產生一組 臨時ID 來替代使用者真實的 ID,並存放到 Token 的 Sid
內,如下圖,同一個使用者(user_uid) 每次登入存儲在 Token 內的 Sid 都會不同,多一個資料表就可以讓安全性再提高一點。
筆者此系統是讓一個帳號 (使用者 User )可以使多家公司 (Company ),所以中間會多一個員工 (Employee )來關聯。
AuthorizationMiddleware 接下來我們要建立一個 Authorization 的中介軟體,其主要的功能是解析 JWT 的 Token,並將 Token 轉換回使用者,這樣後續的 Controller 就可以直接抓取使用者資訊。
AuthorizationMiddleware.cs
的主要程式碼如下,這邊我們並沒有去驗證 Token 的授權是否有效,而是僅有解析 Token 資料,跟 資料表 log_login
比對,如果有資料就順便把該員工資料填入到 context.Items["emp"]
內,後面的程式就從此參數取得目前的請求是由哪一位員工發出。
AuthorizationMiddleware.cs 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 using System;using System.IdentityModel.Tokens.Jwt;using System.Linq;using System.Threading.Tasks;using Microsoft.AspNetCore.Builder;using Microsoft.AspNetCore.Http;namespace WebAPI { public class AuthorizationMiddleware { private readonly RequestDelegate _next; public AuthorizationMiddleware (RequestDelegate next ) { _next = next; } public async Task Invoke (HttpContext context, Models.CloudContext db ) { if (!CheckAuthorization(context, db, out string message)) { if (string .IsNullOrEmpty(message)) message = "Unauthorized" ; context.Response.StatusCode = StatusCodes.Status401Unauthorized; await context.Response.WriteAsync(message); return ; } await _next(context); } static bool CheckAuthorization (HttpContext context, Models.CloudContext db, out string message ) { message = string .Empty; string path = context.Request.Path.Value.ToLower(); switch (path) { case "/api/auth/login" : return true ; default : break ; } if (context.Request.Headers.Keys.Contains("Authorization" )) { if (context.Request.Headers.Keys.Contains("Authorization" )) { string accesToken = context.Request.Headers["Authorization" ].ToString(); accesToken = accesToken.Replace("Bearer " , string .Empty); var handler = new JwtSecurityTokenHandler().ReadJwtToken(accesToken); if (handler != null ) { var sidClaim = handler.Claims.FirstOrDefault(x => x.Type == JwtRegisteredClaimNames.Sid); if (sidClaim != null ) { if (Guid.TryParse(sidClaim.Value, out Guid tokenUid) && SetUserInfo(context, db, tokenUid)) { return true ; } } } } } message = "驗證錯誤,請重新登入" ; return false ; } static bool SetUserInfo (HttpContext context, Models.CloudContext db, Guid tokenUid ) { var queryLog = db.LogLogins.FirstOrDefault(x => x.Uid == tokenUid); if (queryLog == null ) return false ; var userInfo = new Models.SysEmployee { UserUid = queryLog.UserUid, CompanyUid = queryLog.CompanyUid, Email = string .Empty, Uid = queryLog.EmployeeUid }; context.Items["emp" ] = userInfo; return true ; } } partial class Ext { internal static IApplicationBuilder UseAuthorizationMiddleware (this IApplicationBuilder builder ) { return builder.UseMiddleware<AuthorizationMiddleware>(); } } }
最後一樣在 Startup.cs
的 Configure
內添加 app.UseAuthorizationMiddleware();
,因為沒有使用到內建的驗證機制,所以可以先移除 app.UseAuthorization();
。
後記 在 AuthorizationMiddleware
內我們沒有驗證 Token 的授權證書,這看起來已經是捨棄 JTW 的稽核機制,筆者有另一種偷吃步的做法,就是將燈登入驗證的 Token 的資料也存放到資料表 log_login
內直接做字串比對。 對筆者來說 JWT 拿來做身分驗證是很實用的,但是不應該過度信任 JWT 的安全性,我們應該在它之外也加入其他安全稽核功能,還記得一開始我們說可以增加操作紀錄 的 Middleware 嗎?當我們發現異常操作時可以在 log_login
內將該臨時 ID 給註銷2掉(disable = true
),這樣既使該 Token 仍然有時效,但是也無法再對後端做任何存取。
如果網站沒有其他軟硬體的 DDoS 防護機制,我們也可以在 Middleware 內加入基本的阻擋機制,針對不當存取的 IP 給予適當 的封鎖。