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

【从零开始搭建自己的.NET Core Api框架】(七)授权认证进阶篇

程序员文章站 2022-05-04 14:15:59
系列目录 一. 创建项目并集成swagger 1.1 创建 1.2 完善 二. 搭建项目整体架构 三. 集成轻量级ORM框架——SqlSugar 3.1 搭建环境 3.2 实战篇:利用SqlSugar快速实现CRUD 3.3 生成实体类 四. 集成JWT授权验证 五. 实现CORS跨域 六. 集成泛 ......

系列目录

.  创建项目并集成swagger

  1.1 创建

  1.2 完善

二. 搭建项目整体架构

三. 集成轻量级orm框架——sqlsugar

  3.1 搭建环境

  3.2 实战篇:利用sqlsugar快速实现crud

  3.3 生成实体类

四. 集成jwt授权验证

五. 实现cors跨域

六. 集成泛型仓储

七. 授权认证进阶篇

 


 源码稍后上传github~

该篇是第四篇“实战!带你半小时实现接口的授权认证”的进阶篇。

先说一下之前的版本:

之前在第四篇的时候曾经试着集成过一次jwt授权认证,当时搭的第一版是jwt本身的授权认证机制,但是为了实现“令牌”的滑动过期效果,结果最后改成了使用缓存机制。

所以写到最后发现,其实就是变相的session认证机制,因为发放“令牌”的时候完全可以不用jwt,直接生成一个guid也是可以的。

后来想了一下,这样为了实现令牌滑动过期而破坏了授权认证的独立性,感觉得不偿失。于是就决定”进阶“下,在授权认证模块去掉缓存机制,只使用jwt本身的验证机制。

另外,这次还添加了一些关于对身份验证的优化。之前一个接口只能标明允许一种身份的用户访问,修改后可以实现一个接口同时允许多个身份访问(比如同时允许客户端和后台管理员两种身份的令牌访问)。

 btw,为了完整性考虑,下面有部分内容和之前第四篇相同,有需要的可以跳着看。

  1. 根

根据*定义,jwt(读作 [/dʒɒt/]),即json web tokens,是一种基于json的、用于在网络上声明某种主张的令牌(token)规范。

jwt通常由三部分组成: 头信息(header), 消息体(payload)和签名(signature)。它是一种用于双方之间传递安全信息的表述性声明规范。

jwt作为一个开放的标准(rfc 7519),定义了一种简洁的、自包含的方法,从而使通信双方实现以json对象的形式安全的传递信息。

 

以上是jwt的官方解释,可以看出jwt并不是一种只能权限验证的工具,而是一种标准化的数据传输规范。所以,只要是在系统之间需要传输简短但却需要一定安全等级的数据时,都可以使用jwt规范来传输。规范是不因平台而受限制的,这也是jwt做为授权验证可以跨平台的原因。

如果理解还是有困难的话,我们可以拿jwt和json类比:

json是一种轻量级的数据交换格式,是一种数据层次结构规范。它并不是只用来给接口传递数据的工具,只要有层级结构的数据都可以使用json来存储和表示。当然,json也是跨平台的,不管是win还是linux,.net还是java,都可以使用它作为数据传输形式。

 

该篇的主要目的是实战,所以关于jwt本身的优点,以及使用jwt作为系统授权验证的优缺点,这里就不细说了,感兴趣的可以自己去查阅相关资料。

 

 1.1 在授权验证系统中,jwt是怎么工作的呢?

如果将jwt运用到web api的授权验证中,那么它的工作原理是这样的:

【从零开始搭建自己的.NET Core Api框架】(七)授权认证进阶篇

 

1)客户端向授权服务系统发起请求,申请获取“令牌”。

2)授权服务根据用户身份,生成一张专属“令牌”,并将该“令牌”以jwt规范返回给客户端

3)客户端将获取到的“令牌”放到http请求的headers中后,向主服务系统发起请求。主服务系统收到请求后会从headers中获取“令牌”,并从“令牌”中解析出该用户的身份权限,然后做出相应的处理(同意或拒绝返回资源)

 

