【ASP.NET Core】用組態檔來設定授權角色

2023-01-23 18:00:12

在開始之前,老周先祝各個次元的夥伴們新春快樂、生活愉快、萬事如意。

在上一篇水文中,老周介紹了角色授權的一些內容。本篇咱們來聊一個比較實際的問題——把用於授權的角色名稱放到外部設定,不要寫死,以方便後期修改。

由於要設定的東西比較簡單,咱們並不需要存在資料庫,而是用 JSON 檔案設定就可以了。將授權策略和角色列表關聯起來。比如,老周這裡有個 authorRoles.json 檔案,它的內容如下:

{
  "cust1": {
    "roles": ["admin", "supperuser"]
  },
  "cust2": {
    "roles": ["user", "web", "logger"]
  }
}

其中,cust1、cust2 是策略名稱,所以上面就設定了兩個授權策略。每個策略下有個 roles 屬性,它的值是陣列,這個陣列用來指定此策略下允許的角色列表。故:cust1 策略下允許admin、supperuser兩種角色的使用者存取;cust2 策略下允許 user、web、logger 角色的使用者存取。

在 WebApplicationBuilder 的設定中,咱們可以單獨載入 authorRoles.json 檔案中的內容,然後根據組態檔內容動態新增授權策略。

1、先把組態檔中的內容讀出來。

// 組態檔名
const string roleConfigFile = "authorRoles.json";
// 單獨載入設定
IConfigurationBuilder configBuilder = new ConfigurationBuilder();
// 新增設定源,此處是JSON檔案
configBuilder.AddJsonFile(roleConfigFile);
// 生成設定樹物件
IConfiguration myconfig = configBuilder.Build();

此時,myconfig 變數中就包含了 authorRoles.json 檔案的內容了。

2、動態新增授權策略。

var builder = WebApplication.CreateBuilder(args);

// 根據組態檔的內容來設定授權策略
builder.Services.AddAuthorization(opt =>
{
    foreach (IConfigurationSection cc in myconfig.GetChildren())
    {
        var policyName = cc.Key;
        opt.AddPolicy(policyName, pbd =>
        {
            // 獲取子節點
            var roles = cc.GetSection("roles");
            // 取出角色名稱列表
            string[]? roleslist = roles.Get<string[]>();
            if (roleslist is not null)
            {
                // 新增角色
                pbd.RequireRole(roleslist);
                // 關聯驗證架構
                pbd.AddAuthenticationSchemes(CustAuthenticationSchemeDefault.SchemeName);
            }
        });
    }
});

在讀設定的時候,GetChildren 方法會返回兩個節點:cust1 和 cust2。然後用 GetSection 再讀下一層,即 roles。接著用 Get 方法就能把字串陣列型別的角色列表讀出來了。

這裡關聯了一個驗證架構(或叫驗證方案),這個驗證架構是老周自己寫的,主要是為了簡單。老周這個範例是用 Web API 的形式呈現的,所以,不用 Cookie,而是用一個簡單的 Token,呼叫時附加在 URL 的查詢字串中傳遞給伺服器。

如果你的專案的 Token 只是在自己專案中用,不用遵守通用標準,你完全可以自己生成。生成方式你看著辦,比如用隨機位元組什麼的都行。在 Token 中不要帶密碼等安全資訊。畢竟,Token 這種東西你說安全,也不見得多安全,別人只要拿到你的 Token 就可以代替你存取伺服器。當然你會說,我把 Token 加密再傳輸。其實別人盜你的 Token 根本不需要知道明文,人家只要按照正確的傳遞方式(如 Query String、Cookies 等),把你加密後的 Token 放上去,也可以冒用你身份的。所以,很多開放平臺都會分配給你 App Key 和金鑰,並且強調你的金鑰必須保管好,不能讓別人知道。

