欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

三分钟学会.NET Core Jwt 策略授权认证

程序员文章站 2023-11-14 09:46:40
大家好我又回来了,前几天讲过一个关于Jwt的身份验证最简单的案例,但是功能还是不够强大,不适用于真正的项目,是的,在真正面对复杂而又苛刻的客户中,我们会不知所措,就现在需要将认证授权这一块也变的复杂而又实用起来,那在专业术语中就叫做自定义策略的API认证,本次案例运行在.NET Core 3.0中,... ......

一.前言

  大家好我又回来了,前几天讲过一个关于jwt的身份验证最简单的案例,但是功能还是不够强大,不适用于真正的项目,是的,在真正面对复杂而又苛刻的客户中,我们会不知所措,就现在需要将认证授权这一块也变的复杂而又实用起来,那在专业术语中就叫做自定义策略的api认证,本次案例运行在.net core 3.0中,最后我们将在swagger中进行浏览,来尝试项目是否正常,对于.net core 2.x 版本,这篇文章有些代码不适用,但我会在文中说明。

二.在.net core中尝试

  我们都知道jwt是为了认证,微软给我们提供了进城打鬼子的城门,那就是 authorizationhandle

  我们首先要实现它,并且我们还可以根据依赖注入的 authorizationhandlercontext 来获取上下文,就这样我们就更可以做一些权限的手脚