可以看出,jwt授权服务是可以脱离我们的主服务系统而作为一个独立系统存在的。

 1.2 令牌是什么?jwt就是令牌吗?

前面说了其实把jwt理解为一种规范更为贴切,但是往往大家把根据jwt规则生成的加密字符串也叫作jwt,还有人直接称呼jwt为令牌。本文为了阐述方便,特此做了一些区分:

 1.2.1 jwt:

本文所说的jwt皆指的是jwt规范

 1.2.2 jwt字符串:

本文所说的“jwt字符串”是指通过jwt规则加密后生成的字符串,它由三部分组成:header(头部)、payload(数据)、signature(签名),将这三部分由‘.’连接而组成的一长串加密字符串就成为jwt字符串。

1)header

由且只由两个数据组成,一个是“alg”(加密规范)指定了该jwt字符串的加密规则,另一个是“typ”(jwt字符串类型)。例如:

{
  "alg": "hs256",
  "typ": "jwt"
}

将这组json格式的数据通过base64url格式编码后,生成的字符串就是我们jwt字符串的第一个部分。

2)payload

由一组数据组成,它负责传递数据,我们可以添加一些已注册声明,比如“iss”(jwt字符串的颁发人名称)、“exp”(该jwt字符串的过期时间)、“sub”(身份)、“aud”(受众),除了这些,我们还可根据需要添加自定义的需要传输的数据,一般是发起请求的用户的信息。例如:

{
  “iss”:"raypi",
  "sub": "client",
  "name": "张三",
  "uid": 1
}

将该json格式的数据通过base64url格式编码后,生成的字符串就是我们jwt字符串的第二部分。

3)signature

数字签名,由4个因素所同时决定:编码后的header字符串,编码后的payload字符串,之前在头部声明的加密算法,我们自定义的一个秘钥字符串(secret)。例如:

hmacsha256(
  base64urlencode(header) + "." +
  base64urlencode(payload),
  secret)

所以签名可以安全地验证一个jwt的合法性(有没有被篡改过)。

最后,给一个实际生成后的jwt字符串的完整样例:

eyjhbgcioijiuzi1niisinr5cci6ikpxvcj9.eyjzdwiioijdbgllbnqilcjqdgkioiiwztrjyzvknc0ymmizltqwyzutotbjmy0wotk0mjfjnwrjmjkilcjpyxqioiiymde4lzcvmyayoje3ojq5iiwizxhwijoxntmwnji3ndy5lcjpc3mioijsyxlqssj9.98paadvhnwvfishqvexkhye2ml6wk_f9ryc-iwyqepu

我们可以拿着这个jwt字符串到试着解析出前两部分的内容。

 1.2.3 令牌:

本文的“令牌”指的是用于http传输headers中用于验证授权的json数据,它是key和value两部分组成,在本文中,key为“authorization”,value为“bearer {jwt字符串}”,其中value除了jwt字符串外,还在前面添加了“bearer ”字符串,这里可以把它理解为大家约约定俗成的规定即可,没有实际的作用。例如:

{ "authorization": "bearer eyjhbgcioijiuzi1niisinr5cci6ikpxvcj9.eyjzdwiioijdbgllbnqilcjqdgkioiiwztrjyzvknc0ymmizltqwyzutotbjmy0wotk0mjfjnwrjmjkilcjpyxqioiiymde4lzcvmyayoje3ojq5iiwizxhwijoxntmwnji3ndy5lcjpc3mioijsyxlqssj9.98paadvhnwvfishqvexkhye2ml6wk_f9ryc-iwyqepu" }

 1.3 授权?认证?傻傻分不清楚

先问一个问题:认证和授权是一回事吗?

答案显然是否定的。因为大家都喜欢把这两个词放在一起说,所以很容易就混淆了他们含义。在.net core中,认证的单词是“authentication”,而授权的单词是“authorization”。

认证,验证身份的意思。即验证当前请求的用户是否为合法用户(放在当前场景下,就是验证用户携带的令牌是否为一个合法令牌);

授权,给用户颁发权限的意思。即给验证通过的用户授予相应的权限(放在当前场景下,就是根据令牌中解析出的用户身份,赋予该http请求,该http请求使用该身份就可以访问对应的接口)

