4

【ASP.NET Core】用配置文件来设置授权角色 - 东邪独孤

 1 year ago
source link: https://www.cnblogs.com/tcjiaan/p/17065118.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

【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。

367389-20230123163711717-1867737390.png

访问 Hello 方法也一样,授权策略是 cust2,允许的角色是 user、web、logger。

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

授权通过,返回 200 状态码。

367389-20230123164216332-30627460.png

用配置文件来设置角色,算是一种简单方案。如果授权需要的角色有变化,只要修改配置文件中的角列表就行。当然,像 cust1、cust2 等策略名称要事先规划好,策略名称不随便改。

有大伙伴会说,干脆连MVC控制器或其方法上应用哪个授权策略也转到配置文件中,岂不美哉!好是好,但不好弄。可以要自己写个授权的 Filter,主要问题是自己写有时候没有官方内置的代码严谨,容易出“八阿哥”。

所以,综合复杂性与灵活性的平衡,在不扩展现有接口的前提下,咱们这个示例是比较好的,至少,咱们可以在配置文件中修改角色列表。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK