title: 化繁為簡:03 後端篇(二)驗證
date: 2021-06-25
categories: Projects
keywords:
在上一篇 化繁為簡:02 後端篇(一) 中,我們最後製作一個可以添加內容安全策略 (Content Security Policy) 的 Middleware,其實還有很多事情我們可以集中到 Middleware 處理,例如操作紀錄,我們在 Controllers 內僅能了解功能面的存取狀況,但是拉到 Middleware 來紀錄時,就可以了解至整個作業面的資料存取是否合理,如果發現異常就可以加入還名單並終止使用者權限(中斷他的請求),而這些集中化的紀錄還可以拿來分析優化系統,說了這些,我們今天卻不是要實作它,重要的是要說明我們為了加速開發而在弱化整個系統架構時,其實後續事可以逐步強化回來,讓系統回覆該有的嚴謹度,在某種程度上與敏捷式開發有著同樣的目標。
雖著智慧手機的興起,大量的程式設計師透入 App 開發,以往系統間的資料交換首選幾乎都是以嚴謹聞名的 XML 格式,如今雖著 App 的迅速發展,有著輕量化的 JSON 資料格式已經幾乎取代了 XML 的地位,而隨著 JSON 的流行 JWT(JSON Web Tokens) 也變成很普及的身分驗證方式,今天我們就來弱化 JWT。
JWT 主要的流程就是當我們登入(驗證)成功時,系統會產生一組可以代表我們身分的 Token,後續發出請求(Request)時只要夾帶這組 Token,後端系統就可以藉此來驗證他的身分,這樣可以避免發出請求時需要夾帶敏感的身分資料。
下面程式碼是一個簡單產生 Token 的作法,我們可在 Token 內夾帶一些其他資訊,例如:有效期限、有效簽證,但是使用者的敏感資料就避免存放。
{% codeblock AuthController.cs lang:cs %}
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);
}
}
}
{% endcodeblock %}
這常情況下我們會在 Token 存放可以代表使用者的資訊,例如:User Id
,不過筆者會建立一個資料表(log_login
),每次登入驗證成功之後系統會產生一組 臨時ID 來替代使用者真實的 ID,並存放到 Token 的 Sid
內,如下圖,同一個使用者(user_uid) 每次登入存儲在 Token 內的 Sid 都會不同,多一個資料表就可以讓安全性再提高一點。
筆者此系統是讓一個帳號(使用者 User)可以使多家公司(Company),所以中間會多一個員工(Employee)來關聯。
接下來我們要建立一個 Authorization 的中介軟體,其主要的功能是解析 JWT 的 Token,並將 Token 轉換回使用者,這樣後續的 Controller 就可以直接抓取使用者資訊。
AuthorizationMiddleware.cs
的主要程式碼如下,這邊我們並沒有去驗證 Token 的授權是否有效,而是僅有解析 Token 資料,跟 資料表 log_login
比對,如果有資料就順便把該員工資料填入到 context.Items["emp"]
內,後面的程式就從此參數取得目前的請求是由哪一位員工發出。
{% codeblock AuthorizationMiddleware.cs lang:cs %}
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>();
}
}
}
{% endcodeblock %}
最後一樣在 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 給予適當的封鎖。