所以,我们下面要实现的总体思路是:

在每个接口上都标明该接口允许什么样的身份访问(比如“client”代表客户端,“admin”代表后台管理员)。

在用户登录成功后,我们将该用户的身份(是client还是admin)等信息生成jwt规范的令牌返回。客户端将返回的令牌存储好(一般是存在cookie中),以后每次调用接口都要将该令牌携带上。

服务端收到请求后,提取令牌,先进行认证,如果不合法(比如被篡改),将驳回请求。如果认证通过,则从令牌中提取身份,进行授权操作,将该身份赋予http请求,放行请求。

请求进到接口后会和接口标明的允许访问身份进行匹配,如果该接口允许该身份访问,则返回相应请求资源,如果不允许,则驳回请求。

 

思路明白了,下面实战起来就不会乱了。

  2. 道

 搭建完的项目架构应该是这样的:

 【从零开始搭建自己的.NET Core Api框架】(七)授权认证进阶篇

这里有三块工作区域:

1)jwthelper是一个jwt帮助类,里面有两个函数,一个函数帮助生成jwt字符串并返回,一个帮助从jwt字符串逆向解析出数据。

2)jwtauthorizationfilter.cs是一个授权中间件

3)startup中添加了认证服务和授权服务

 

2.1 jwthelper

【从零开始搭建自己的.NET Core Api框架】(七)授权认证进阶篇
using microsoft.identitymodel.tokens;
using system;
using system.collections.generic;
using system.identitymodel.tokens.jwt;
using system.security.claims;
using system.text;
//
using raypi.model.configmodel;

namespace raypi.helper
{
    public class jwthelper
    {
        /// <summary>
        /// 颁发jwt字符串
        /// </summary>
        /// <param name="tokenmodel"></param>
        /// <returns></returns>
        public static string issuejwt(tokenmodel tokenmodel)
        {
            var datetime = datetime.utcnow;
            var claims = new claim[]
            {
                new claim(jwtregisteredclaimnames.jti,tokenmodel.uid.tostring()),//用户id
                new claim("role", tokenmodel.role),//身份
                new claim("project", tokenmodel.project),//身份
                new claim(jwtregisteredclaimnames.iat,datetime.tostring(),claimvaluetypes.integer64)
            };
            //秘钥
            var jwtconfig = new jwtauthconfigmodel();
            var key = new symmetricsecuritykey(encoding.utf8.getbytes(jwtconfig.jwtsecretkey));
            var creds = new signingcredentials(key, securityalgorithms.hmacsha256);
            //过期时间
            double exp = 0;
            switch (tokenmodel.tokentype)
            {
                case "web":
                    exp = jwtconfig.webexp;
                    break;
                case "app":
                    exp = jwtconfig.appexp;
                    break;
                case "miniprogram":
                    exp = jwtconfig.miniprogramexp;
                    break;
                case "other":
                    exp = jwtconfig.otherexp;
                    break;
            }
            var jwt = new jwtsecuritytoken(
                issuer: "raypi",
                claims: claims, //声明集合
                expires: datetime.addhours(exp),
                signingcredentials: creds);

            var jwthandler = new jwtsecuritytokenhandler();
            var encodedjwt = jwthandler.writetoken(jwt);

            return encodedjwt;
        }

        /// <summary>
        /// 解析
        /// </summary>
        /// <param name="jwtstr"></param>
        /// <returns></returns>
        public static tokenmodel serializejwt(string jwtstr)
        {
            var jwthandler = new jwtsecuritytokenhandler();
            jwtsecuritytoken jwttoken = jwthandler.readjwttoken(jwtstr);
            object role = new object(); ;
            object project = new object();
            try
            {
                jwttoken.payload.trygetvalue("role", out role);
                jwttoken.payload.trygetvalue("project", out project);
            }
            catch (exception e)
            {
                console.writeline(e);
                throw;
            }
            var tm = new tokenmodel
            {
                uid = long.parse(jwttoken.id),
                role = role.tostring(),
                project = project.tostring()
            };
            return tm;
        }
    }

