asp.net core按用户等级授权的方法-kb88凯时官网登录

来自:网络
时间:2024-06-10
阅读:

验证和授权是两个独立但又存在联系的过程。验证是检查访问者的合法性,授权是校验访问者有没有权限查看资源。它们之间的联系——先验证再授权。

贯穿这两过程的是叫 claim 的东东,可以叫它“声明”。没什么神秘的,就是由两个字符串组成的对象,一曰 type,一曰 value。type 和 value 有着映射关系,类似字典结构的 key 和 value。claim 用来收集用户相关信息,比如

username = admin
age = 105
birth = 1990,4,12
address = 火星街130号

claimtypes 静态类定义了一些标准的 type 值。如用户名name,国家country,手机号mobilephone,家庭电话homephone 等等。你也可以自己定义一个,反正就是个字符串。

另外,还有一个claimvaluetypes 辅助类,也是一组字符串,用于描述 value 的类型。如integer、hexbinary、string、dnsname 等。其实所有 value 都是用字符串表示的,valuetypes 只是基于内容本身的含义而定义的分类,在查找和分析 claim 时有辅助作用。比如,值是 “00:15:30”,可以认为其 valuetype 是 time,这样在分析这些数据时可以方便一些。

一般,代码会在 sign in 前收集这些用户信息。作用是为后面的授权做准备。授权时会对这些用户信息进行综合评估,以决定该用户是否有能力访问某些资源。

回到本文主题。本文的重点是说授权,老周的想法是根据用户的等级来授权。比如,用户a的等级是2,如果某个url要求4级以上的用户才能访问,那么a就无权访问了。

为了简单,老周就不建数据库这么复杂的东西了,直接写个类就好了。

public class user
{
    public string? username { get; set; }
    public string? password { get; set; }
    /// 
    /// 用户等级,1-5
    /// 
    public int level { get; set; } = 1;
}

上面类中,level 属性表示的是用户等级。然后,用下面的代码来产生一些用户数据。

public static class userdatas
{
    internal static readonly ienumerable userlist = new user[]
    {
        new(){username="admin", password="123456", level=5},
        new(){username="kitty", password="112211", level=3},
        new(){username="bob",password="215215", level=2},
        new(){username="billy", password="886600", level=1}
    };
    // 获取所有用户
    public static ienumerable getusers() => userlist;
    // 根据用户名和密码校对后返回的用户实体
    public static user? checkuser(string username, string passwd)
    {
        return userlist.firstordefault(u => u.username!.equals(username, stringcomparison.ordinalignorecase) && u.password == passwd);
    }
}

这样的功能,对于咱们今天要说的内容,已经够用了。

关于验证,这里不是重点。所以老周用最简单的方案——cookie。

builder.services.addauthentication(cookieauthenticationdefaults.authenticationscheme).addcookie(opt =>
{
    opt.loginpath = "/userlog";
    opt.logoutpath = "/logout";
    opt.accessdeniedpath = "/denied";
    opt.cookie.name = "ck_auth_ent";
    opt.returnurlparameter = "backurl";
});

这个验证方案是结合 session 和 cookie 来完成的,也是web身份验证的经典方案了。上述代码中我配置了一些选项:

loginpath——当 sessionid 和 cookie 验证不成功时,自动转到些路径,要求用户登录。

logoutpath——退出登录(注销)时的路径。

accessdeniedpath——访问被拒绝后转到的路径。

returnurlparameter——回调url,就是验证失败后会转到登录url,然后会在url参数中加一个回调url。这个选项就是配置这个参数的名称的。比如这里我配置为backurl。假如我要访问/home,但是,验证失败,跳转到 /userlog 登录,这时候会在url后面加上 /userlog?backurl=/home。如果登录成功且验证也成功了,就会跳转回 backurl指定的路径(/home)。

这里要注意的是,我们不能把要求输入用户名和密码作为验证过程。验证由内置的cookieauthenticationhandler 类去处理,它只验证 session 和 cookie 中的数据是否匹配,而不是检查用户名/密码对不对。你想想,如果把检查用户名和密码作为验证过程,那岂不是每次都要让用户去输入一次?说不定每访问一个url都要验证一次的,那用户不累死?所以,输入用户名/密码登录只在 loginpath 选项中配置,只在必要时输入一次,然后配合 session 和 cookie 把状态记录下来,下次再访问,只验证此状态即可,不用再输入了。

