当前位置: 移动技术网 > IT编程>开发语言>.net > 三分钟学会.NET Core Jwt 策略授权认证

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

2019年12月27日  | 移动技术网IT编程  | 我要评论

无花果的功效与作用,扮霸王龙调戏鳄鱼,景安

一.前言

  大家好我又回来了,前几天讲过一个关于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}" };
        }

三.效果图

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

  看到很多前辈彩的坑,原来的  (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  谢谢大家

如对本文有疑问,请在下面进行留言讨论,广大热心网友会与你互动!! 点击进行留言回复

相关文章:

验证码:
移动技术网