下面看看老周自己寫的驗證。

    using Microsoft.AspNetCore.Authentication;
    using Microsoft.AspNetCore.Http;
    using System.Threading.Tasks;

    public class CustAuthenticationHandler : IAuthenticationHandler
    {
#pragma warning disable CS8618
        private HttpContext HttpContext { set; get; }
        private AuthenticationScheme Scheme { get; set; }
#pragma warning restore CS8618

        public Task<AuthenticateResult> AuthenticateAsync()
        {
            // 獲取設定的Token
            IConfiguration appconfig = HttpContext.RequestServices.GetRequiredService<IConfiguration>();
            string[]? tks = appconfig.GetSection("custAuthen:tokens").Get<string[]>();
            if (tks != null && tks.Length > 0 && HttpContext.Request.Query.TryGetValue("token", out var reqToken))
            {
                // 看看有沒有效
                if (!tks.Any(t => t == reqToken))
                {
                    return Task.FromResult(AuthenticateResult.Fail("未提供有效的Token"));
                }
                // 成功
                var tickit = new AuthenticationTicket(HttpContext.User, Scheme.Name);
                return Task.FromResult(AuthenticateResult.Success(tickit));
            }
            return Task.FromResult(AuthenticateResult.NoResult());
        }

        public Task ChallengeAsync(AuthenticationProperties? properties)
        {
            HttpContext.Response.StatusCode = StatusCodes.Status401Unauthorized;
            return Task.CompletedTask;
        }

        public Task ForbidAsync(AuthenticationProperties? properties)
        {
            HttpContext.Response.StatusCode = StatusCodes.Status403Forbidden;
            return Task.CompletedTask;
        }

        public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context)
        {
            if (context == null) throw new ArgumentNullException("context");
            HttpContext = context;
            Scheme = scheme;
            // 看看驗證架構是否一致
            if (!scheme.Name.Equals(CustAuthenticationSchemeDefault.SchemeName, StringComparison.OrdinalIgnoreCase))
            {
                throw new Exception("驗證架構不一致");
            }
            return Task.CompletedTask;
        }
    }

    public static class CustAuthenticationSchemeDefault
    {
        public readonly static string SchemeName = "CustToken";
    }

這裡老周沒有用什麼高階演演算法生成 Token,而是四個字串(字串也是隨便輸入的),表示四個 Token,只要有一個匹配就算是驗證成功了。這些 Token 全寫在 appsettings.json 裡面。

{
  "Logging": {
    ……
    }
  },
  "AllowedHosts": "*",
  "custAuthen": {
    "tokens": [
      "662CV08Y4GHXOP3",
      "BI4C68DLO2HOS0D",
      "7GSEJ0J8F0246K5",
      "O9FG6V974KWO9G8"
    ]
  }
}

所以,存取這四個 Token 的設定路徑就是 custAuthen:tokens。

在實現 ForbidAsync 和 ChallengeAsync 方法時,不要呼叫 HttpContext 的擴充套件方法 ForbidAsync、ChallengeAsync,因為這些擴充套件方法內部是通過呼叫 AuthenticationService 類的 ForbidAsync、ChallengeAsync 方法實現的。最終又會回過頭來呼叫 CustAuthenticationHandler 類的  ChallengeAsync、ForbidAsync 方法。這等於轉了一圈,到頭來自己呼叫自己,易造成無限遞迴。所以這裡我只設定一個 Status Code 就好了。

在服務容器上註冊一下自定義的驗證處理方案。

var builder = WebApplication.CreateBuilder(args);
// 新增驗證功能
builder.Services.AddAuthentication(opt =>
{
    // 註冊驗證架構(方案)
    opt.AddScheme<CustAuthenticationHandler>(CustAuthenticationSchemeDefault.SchemeName, displayName: null);
});

所以,整個應用程式的初始化程式碼就是這樣。

// 組態檔名
const string roleConfigFile = "authorRoles.json";
// 單獨載入設定
IConfigurationBuilder configBuilder = new ConfigurationBuilder();
// 新增設定源,此處是JSON檔案
configBuilder.AddJsonFile(roleConfigFile);
// 生成設定樹物件
IConfiguration myconfig = configBuilder.Build();