logoutpath 和accessdeniedpath 我就不弄太复杂了,直接这样就完事。

app.mapget("/denied", () => "访问被拒绝");
app.mapget("/logout", async (httpcontext context) =>
{
    await context.signoutasync();
});

对于 loginpath,我用一个 razor pages 来处理。

@page
@using myapp
@using microsoft.aspnetcore.authentication
@using microsoft.aspnetcore.authentication.cookies
@using system.security.claims
@addtaghelper *,microsoft.aspnetcore.mvc.taghelpers
@functions{ //[ignoreantiforgerytoken] public async void onpost(string username, string password) { var u = userdatas.checkuser(username, password); if(u != null) { claim[] cs = new claim[] { new claim(claimtypes.name, u.username!), new claim("level", u.level.tostring()) //注意这里,收集重要情报 }; claimsidentity id = new(cs, cookieauthenticationdefaults.authenticationscheme); claimsprincipal p = new(id); await httpcontext.signinasync(cookieauthenticationdefaults.authenticationscheme, p); //httpcontext.response.redirect("/"); } } }

其他的各位可以不关注,重点是 onpost 方法,首先用刚才写的userdatas.checkuser 静态方法来验证用户名和密码(这个是要我们自己写代码来完成的,cookieauthenticationhandler 可不负责这个)。用户名和密码正确后,咱们就要收集信息了。收集啥呢?这个要根据你稍后在授权时要用到什么来决定的。就拿今天的主题来讲,我们需要知道用户等级,所以要收集 level 属性的值。这里 claimtype 我直接用“level”,value 就是 level 属性的值。

收集完用户信息后,要汇总到claimsprincipal 对象中,随后调用httpcontext.signinasync 扩展方法,会触发cookieauthenticationhandler 去保存状态,因为它实现了iauthenticationsigninhandler 接口,从而带有signinasync 方法。

var ticket = new authenticationticket(signincontext.principal!, signincontext.properties, signincontext.scheme.name);
    // 保存 session
   if (options.sessionstore != null)
   {
       if (_sessionkey != null)
       {
           // renew the ticket in cases of multiple requests see: https://github.com/dotnet/aspnetcore/issues/22135
           await options.sessionstore.renewasync(_sessionkey, ticket, context, context.requestaborted);
       }
       else
       {
           _sessionkey = await options.sessionstore.storeasync(ticket, context, context.requestaborted);
       }
       var principal = new claimsprincipal(
           new claimsidentity(
               new[] { new claim(sessionidclaim, _sessionkey, claimvaluetypes.string, options.claimsissuer) },
               options.claimsissuer));
       ticket = new authenticationticket(principal, null, scheme.name);
   }
  // 生成加密后的 cookie 值
   var cookievalue = options.ticketdataformat.protect(ticket, gettlstokenbinding());
    // 追加 cookie 到响应消息中
   options.cookiemanager.appendresponsecookie(
       context,
       options.cookie.name!,
       cookievalue,
       signincontext.cookieoptions);
 ……

----------------------------------------------------------------------------------------

好了,上面的都是周边工作,下面我们来干正事。

授权大体上分为两种模式:

1、基于角色授权。即“你是谁就给你相应的权限”。你是狼人吗?你是预言家吗?你是女巫吗?你是好人吗?是狼人就赋予你杀人的权限。

2、基于策略。老周觉得这个灵活性高一点(纯个人看法)。一个策略需要一定数量的约束条件,是否赋予用户权限就看他能否满足这些约束条件了。约束实现iauthorizationrequirement 接口。这个接口未包含任何成员,因此你可以自由发挥了。

这只不过是按用途来划分的,若从类型本质上看,就是一堆iauthorizationrequirement 组合起来提供给了authorizationhandlercontext,authorizationhandlercontext 再通过一堆iauthorizationhandler 来处理。最后由iauthorizationevaluator 去总结授权的结果。

这里咱们需要的约束条件是用户等级,所以,咱们实现一个levelauthorizationrequirement。

public class levelauthorizationrequirement : iauthorizationrequirement
 {
     public int level { get; private set; }
     public levelauthorizationrequirement(int lv)
     {
         level = lv;
     }
 }

授权处理有两个接口:

1、iauthorizationhandler:处理过程,一个授权请求可以执行多个iauthorizationhandler。一般用于授权过程中的某个阶段(或针对某个约束条件)。一个授权请求可以由多iauthorizationhandler 参与处理。

2、iauthorizationevaluator:综合评估是否决定授权。评估一般在各种iauthorizationhandler 之后进行收尾工作。所以只执行一次就可以了,用于总结整个授权过程的情况得出最终结论(放权还是不放权)。

asp.net core 内置了defaultauthorizationevaluator,这是默认实现,如无特殊需求,我们不会重新实现。

public class defaultauthorizationevaluator : iauthorizationevaluator
{
    public authorizationresult evaluate(authorizationhandlercontext context)
        => context.hassucceeded
            ? authorizationresult.success()
            : authorizationresult.failed(context.hasfailed
                ? authorizationfailure.failed(context.failurereasons)
                : authorizationfailure.failed(context.pendingrequirements));
}

所以,咱们的代码可以选择实现一个抽象类:authorizationhandler,其中,trequirement 需要实现iauthorizationrequirement 接口。这个抽象类已经满足咱们的需求了。

public class levelauthorizationhandler : authorizationhandler
{
    // 策略名称,写成常量方便使用
    public const string policy_name = "level";
    protected override task handlerequirementasync(authorizationhandlercontext context, levelauthorizationrequirement requirement)
    {
        // 查找声明
        claim? clm = context.user.claims.firstordefault(c => c.type == "level");
        if(clm != null)
        {
            // 读出用户等级
            int lv = int.parse(clm.value);
            // 看看用户等级是否满足条件
            if(lv >= requirement.level)
            {
                // 满足,标记此阶段允许授权
                context.succeed(requirement);
            }
        }
        return task.completedtask;
    }
}

在授权请求启动时,authorizationhandlercontext (上下文)对象会把所有iauthorizationrequirement 对象添加到一个哈希表中(hashset),表示一大串正等着授权处理的约束条件。

当我们调用 succeed 方法时,会把已满足要求的iauthorizationrequirement 传递给方法参数。在 success 方法内部会从哈希表中删除此iauthorizationrequirement,以表示该条件已满足了,不必再证。

public virtual void succeed(iauthorizationrequirement requirement)
{
    _succeedcalled = true;
    _pendingrequirements.remove(requirement);
}

记得要在服务容器中注册,否则咱们写的 handler 是不起作用的。

builder.services.addsingleton();

builder.services.addsingleton();
builder.services.addauthorizationbuilder().addpolicy(levelauthorizationhandler.policy_name, pb =>
{
    pb.addauthenticationschemes(cookieauthenticationdefaults.authenticationscheme);
    pb.addrequirements(new levelauthorizationrequirement(3));
});

策略的名称我们前面以常量的方式定义了,记得否?

  public const string policy_name = "level";

addauthenticationschemes 是把此授权策略与一个验证方案关联,当进行鉴权时顺便做一次验证。上述代码我们关联 cookie 验证即可,这个在文章前面已经设置了。addrequirements 方法添加我们自定义的约束条件,这里我设置的用户等级是 3 —— 用户等级要 >= 3 才允许访问。

下面写个 mvc 控制器来检验一下是否能正确授权。

public class homecontroller : controller
{
    [httpget("/")]
    [authorize(policy = levelauthorizationhandler.policy_name)]
    public iactionresult index()
    {
        return view();
    }
}

这里咱们用基于策略的授权方式,所以[authorize]特性要指定策略名称。

好,运行。本来是访问根目录 / 的,但由于验证不通过,自动跳到登录页了。

asp.net core按用户等级授权的方法

注意url上的 backurl 参数:?backurl=/。本来要访问 / 的,所以登录后再跳回 / 。我们选一个用户等级为 5 的登录。

asp.net core按用户等级授权的方法

由于用户等级为 5,是 >=3 的存在,所以授权通过。

asp.net core按用户等级授权的方法

现在,把名为 ck_auth_ent 的cookie删除。

asp.net core按用户等级授权的方法

这个 ck_auth_ent 是在代码中配置的,还记得吗?

builder.services.addauthentication(cookieauthenticationdefaults.authenticationscheme).addcookie(opt =>
{
    opt.loginpath = "/userlog";
    opt.logoutpath = "/logout";
    opt.accessdeniedpath = "/denied";
    opt.cookie.name = "ck_auth_ent";
    opt.returnurlparameter = "backurl";
});

现在咱们找个用户等级低于 3 的登录。

asp.net core按用户等级授权的方法

登录后被拒绝访问。

asp.net core按用户等级授权的方法

到此为止,好像、貌似、似乎已大功告成了。但是,老周又发现问题了:如果我一个控制器内或不同控制器之间有的操作方法要让用户等级 3 以上的用户访问,有些操作方法只要等级在 2 以上的用户就可以访问。这该咋整呢?有大伙伴可以会说了,那就多弄几个策略,每个策略代表一个等级。

builder.services.addauthorizationbuilder()
    .addpolicy("level3", pb =>
    {
        pb.addauthenticationschemes(cookieauthenticationdefaults.authenticationscheme);
        pb.addrequirements(new levelauthorizationrequirement(3));
    })
    .addpolicy("level5", pb =>
    {
        pb.addauthenticationschemes(cookieauthenticationdefaults.authenticationscheme);
        pb.addrequirements(new levelauthorizationrequirement(5));
    });

是的,这样确实是可行的。不过不够动态,要是我弄个策略从 level1 到 level10 呢,岂不要写十个?

官方有个用 age 生成授权策略的示例让老周获得了灵感——是的,咱们就是要动态生成授权策略。需要用到一个接口:iauthorizationpolicyprovider。这个接口可以根据策略名称返回授权策略,所以,咱们可以拿它做文章。

public class levelauthorizationpolicyprovider : iauthorizationpolicyprovider
{
    private readonly authorizationoptions _options;
    public levelauthorizationpolicyprovider(ioptions opt)
    {
        _options = opt.value;
    }
    public task getdefaultpolicyasync()
    {
        return task.fromresult(_options.defaultpolicy);
    }
    public task getfallbackpolicyasync()
    {
        return task.fromresult(_options.fallbackpolicy);
    }
    public task getpolicyasync(string policyname)
    {
        if(policyname.startswith(levelauthorizationhandler.policy_name,stringcomparison.ordinalignorecase))
        {
            // 比如,策略名 level4,得到等级4
            // 提取名称最后的数字
            int prefixlen = levelauthorizationhandler.policy_name.length;
            if(int.tryparse(policyname.substring(prefixlen), out int level))
            {
                // 动态生成策略
                authorizationpolicybuilder plcybd = new authorizationpolicybuilder();
                plcybd.addauthenticationschemes(cookieauthenticationdefaults.authenticationscheme);
                plcybd.addrequirements(new levelauthorizationrequirement(level));
                // build 方法生成策略
                return task.fromresult(plcybd.build())!;
            }
        }
        // 未处理,交由选项类去返回默认的策略
        return task.fromresult(_options.getpolicy(policyname));
    }
}

这样可以根据给定的策略名称,生成与用户等级相关的配置。例如,名称“level3”,就是等级3;“level5”就是等级5。

于是,在配置服务容器时,我们不再需要addauthorizationbuilder 一大段代码了,直接把levelauthorizationpolicyprovider 注册一下就行了。

builder.services.addsingleton();
builder.services.addtransient();
builder.services.addauthentication(cookieauthenticationdefaults.authenticationscheme).addcookie(opt =>
……

然后,在mvc控制器上咱们就可以666地玩了。

public class homecontroller : controller
 {
     [httpget("/")]
     [authorize(policy = $"{levelauthorizationhandler.policy_name}3")]
     public iactionresult index()
     {
         return view();
     }
     [httpget("/music")]
     [authorize(policy = $"{levelauthorizationhandler.policy_name}2")]
     public iactionresult foo()
         => content("2星级用户扰民音乐俱乐部");
     [httpget("/movie")]
     [authorize(policy = $"{levelauthorizationhandler.policy_name}5")]
     public iactionresult movies()
         => content("5星级鬼畜影院");
 }

这样一来,配置不同等级的授权就方便多了。

返回顶部
顶部
网站地图