    /// <summary>
    /// 令牌
    /// </summary>
    public class tokenmodel
    {
        /// <summary>
        /// 用户id
        /// </summary>
        public long uid { get; set; }
        /// <summary>
        /// 身份
        /// </summary>
        public string role { get; set; }
        /// <summary>
        /// 项目名称
        /// </summary>
        public string project { get; set; }
        /// <summary>
        /// 令牌类型
        /// </summary>
        public string tokentype { get; set; }
    }
}
jwthelper

 

其中jwtauthconfigmodel是一个存储配置文件的类,里面读取了配置文件中的jwt秘钥和几个过期时间。这里就不放了,可以在代码里直接写死。

2.2. jwtauthorizationfilter

【从零开始搭建自己的.NET Core Api框架】(七)授权认证进阶篇
using microsoft.aspnetcore.http;
using raypi.bussiness;
using raypi.helper;
using system;
using system.collections.generic;
using system.linq;
using system.security.claims;
using system.threading.tasks;

namespace raypi.authhelp
{
    /// <summary>
    /// 授权中间件
    /// </summary>
    public class jwtauthorizationfilter
    {
        private readonly requestdelegate _next;

        /// <summary>
        /// 
        /// </summary>
        /// <param name="next"></param>
        public jwtauthorizationfilter(requestdelegate next)
        {
            _next = next;
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="httpcontext"></param>
        /// <returns></returns>
        public task invoke(httpcontext httpcontext)
        {
            //检测是否包含'authorization'请求头,如果不包含则直接放行
            if (!httpcontext.request.headers.containskey("authorization"))
            {
                return _next(httpcontext);
            }
            var tokenheader = httpcontext.request.headers["authorization"];
            tokenheader = tokenheader.tostring().substring("bearer ".length).trim();

            tokenmodel tm = jwthelper.serializejwt(tokenheader);

            //basebll.tokenmodel = tm;//将tokenmodel存入basebll

            //授权
            var claimlist = new list<claim>();
            var claim = new claim(claimtypes.role, tm.role);
            claimlist.add(claim);
            var identity = new claimsidentity(claimlist);
            var principal = new claimsprincipal(identity);
            httpcontext.user = principal;

            return _next(httpcontext);
        }
    }
}
jwtauthorizationfilter

 

2.3 startup

【从零开始搭建自己的.NET Core Api框架】(七)授权认证进阶篇

完整代码:

【从零开始搭建自己的.NET Core Api框架】(七)授权认证进阶篇
using system.collections.generic;
using system.io;
using microsoft.aspnetcore.builder;
using microsoft.aspnetcore.hosting;
using microsoft.extensions.configuration;
using microsoft.extensions.dependencyinjection;
using microsoft.extensions.platformabstractions;
using swashbuckle.aspnetcore.swagger;
using raypi.model.configmodel;
using microsoft.aspnetcore.authentication.jwtbearer;
using microsoft.identitymodel.tokens;
using system.text;
using raypi.authhelp;

namespace raypi
{
    /// <summary>
    /// 
    /// </summary>
    public class startup
    {
        /// <summary>
        /// 
        /// </summary>
        /// <param name="env"></param>
        public startup(ihostingenvironment env)
        {
            var builder = new configurationbuilder()
                .setbasepath(env.contentrootpath)
                .addjsonfile("appsettings.json", optional: true, reloadonchange: true);

            this.configuration = builder.build();

            baseconfigmodel.setbaseconfig(configuration);
        }
        /// <summary>
        /// 
        /// </summary>
        public iconfiguration configuration { get; }

        /// <summary>
        /// this method gets called by the runtime. use this method to add services to the container.
        /// </summary>
        /// <param name="services"></param>
        public void configureservices(iservicecollection services)
        {
            services.addmvc().addjsonoptions(options =>
            {
                options.serializersettings.dateformatstring = "yyyy-mm-dd hh:mm:ss";//设置时间格式
            });

            #region swagger
            services.addswaggergen(c =>
            {
                c.swaggerdoc("v1", new info
                {
                    version = "v1.1.0",
                    title = "ray webapi",
                    description = "框架集合",
                    termsofservice = "none",
                    contact = new swashbuckle.aspnetcore.swagger.contact { name = "raywang", email = "2271272653@qq.com", url = "http://www.cnblogs.com/raywang" }
                });
                //添加注释服务
                var basepath = platformservices.default.application.applicationbasepath;
                var apixmlpath = path.combine(basepath, "apihelp.xml");
                var entityxmlpath = path.combine(basepath, "entityhelp.xml"); 
                c.includexmlcomments(apixmlpath, true);//控制器层注释(true表示显示控制器注释)
                c.includexmlcomments(entityxmlpath);

                //添加控制器注释
                //c.documentfilter<swaggerdoctag>();

                //添加header验证信息
                //c.operationfilter<swaggerheader>();
                var security = new dictionary<string, ienumerable<string>> { { "bearer", new string[] { } }, };
                c.addsecurityrequirement(security);//添加一个必须的全局安全信息,和addsecuritydefinition方法指定的方案名称要一致,这里是bearer。
                c.addsecuritydefinition("bearer", new apikeyscheme
                {
                    description = "jwt授权(数据将在请求头中进行传输) 参数结构: \"authorization: bearer {token}\"",
                    name = "authorization",//jwt默认的参数名称
                    in = "header",//jwt默认存放authorization信息的位置(请求头中)
                    type = "apikey"
                });
            });
            #endregion

            #region 认证
            services.addauthentication(x =>
                {
                    x.defaultauthenticatescheme = jwtbearerdefaults.authenticationscheme;
                    x.defaultchallengescheme = jwtbearerdefaults.authenticationscheme;
                })
                .addjwtbearer(o =>
                {
                    jwtauthconfigmodel jwtconfig=new jwtauthconfigmodel();
                    o.tokenvalidationparameters = new tokenvalidationparameters
                    {
                        validissuer = "raypi",
                        validaudience = "wr",
                        issuersigningkey = new symmetricsecuritykey(encoding.ascii.getbytes(jwtconfig.jwtsecretkey)),

                        /***********************************tokenvalidationparameters的参数默认值***********************************/
                        requiresignedtokens = true,
                        // savesignintoken = false,
                        // validateactor = false,
                        // 将下面两个参数设置为false,可以不验证issuer和audience,但是不建议这样做。
                        validateaudience = false,
                        validateissuer = true,
                        validateissuersigningkey = true,
                        // 是否要求token的claims中必须包含 expires
                        requireexpirationtime = true,
                        // 允许的服务器时间偏移量
                        // clockskew = timespan.fromseconds(300),
                        // 是否验证token有效期,使用当前时间与token的claims中的notbefore和expires对比
                        validatelifetime = true
                    };
                });
            #endregion

            #region 授权
            services.addauthorization(options =>
            {
                options.addpolicy("requireclient", policy => policy.requirerole("client").build());
                options.addpolicy("requireadmin", policy => policy.requirerole("admin").build());
                options.addpolicy("requireadminorclient", policy => policy.requirerole("admin,client").build());
            });
            #endregion

            #region cors
            services.addcors(c =>
            {
                c.addpolicy("any", policy =>
                 {
                     policy.allowanyorigin()
                     .allowanymethod()
                     .allowanyheader()
                     .allowcredentials();
                 });

                c.addpolicy("limit", policy =>
                 {
                     policy
                     .withorigins("localhost:8083")
                     .withmethods("get", "post", "put", "delete")
                     //.withheaders("authorization");
                     .allowanyheader();
                 });
            });
            #endregion
        }

        /// <summary>
        /// this method gets called by the runtime. use this method to configure the http request pipeline.
        /// </summary>
        /// <param name="app"></param>
        /// <param name="env"></param>
        public void configure(iapplicationbuilder app, ihostingenvironment env)
        {
            if (env.isdevelopment())
            {
                app.usedeveloperexceptionpage();
            }

            #region swagger
            app.useswagger();
            app.useswaggerui(c =>
            {
                c.swaggerendpoint("/swagger/v1/swagger.json", "apihelp v1");
            });
            #endregion

            //认证
            app.useauthentication();

            //授权
            app.usemiddleware<jwtauthorizationfilter>();

            app.usemvc();

            app.usestaticfiles();//用于访问wwwroot下的文件 
        }
    }
}
startup 

tips:

这里有一个坑,不太了解依赖注入和中间件的人很容易踩到(其实就是我自己了)

在startup.cs的configure函数中,里面每个app.usexxxxx();是有一定顺序。可以理解为,这里添加中间件的顺序就是客户端发起http请求时所经过的顺序。

之前我因为把“app.usemvc();”写到了认证授权上面去了,结果导致怎么debug都找不到问题。。。

所以一定要先app.useauthentication()认证,然后app.usemiddleware<jwtauthorizationfilter>()授权,最后再app.usemvc()

 