public class policyhandler : authorizationhandler<policyrequirement>
    {
        protected override task handlerequirementasync(authorizationhandlercontext context, policyrequirement requirement)
        {
            var http = (context.resource as microsoft.aspnetcore.routing.routeendpoint);
            var questurl = "/"+http.routepattern.rawtext; 
            //赋值用户权限
            var userpermissions = requirement.userpermissions;
            //是否经过验证
            var isauthenticated = context.user.identity.isauthenticated;
            if (isauthenticated)
            {
                if (userpermissions.any(u=>u.url == questurl))
                {
                    //用户名
                    var username = context.user.claims.singleordefault(s => s.type == claimtypes.nameidentifier).value;
                    if (userpermissions.any(w => w.username == username))
                    {
                        context.succeed(requirement);
                    }
                }
            }
            return task.completedtask;
        }
    }

  首先,我们重写了 handlerequirementasync 方法,如果你看过aspnetcore的源码你一定知道,它是jwt身份认证的开端,也就是说你重写了它,原来那一套就不会走了,我们观察一下源码,我贴在下面,可以看到这就是一个最基本的授权,通过 context.succeed(requirement 完成了最后的认证动作!

public class denyanonymousauthorizationrequirement : authorizationhandler<denyanonymousauthorizationrequirement>, iauthorizationrequirement
    {
        /// <summary>
        /// makes a decision if authorization is allowed based on a specific requirement.
        /// </summary>
        /// <param name="context">the authorization context.</param>
        /// <param name="requirement">the requirement to evaluate.</param>
        protected override task handlerequirementasync(authorizationhandlercontext context, denyanonymousauthorizationrequirement requirement)
        {
            var user = context.user;
            var userisanonymous =
                user?.identity == null ||
                !user.identities.any(i => i.isauthenticated);
            if (!userisanonymous)
            {
                context.succeed(requirement);
            }
            return task.completedtask;
        }
    }

那么  succeed  是一个什么呢?它是一个在  authorizationhandlercontext的定义动作,包括fail() ,也是如此,当然具体实现我们不在细谈,其内部还是挺复杂的,不过我们需要的是  denyanonymousauthorizationrequirement  被当作了抽象的一部分。

public abstract class authorizationhandler<trequirement> : iauthorizationhandler
            where trequirement : iauthorizationrequirement
    {}

好吧,言归正传(看源码挺刺激的),我们刚刚在  policyhandler实现了自定义认证策略,上面还说到了两个方法。现在我们在项目中配置并启动它,并且我在代码中也是用了swagger用于后面的演示。

在  addjwtbearer中我们添加了jwt验证包括了验证参数以及几个事件处理,这个很基本,不在解释。不过在swagger中添加jwt的一些功能是在  addsecuritydefinition  中写入的。

public void configureservices(iservicecollection services)
        {
            //添加策略鉴权模式
            services.addauthorization(options =>
            {
                options.addpolicy("permission", policy => policy.requirements.add(new policyrequirement()));
            })
            .addauthentication(s =>
            {
                //添加jwt scheme
                s.defaultauthenticatescheme = jwtbearerdefaults.authenticationscheme;
                s.defaultscheme = jwtbearerdefaults.authenticationscheme;
                s.defaultchallengescheme = jwtbearerdefaults.authenticationscheme;
            })
            //添加jwt验证:
            .addjwtbearer(options =>
            {
                options.tokenvalidationparameters = new tokenvalidationparameters
                {
                    validatelifetime = true,//是否验证失效时间
                    clockskew = timespan.fromseconds(30),

                    validateaudience = true,//是否验证audience
                    //validaudience = const.getvalidudience(),//audience
                    //这里采用动态验证的方式,在重新登陆时,刷新token,旧token就强制失效了
                    audiencevalidator = (m, n, z) =>
                    {
                        return m != null && m.firstordefault().equals(const.validaudience);
                    },
                    validateissuer = true,//是否验证issuer
                    validissuer = const.domain,//issuer,这两项和前面签发jwt的设置一致

                    validateissuersigningkey = true,//是否验证securitykey
                    issuersigningkey = new symmetricsecuritykey(encoding.utf8.getbytes(const.securitykey))//拿到securitykey
                };
                options.events = new jwtbearerevents
                {
                    onauthenticationfailed = context =>
                    {
                        //token expired
                        if (context.exception.gettype() == typeof(securitytokenexpiredexception))
                        {
                            context.response.headers.add("token-expired", "true");
                        }
                        return task.completedtask;
                    }
                };
            }); 
            services.addswaggergen(c =>
            {
                c.swaggerdoc("v1", new openapiinfo
                {
                    version = "v1",
                    title = "haozi jwt",
                    description = "基于.net core 3.0 的jwt 身份验证",
                    contact = new openapicontact
                    {
                        name = "zaranet",
                        email = "zaranet@163.com",
                        url = new uri("http://cnblogs.com/zaranet"),
                    },
                });
                c.addsecuritydefinition("bearer", new openapisecurityscheme()
                {
                    description = "在下框中输入请求头中需要添加jwt授权token:bearer token",
                    name = "authorization",
                    in = parameterlocation.header,
                    type = securityschemetype.apikey,
                    bearerformat = "jwt",
                    scheme = "bearer"
                });
                c.addsecurityrequirement(new openapisecurityrequirement
                {
                    {
                        new openapisecurityscheme
                        {
                            reference = new openapireference {
                                type = referencetype.securityscheme,
                                id = "bearer"
                            }
                        },
                        new string[] { }
                    }
                });
            });
            //认证服务
            services.addsingleton<iauthorizationhandler, policyhandler>();
            services.addcontrollers();
        }

在以上代码中,我们通过鉴权模式添加了认证规则,一个名叫  policyrequirement  的类,它实现了  iauthorizationrequirement  接口,其中我们需要定义一些规则,通过构造函数我们可以添加我们要识别的权限规则。那个username就是 attribute 。

public class policyrequirement : iauthorizationrequirement
    {/// <summary>
     /// user rights collection
     /// </summary>
        public list<userpermission> userpermissions { get; private set; }
        /// <summary>
        /// no permission action
        /// </summary>
        public string deniedaction { get; set; }
        /// <summary>
        /// structure
        /// </summary>
        public policyrequirement()
        {
            //jump to this route without permission
            deniedaction = new pathstring("/api/nopermission");
            //route configuration that users have access to, of course you can read it from the database, you can also put it in redis for persistence
            userpermissions = new list<userpermission> {
                              new userpermission {  url="/api/value3", username="admin"},
                          };
        }
    }
    public class userpermission
    {
        public string username { get; set; }
        public string url { get; set; }
    }

随后我们应当启动我们的服务,在.net core 3.0 中身份验证的中间件位置需要在路由和端点配置的中间。

public void configure(iapplicationbuilder app, iwebhostenvironment env)
        {
            if (env.isdevelopment())
            {
                app.usedeveloperexceptionpage();
            }
            app.useswagger();
            app.useswaggerui(c =>
            {
                c.swaggerendpoint("/swagger/v1/swagger.json", "my api v1");
            });
            app.userouting();
            app.useauthentication();
            app.useauthorization();
            app.useendpoints(endpoints =>
            {
                endpoints.mapcontrollers();
            });
        }

  我们通常会有一个获取token的api,用于让jwt通过  jwtsecuritytokenhandler().writetoken(token)  用于生成我们的token,虽然jwt是没有状态的,但你应该也明白,如果你的jwt生成了随后你重启了你的网站,你的jwt会失效,这个是因为你的密钥进行了改变,如果你的密钥一直写死,那么这个jwt将不会再过期,这个还是有安全风险的,这个我不在这里解释,gettoken定义如下:

  [apicontroller]
    public class authcontroller : controllerbase
    {
        [allowanonymous]
        [httpget]
        [route("api/nopermission")]
        public iactionresult nopermission()
        {
            return forbid("no permission!");
        }
        /// <summary>
        /// login
        /// </summary>
        [allowanonymous]
        [httpget]
        [route("api/auth")]
        public iactionresult get(string username, string pwd)
        {
            if (checkaccount(username, pwd, out string role))
            {
                const.validaudience = username + pwd + datetime.now.tostring();
                // push the user’s name into a claim, so we can identify the user later on.
                //这里可以随意加入自定义的参数,key可以自己随便起
                var claims = new[]
                {
                    new claim(jwtregisteredclaimnames.nbf,$"{new datetimeoffset(datetime.now).tounixtimeseconds()}") ,
                    new claim (jwtregisteredclaimnames.exp,$"{new datetimeoffset(datetime.now.addminutes(30)).tounixtimeseconds()}"),
                    new claim(claimtypes.nameidentifier, username),
                    new claim("role", role)
                };
                //sign the token using a secret key.this secret will be shared between your api and anything that needs to check that the token is legit.
                var key = new symmetricsecuritykey(encoding.utf8.getbytes(const.securitykey));
                var creds = new signingcredentials(key, securityalgorithms.hmacsha256);
                //.net core’s jwtsecuritytoken class takes on the heavy lifting and actually creates the token.
                var token = new jwtsecuritytoken(
                    issuer: const.domain, //颁发者
                    audience: const.validaudience,//过期时间
                    expires: datetime.now.addminutes(30),// 签名证书
                    signingcredentials: creds, //自定义参数
                    claims: claims );
                return ok(new
                {
                    token = new jwtsecuritytokenhandler().writetoken(token)
                });
            }
            else
            {
                return badrequest(new { message = "username or password is incorrect." });
            }
        }
        /// <summary>
        /// 模拟登陆校验
        /// </summary>
        private bool checkaccount(string username, string pwd, out string role)
        {
            role = "user";
            if (string.isnullorempty(username))
                return false;
            if (username.equals("admin"))
                role = "admin";
            return true;
        }

  可能比较特别的是  allowanonymous  ,这个看我文章的同学可能头一次见,其实怎么说好呢,这个可无可有,没有硬性的要求,我看到好几个知名博主加上了,我也加上了~...最后我们创建了几个资源控制器,它们是受保护的。

  在你添加策略权限的时候例如政策名称是xxx,那么在对应的api表头就应该是xxx,随后到了  policyhandler我们解析了 claims 处理了它是否有权限。

// get api/values1
        [httpget]
        [route("api/value1")]
        public actionresult<ienumerable<string>> get()
        {
            return new string[] { "value1", "value1" };
        }
        // get api/values2
        /**
         * 该接口用authorize特性做了权限校验,如果没有通过权限校验,则http返回状态码为401
         */
        [httpget]
        [route("api/value2")]
        [authorize]
        public actionresult<ienumerable<string>> get2()
        {
            var auth = httpcontext.authenticateasync().result.principal.claims;
            var username = auth.firstordefault(t => t.type.equals(claimtypes.nameidentifier))?.value;
            return new string[] { "这个接口登陆过的都能访问", $"username={username}" };
        }
        /**
         * 这个接口必须用admin
         **/
        [httpget]
        [route("api/value3")]
        [authorize("permission")]
        public actionresult<ienumerable<string>> get3()
        {
            //这是获取自定义参数的方法
            var auth = httpcontext.authenticateasync().result.principal.claims;
            var username = auth.firstordefault(t => t.type.equals(claimtypes.nameidentifier))?.value;
            var role = auth.firstordefault(t => t.type.equals("role"))?.value;
            return new string[] { "这个接口有管理员权限才可以访问", $"username={username}", $"role={role}" };
        }

三.效果图

三分钟学会.NET Core Jwt 策略授权认证

四.栗子源代码和以往版本

  看到很多前辈彩的坑,原来的  (context.resource as microsoft.aspnetcore.routing.routeendpoint);  实际上在.net core 3.0 已经不能用了,原因是.net core 3.0 启用 endpointrouting 后,权限filter不再添加到 actiondescriptor ,而将权限直接作为中间件运行,同时所有filter都会添加到  endpoint.metadata  ,如果在.net core 2.1 & 2.2 版本中你通常handler可以这么写:

public class policyhandler : authorizationhandler<policyrequirement>
    {
        protected override task handlerequirementasync(authorizationhandlercontext context, policyrequirement requirement)
        {
            //赋值用户权限
            var userpermissions = requirement.userpermissions;
            //从authorizationhandlercontext转成httpcontext,以便取出表求信息
            var httpcontext = (context.resource as microsoft.aspnetcore.mvc.filters.authorizationfiltercontext).httpcontext;
            //请求url
            var questurl = httpcontext.request.path.value.toupperinvariant();
            //是否经过验证
            var isauthenticated = httpcontext.user.identity.isauthenticated;
            if (isauthenticated)
            {
                if (userpermissions.groupby(g => g.url).any(w => w.key.toupperinvariant() == questurl))
                {
                    //用户名
                    var username = httpcontext.user.claims.singleordefault(s => s.type == claimtypes.nameidentifier).value;
                    if (userpermissions.any(w => w.username == username && w.url.toupperinvariant() == questurl))
                    {
                        context.succeed(requirement);
                    }
                    else
                    {
                        //无权限跳转到拒绝页面
                        httpcontext.response.redirect(requirement.deniedaction);
                    }
                }
                else
                    context.succeed(requirement);
            }
            return task.completedtask;
        }
    }

  该案例源代码在我的github上:https://github.com/zaranetcore/aspnetcore_jsonwebtoken/tree/master/jwt_policy_demo  谢谢大家