var builder = WebApplication.CreateBuilder(args);
// 新增驗證功能
builder.Services.AddAuthentication(opt =>
{
    // 註冊驗證架構(方案)
    opt.AddScheme<CustAuthenticationHandler>(CustAuthenticationSchemeDefault.SchemeName, displayName: null);
});
// 根據組態檔的內容來設定授權策略
builder.Services.AddAuthorization(opt =>
{
    foreach (IConfigurationSection cc in myconfig.GetChildren())
    {
        var policyName = cc.Key;
        opt.AddPolicy(policyName, pbd =>
        {
            // 獲取子節點
            var roles = cc.GetSection("roles");
            // 取出角色名稱列表
            string[]? roleslist = roles.Get<string[]>();
            if (roleslist is not null)
            {
                // 新增角色
                pbd.RequireRole(roleslist);
                // 關聯驗證架構
                pbd.AddAuthenticationSchemes(CustAuthenticationSchemeDefault.SchemeName);
            }
        });
    }
});
builder.Services.AddControllers();
var app = builder.Build();

 

之後,是設定中介軟體管道。為了簡單演示,老周沒有寫用於身份驗證的 Web API,而是直接通過 URL 引數來提供當前存取者的角色。實際開發中不能這樣做,而應該從資料庫中根據使用者查詢出使用者的角色。但此處是為了演示的簡單,也是為了延長鍵盤壽命,就不建資料庫了,不然完成這個範例需要一坤年的時間。

不過,咱們知道,授權是用 Claim 來收集資訊的,所以,要在授權執行之前收集好資訊。我這裡用一箇中介軟體,在授權和呼叫 API 之前執行。

app.Use((context, next) =>
{
    var val = context.Request.Query["role"];
    string? role = val.FirstOrDefault();
    if(role != null)
    {
        ClaimsIdentity id = new(new[]
        {
            new Claim(ClaimTypes.Role, role)
        }/*, CustAuthenticationSchemeDefault.SchemeName*/);
        ClaimsPrincipal p = new(id);
        context.User = p;
    }
    return next();
});

由於 WebApplication 物件預設幫我們呼叫了 UseRouting 和 UseEndpoints 方法。Web API 在存取時路由的是 MVC 控制器,直接走 End point 路線,會導致咱們上面的 Use 方法設定使用者角色的中介軟體不執行。所以要重新呼叫 UseRouting 和 UseAuthorization 方法。

app.UseRouting();
app.UseAuthorization();
app.MapControllers();

 

用一個名為 Demo 的控制器來做驗證。

[Route("api/[controller]")]
[ApiController]
public class DemoController : ControllerBase
{
    [HttpGet("backup")]
    [Authorize("cust1")]
    public string Backup() => "備份完成";

    [HttpGet("hello/{name}")]
    [Authorize("cust2")]
    public string Hello(string name)
    {
        return $"你好,{name}";
    }
}

cust1、cust2 正是咱們前面設定裡的節點名稱,即策略名稱。例如,呼叫 Hello 方法使用 cust2 授權策略,它設定的角色為 user、web、loggor。

 

在呼叫這些 API 時,URL需要攜帶兩個引數:

1、role:使用者角色;

2、token:用於驗證。

用 http-repl 工具先測試 demo/backup 方法的呼叫。

 get /api/demo/backup?role=web&token=O9FG6V974KWO9G8

上述呼叫提供的使用者角色為 web,根據前面的設定,web 角色應使用 cust2 策略。但 Backup 方法應用的授權策略是 cust1,因此無權存取,返回 403。

咱們改一下,使用角色為 admin 的使用者。

get /api/demo/backup?role=admin&token=O9FG6V974KWO9G8

此時,授權通過,返回 200。

 

 

存取 Hello 方法也一樣,授權策略是 cust2,允許的角色是 user、web、logger。

get /api/demo/hello/小紅?role=web&token=BI4C68DLO2HOS0D

授權通過,返回 200 狀態碼。

 

 

用組態檔來設定角色,算是一種簡單方案。如果授權需要的角色有變化,只要修改組態檔中的角列表就行。當然,像 cust1、cust2 等策略名稱要事先規劃好,策略名稱不隨便改。

有大夥伴會說,乾脆連MVC控制器或其方法上應用哪個授權策略也轉到組態檔中,豈不美哉!好是好,但不好弄。可以要自己寫個授權的 Filter,主要問題是自己寫有時候沒有官方內建的程式碼嚴謹,容易出「八阿哥」。

所以,綜合複雜性與靈活性的平衡,在不擴充套件現有介面的前提下,咱們這個範例是比較好的,至少,咱們可以在組態檔中修改角色列表。