  3.果

 搭建完成之后,下面就是测试了。

选择一个测试控制器,在其头上标注[authorize]属性

【从零开始搭建自己的.NET Core Api框架】(七)授权认证进阶篇

(policy和roles两种写法是一个意思,但是必须要是startup中已经申明的,否则swagger会直接报错)

 

 f5运行,在swagger ui中调用一个需要授权验证的接口(根据id获取学生信息)

【从零开始搭建自己的.NET Core Api框架】(七)授权认证进阶篇

 

输入1,先不进行任何授权认证的操作,直接点击excute尝试调用,返回结果如下:

【从零开始搭建自己的.NET Core Api框架】(七)授权认证进阶篇

 

状态码500,还返回了一大段html代码,我们可以将接口的完整地址输入到浏览器地址栏进行访问,就可以看到这段html代码的页面了:

【从零开始搭建自己的.NET Core Api框架】(七)授权认证进阶篇

 

可以看到接口返回了一个错误页,原因就是中间件在http请求的头部(headers)中没有找到“authorization"字段里的”令牌“。

 

现在,我们先调用获取jwt接口(实际项目中不应该有该接口,分发令牌的功能应该集成到登陆功能中,但是这里为了简单直观,我将分发令牌的功能直接写成了接口,以供测试),输入相应的客户端信息,excute:

【从零开始搭建自己的.NET Core Api框架】(七)授权认证进阶篇

 

 

接口会生成”令牌“,返回jwt字符串:

【从零开始搭建自己的.NET Core Api框架】(七)授权认证进阶篇

 

我们要复制这串jwt字符串,然后将其添加到http请求的headers中去。测试方法有两个:

 

1)可以新建一个html页面,模拟前端写个ajax调用接口,在ajax添加headers字段,如下:

$.ajax({
                url: "http://localhost:3608/api/admin/student/1",
                type: ”get“,
                datatype: "json",
                //data: {},
                async: false,
          //手动高亮 headers: { "authorization": "bearer eyjhbgcioijiuzi1niisinr5cci6ikpxvcj9.eyjzdwiioijbzg1pbiisimp0asi6ijhjmdewmzi2lte4m2mtngq5zc1imdfjlwfjm2ezntizodyxocisimlhdci6ijiwmtgvny8yide1ojazojq4iiwizxhwijoxntmwntg3mdi4lcjpc3mioijsyxlqssj9.1bb7hwodd12n8ymcqsu79xm-gdq14gerhs9b-1l1kmg" }, success: function (d) { alert(json.stringify(d)); }, error: function (d) { alert(json.stringify(d)) } });

 

2)如果你的swagger像我一样,集成了添加authrize头部功能,那么可以点击这个按钮进行添加(如果你的swagger看不到这个按钮,可以参考我之前的章节【从零开始搭建自己的.net core api框架】(一)创建项目并集成swagger:1.2 完善,对swagger进行相关的设置)

【从零开始搭建自己的.NET Core Api框架】(七)授权认证进阶篇

【从零开始搭建自己的.NET Core Api框架】(七)授权认证进阶篇

 

这里除了jwt字符串外,前面还需要手动写入“bearer ”(有一个空格)字符串。点击authorize保存"令牌"。

 

再次调用刚才的”根据id获取学生信息“接口,发现获取成功:

【从零开始搭建自己的.NET Core Api框架】(七)授权认证进阶篇

可以看到swagger向http请求的headers中添加了我们刚才保存的”令牌“。

 

参考内容:

https://jwt.io/

https://www.cnblogs.com/webenh/p/9039322.html