【ASP.NET Core】用配置檔案來設定授權角色

東邪獨孤發表於2023-01-23

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

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

由於要配置的東西比較簡單,我們們並不需要存在資料庫,而是用 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,主要問題是自己寫有時候沒有官方內建的程式碼嚴謹,容易出“八阿哥”。

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

相關文章