【ASP.NET Core】用配置文件來設置授權角色

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

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

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

所以,綜合複雜性與靈活性的平衡,在不擴展現有接口的前提下,咱們這個示例是比較好的,至少,咱們可以在配置文件中修改角色列表。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章