快速理解ASP.NET Core的认证与授权

ASP.NET Core的认证与授权已经不是什么新鲜事了,微软官方的文档对于如何在ASP.NET Core中实现认证与授权有着非常详细深入的介绍。但有时候在开发过程中,我们也往往会感觉无从下手,或者由于一开始没有进行认证授权机制的设计与规划,使得后期出现一些混乱的情况。这里我就尝试结合一个实际的例子,从0到1来介绍ASP.NET Core中如何实现自己的认证与授权机制。

当我们使用Visual Studio自带的ASP.NET Core Web API项目模板新建一个项目的时候,Visual Studio会问我们是否需要启用认证机制,如果你选择了启用,那么Visual Studio会在项目创建的时候,加入一些辅助依赖和一些辅助类,比如加入对Entity Framework以及ASP.NET Identity的依赖,以帮助你实现基于Entity Framework和ASP.NET Identity的身份认证。如果你还没有了解过ASP.NET Core的认证与授权的一些基础内容,那么当你打开这个由Visual Studio自动创建的项目的时候,肯定会一头雾水,不知从何开始,你甚至会怀疑自动创建的项目中,真的是所有的类或者方法都是必须的吗?所以,为了让本文更加简单易懂,我们还是选择不启用身份认证,直接创建一个最简单的ASP.NET Core Web API应用程序,以便后续的介绍。

新建一个ASP.NET Core Web API应用程序,这里我是在Linux下使用JetBrains Rider新建的项目,也可以使用标准的Visual Studio或者VSCode来创建项目。创建完成后,运行程序,然后使用浏览器访问/WeatherForecast端点,就可以获得一组随机生成的天气及温度数据的数组。你也可以使用下面的curl命令来访问这个API:

1

curl -X GET "http://localhost:5000/WeatherForecast" -H  "accept: text/plain"

现在让我们在WeatherForecastController的Get方法上设置一个断点,重新启动程序,仍然发送上述请求以命中断点,此时我们比较关心User对象的状态,打开监视器查看User对象的属性,发现它的IsAuthenticated属性为false:

4f7ed2f1072fee9369a7f7a53f2eb0c5.png

在很多情况下,我们可能并不需要在Controller的方法中获取认证用户的信息,因此也从来不会关注User对象是否真的处于已被认证的状态。但是当API需要根据用户的某些信息来执行一些特殊逻辑时,我们就需要在这里让User的认证信息处于一种合理的状态:它是已被认证的,并且包含API所需的信息。这就是本文所要讨论的ASP.NET Core的认证与授权。

认证

应用程序对于使用者的身份认定包含两部分:认证授权。认证是指当前用户是否是系统的合法用户,而授权则是指定合法用户对于哪些系统资源具有怎样的访问权限。我们先来看如何实现认证。

在此,我们单说由ASP.NET Core应用程序本身实现的认证,不讨论具有统一Identity Provider完成身份认证的情况(比如单点登录),这样的话就能够更加清晰地了解ASP.NET Core本身的认证机制。接下来,我们尝试在ASP.NET Core应用程序上,实现Basic认证。

Basic认证需要将用户的认证信息附属在HTTP请求的Authorization的头(Header)上,认证信息是一串由用户名和密码通过BASE64编码后所产生的字符串,例如,当你采用Basic认证,并使用daxnet和password作为访问WeatherForecast API的用户名和密码时,你可能需要使用下面的命令行来调用WeatherForecast:

1

curl -X GET "http://localhost:5000/WeatherForecast" -H  "accept: text/plain" -H "Authorization: Basic ZGF4bmV0OnBhc3N3b3Jk"

在ASP.NET Core Web API中,当应用程序接收到上述请求后,就会从Request的Header里读取Authorization的信息,然后BASE64解码得到用户名和密码,然后访问数据库来确认所提供的用户名和密码是否合法,以判断认证是否成功。这部分工作通常可以采用ASP.NET Core Identity框架来实现,不过在这里,为了能够更加清晰地了解认证的整个过程,我们选择自己动手来实现。

首先,我们定义一个User对象,并且预先设计好几个用户,以便模拟存储用户信息的数据库,这个User对象的代码如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

public class User

{

    public string UserName { get; set; }

    public string Password { get; set; }

    public IEnumerable<string> Roles { get; set; }

    public int Age { get; set; }

    public override string ToString() => UserName;

    public static readonly User[] AllUsers = {

        new User

        {

            UserName = "daxnet", Password = "password", Age = 16, Roles = new[] { "admin", "super_admin" }

        },

        new User

        {

            UserName = "admin", Password = "admin", Age = 29, Roles = new[] { "admin" }

        }

    };

}

该User对象包括用户名、密码以及它的角色名称,不过暂时我们不需要关心角色信息。User对象还包含一个静态字段,我们将它作为用户信息数据库来使用。

接下来,在应用程序中添加一个AuthenticationHandler,用来获取Request Header中的用户信息,并对用户信息进行验证,代码如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

public class BasicAuthenticationHandler : AuthenticationHandler<BasicAuthenticationSchemeOptions>

{

    public BasicAuthenticationHandler(

        IOptionsMonitor<BasicAuthenticationSchemeOptions> options,

        ILoggerFactory logger,

        UrlEncoder encoder,

        ISystemClock clock) : base(options, logger, encoder, clock)

    {

    }

    protected override Task<AuthenticateResult> HandleAuthenticateAsync()

    {

        if (!Request.Headers.ContainsKey("Authorization"))

        {

            return Task.FromResult(AuthenticateResult.Fail("Authorization header is not specified."));

        }

        var authHeader = Request.Headers["Authorization"].ToString();

        if (!authHeader.StartsWith("Basic "))

        {

            return Task.FromResult(

                AuthenticateResult.Fail("Authorization header value is not in a correct format"));

        }

        var base64EncodedValue = authHeader["Basic ".Length..];

        var userNamePassword = Encoding.UTF8.GetString(Convert.FromBase64String(base64EncodedValue));

        var userName = userNamePassword.Split(':')[0];

        var password = userNamePassword.Split(':')[1];

        var user = User.AllUsers.FirstOrDefault(u => u.UserName == userName && u.Password == password);

        if (user == null)

        {

            return Task.FromResult(AuthenticateResult.Fail("Invalid username or password."));

        }

        var claims = new[]

        {

            new Claim(ClaimTypes.NameIdentifier, user.UserName),

            new Claim(ClaimTypes.Role, string.Join(',', user.Roles)),

            new Claim(ClaimTypes.UserData, user.Age.ToString())

        };

        var claimsPrincipal =

            new ClaimsPrincipal(new ClaimsIdentity(

                claims,

                "Basic",

                ClaimTypes.NameIdentifier, ClaimTypes.Role));

        var ticket = new AuthenticationTicket(claimsPrincipal, new AuthenticationProperties

        {

            IsPersistent = false

        }, "Basic");

        return Task.FromResult(AuthenticateResult.Success(ticket));

    }

}

在上面的HandleAuthenticateAsync代码中,首先对Request Header进行合法性校验,比如是否包含Authorization的Header,以及Authorization Header的值是否合法,然后,将Authorization Header的值解析出来,通过Base64解码后得到用户名和密码,与用户信息数据库里的记录进行匹配,找到匹配的用户。接下来,基于找到的用户对象,创建ClaimsPrincipal,并基于ClaimsPrincipal创建AuthenticationTicket然后返回。

这段代码中有几点值得关注:

  1. BasicAuthenticationSchemeOptions本身只是一个继承于AuthenticationSchemeOptions的POCO类。AuthenticationSchemeOptions类通常是为了向AuthenticationHandler提供一些输入参数。比如,在某个自定义的用户认证逻辑中,可能需要通过环境变量读入字符串解密的密钥信息,此时就可以在这个自定义的AuthenticationSchemeOptions中增加一个Passphrase的属性,然后在Startup.cs中,通过service.AddScheme调用将从环境变量中读取的Passphrase的值传入

  2. 除了将用户名作为Identity Claim加入到ClaimsPrincipal中之外,我们还将用户的角色(Role)用逗号串联起来,作为Role Claim添加到ClaimsPrincipal中,目前我们暂时不需要涉及角色相关的内容,但是先将这部分代码放在这里以备后用。另外,我们将用户的年龄(Age)放在UserData claim中,在实际中应该是在用户对象上有该用户的出生日期,这样比较合理,然后这个出生日期应该放在DateOfBirth claim中,这里为了简单起见,就先放在UserData中了

  3. ClaimsPrincipal的构造函数中,可以指定哪个Claim类型可被用作用户名称,而哪个Claim类型又可被用作用户的角色。例如上面代码中,我们选择NameIdentifier类型作为用户名,而Role类型作为用户角色,于是,在接下来的Controller代码中,由NameIdentifier这种Claim所指向的字符串值,就会被看成用户名而被绑定到Identity.Name属性上

回过头来看看BasicAuthenticationSchemeOptions类,它的实现非常简单:

1

2

3

4

public class BasicAuthenticationSchemeOptions : AuthenticationSchemeOptions

{

}

接下来,在Startup.cs文件里,修改ConfigureServices和Configure方法,加入Authentication的支持:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

public void ConfigureServices(IServiceCollection services)

{

    services.AddControllers();

    services.AddSwaggerGen(c =>

    {

        c.SwaggerDoc("v1", new OpenApiInfo { Title = "WebAPIAuthSample", Version = "v1" });

    });

    services.AddAuthentication("Basic")

        .AddScheme<BasicAuthenticationSchemeOptions, BasicAuthenticationHandler>(

            "Basic", options => { });

}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)

{

    if (env.IsDevelopment())

    {

        app.UseDeveloperExceptionPage();

        app.UseSwagger();

        app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "WebAPIAuthSample v1"));

    }

    app.UseHttpsRedirection();

    app.UseRouting();

    app.UseAuthentication();

    app.UseEndpoints(endpoints => { endpoints.MapControllers(); });

}

现在,运行应用程序,在WeatherForecastController的Get方法上设置断点,然后执行上面的curl命令,当断点被命中时,观察this.User对象可以发现,IsAuthenticated属性变为了true,Name属性也被设置为用户名:

353b0c784137a313045b74a9930c311f.png

大多数身份认证框架会提供一些辅助方法来帮助开发人员将AuthenticationHandler注册到应用程序中,例如,基于JWT持有者身份认证的框架会提供一个AddJwtBearer的方法,将JWT身份认证机制加入到应用程序中,它本质上也是调用AddScheme方法来完成AuthenticationHandler的注册。在这里,我们也可以自定义一个AddBasicAuthentication的扩展方法:

1

2

3

4

5

6

7

public static class Extensions

{

    public static AuthenticationBuilder AddBasicAuthentication(this AuthenticationBuilder builder)

        => builder.AddScheme<BasicAuthenticationSchemeOptions, BasicAuthenticationHandler>(

            "Basic",

            options => { });

}

然后修改Starup.cs文件,将ConfigureServices方法改为下面这个样子:

1

2

3

4

5

6

7

8

9

public void ConfigureServices(IServiceCollection services)

{

    services.AddControllers();

    services.AddSwaggerGen(c =>

    {

        c.SwaggerDoc("v1", new OpenApiInfo { Title = "WebAPIAuthSample", Version = "v1" });

    });

    services.AddAuthentication("Basic").AddBasicAuthentication();

}

这样做的好处是,你可以为开发人员提供更多比较有针对性的配置认证机制的编程接口,这对于一个认证模块/框架的开发是一个很好的设计。

在curl命令中,如果我们没有指定Authorization Header,或者Authorization Header的值不正确,那么WeatherForecast API仍然可以被调用,只不过IsAuthenticated属性为false,也无法从this.User对象得到用户信息。其实,阻止未认证用户访问API并不是认证的事情,API被未认证(或者说未登录)用户访问也是合理的事情,因此,要实现对于未认证用户的访问限制,就需要进一步实现ASP.NET Core Web API的另一个安全控制组件:授权

授权

认证相比,授权的逻辑会比较复杂:认证更多是技术层面的事情,而授权则更多地与业务相关。市面上常见的认证机制顶多也就是那么几种或者十几种,而授权的方式则是多样化的,因为不同app不同业务,对于app资源访问的授权需求是不同的。最为常见的一种授权方式就是RBAC(Role Based Access Control,基于角色的访问控制),它定义了什么样的角色对于什么资源具有怎样的访问权限。在RBAC中,不同的用户都被赋予了不同的角色,而为了管理方便,又为具有相同资源访问权限的用户设计了用户组,而将访问控制设置在用户组上,更进一步,组和组之间还可以有父子关系。

请注意上面的黑体字,每一个黑体标注的词语都是授权相关的概念,在ASP.NET Core中,每一个授权需求(Authorization Requirement)对应一个实现IAuthorizationRequirement的类,并由AuthorizationHandler负责处理相应的授权逻辑。简单地理解,授权需求表示什么样的用户才能够满足被授权的要求,或者说什么样的用户才能够通过授权去访问资源。一个授权需求往往仅定义并处理一种特定的授权逻辑,ASP.NET Core允许将多个授权需求组合成授权策略(Authorization Policy)然后应用到被访问的资源上,这样的设计可以保证授权需求的设计与实现都是小粒度的,从而分离不同授权需求的关注点。在授权策略的层面,通过组合不同授权需求从而达到灵活实现授权业务的目的。

比如:假设app中有的API只允许管理员访问,而有的API只允许满18周岁的用户访问,而另外的一些API需要用户既是超级管理员又满18岁。那么就可以定义两种Authorization Requirement:GreaterThan18Requirement和SuperAdminRequirement,然后设计三种Policy:第一种只包含GreaterThan18Requirement,第二种只包含SuperAdminRequirement,第三种则同时包含这两种Requirement,最后将这些不同的Policy应用到不同的API上就可以了。

回到我们的案例代码,首先定义两个Requirement:SuperAdminRequirement和GreaterThan18Requirement:

1

2

3

4

5

6

public class SuperAdminRequirement : IAuthorizationRequirement

{

}

public class GreaterThan18Requirement : IAuthorizationRequirement

{

}

然后分别实现SuperAdminAuthorizationHandle和GreaterThan18AuthorizationHandler:



实现逻辑也非常清晰:在GreaterThan18AuthorizationHandler中,通过UserData claim获得年龄信息,如果年龄大于18,则授权成功;在SuperAdminAuthorizationHandler中,通过Role claim获得用户所处的角色,如果角色中包含super_admin,则授权成功。接下来就需要将这两个Requirement加到所需的Policy中,然后注册到应用程序里:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

public void ConfigureServices(IServiceCollection services)

{

    services.AddControllers();

    services.AddSwaggerGen(c =>

    {

        c.SwaggerDoc("v1", new OpenApiInfo { Title = "WebAPIAuthSample", Version = "v1" });

    });

    services.AddAuthentication("Basic").AddBasicAuthentication();

    services.AddAuthorization(options =>

    {

        options.AddPolicy("AgeMustBeGreaterThan18", builder =>

        {

            builder.Requirements.Add(new GreaterThan18Requirement());

        });

        options.AddPolicy("UserMustBeSuperAdmin", builder =>

        {

            builder.Requirements.Add(new SuperAdminRequirement());

        });

    });

    services.AddSingleton<IAuthorizationHandler, GreaterThan18AuthorizationHandler>();

    services.AddSingleton<IAuthorizationHandler, SuperAdminAuthorizationHandler>();

}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)

{

    if (env.IsDevelopment())

    {

        app.UseDeveloperExceptionPage();

        app.UseSwagger();

        app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "WebAPIAuthSample v1"));

    }

    app.UseHttpsRedirection();

    app.UseRouting();

    app.UseAuthentication();

    app.UseAuthorization();

    app.UseEndpoints(endpoints => { endpoints.MapControllers(); });

}

在ConfigureServices方法中,我们定义了两种Policy:AgeMustBeGreaterThan18和UserMustBeSuperAdmin,最后,在API Controller或者Action上,应用AuthorizeAttribute,从而指定所需的Policy即可。比如,如果希望WeatherForecase API只有年龄大于18岁的用户才能访问,那么就可以这样做:

1

2

3

4

5

6

7

8

9

10

11

12

13

[HttpGet]

[Authorize(Policy = "AgeMustBeGreaterThan18")]

public IEnumerable<WeatherForecast> Get()

{

    var rng = new Random();

    return Enumerable.Range(1, 5).Select(index => new WeatherForecast

        {

            Date = DateTime.Now.AddDays(index),

            TemperatureC = rng.Next(-20, 55),

            Summary = Summaries[rng.Next(Summaries.Length)]

        })

        .ToArray();

}

运行程序,假设有三个用户:daxnet、admin和foo,它们的BASE64认证信息分别为:

  • daxnet:ZGF4bmV0OnBhc3N3b3Jk

  • admin:YWRtaW46YWRtaW4=

  • foo:Zm9vOmJhcg==

那么,相同的curl命令,指定不同的用户认证信息时,得到的结果是不一样的:

daxnet用户年龄小于18岁,所以访问API不成功,服务端返回403:

3b07a7cb447f6b94d4d79fc05cd541d2.png

admin用户满足年龄大于18岁的条件,所以可以成功访问API:

c56b05f83448cc77bb7d839da48bd0ae.png

而foo用户本身没有在系统中注册,所以服务端返回401,表示用户没有认证成功:

24e1fa6d4c542aa8393ec98c6e2380c2.png

小结

本文简要介绍了ASP.NET Core中用户身份认证与授权的基本实现方法,帮助初学者或者需要使用这些功能的开发人员快速理解这部分内容。ASP.NET Core的认证与授权体系非常灵活,能够集成各种不同的认证机制与授权方式,文章也无法进行全面详细的介绍。不过无论何种框架哪种实现,它的实现基础也就是本文所介绍的这些内容,如果打算自己开发一套认证和授权的框架,也可以参考本文。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/293254.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

字符用_连接的是什么加密_防水连接器外壳与铜针表面涂层有什么用?

防水连接器外壳与铜针表面涂层会关系到产品的质量&#xff0c;毕竟材料选择方面&#xff0c;以及手工劳动方面都是需要把好关的&#xff0c;这样我们才能确保做出来了的产品送至用户身上是最好的。(凌科BD系列防水连接器铜针镀金效果)1、无氰偏碱亮铜&#xff1a;在铜合金材料防…

【27前端】base标签带有href属性会让chrome里的svg元素url失效

一个chrome的问题&#xff0c;但具体原因不明。 触发条件&#xff1a;chrome浏览器base标签里href属性有值的时候 触发问题&#xff1a;svg里面的元素如果有用url的滤镜和模糊&#xff0c;则会失效&#xff0c;在firefox里和IE10没有发现这个问题。 正常状态&#xff1a; 有bas…

强大的矩阵奇异值分解(SVD)及其应用

本文由LeftNotEasy发布于http://leftnoteasy.cnblogs.com, 本文可以被全部的转载或者部分使用&#xff0c;但请注明出处&#xff0c;如果有问题&#xff0c;请联系wheeleastgmail.com 前言&#xff1a; 上一次写了关于PCA与LDA的文章&#xff0c;PCA的实现一般有两种&#xff0…

已婚男人看见美女都这个眼神?

1 答应我&#xff1a;穿汉服晚上就别骑车了&#xff01;&#xff08;素材来源网络&#xff0c;侵删&#xff09;▼2 真香定理从来不迟到▼3 这万圣节大餐吃得下去吗&#xff1f;&#xff08;素材来源网络&#xff0c;侵删&#xff09;▼4 这...也灵活了吧&#xff1f;▼5 谁…

一个程序如何连接到外网_如何开发制作小程序?做一个电商带直播小程序

开发制作小程序可以让商家更方便地引流获客、增加线上订单。尤其是今年小程序直播大火&#xff0c;商家有了新的运营私域流量的利器&#xff0c;因此做一个电商带直播功能的小程序是很有用的。如何开发一个这样的小程序呢&#xff1f;流程如下&#xff1a;在「上线了」sxl.cn注…

推荐:Flowchart 一种通过文本方式描述的流程图

流程图&#xff08;Flowchart&#xff09;&#xff1a;使用图形表示算法的思路是一种极好的方法&#xff0c;因为千言万语不如一张图。流程图在汇编语言和早期的BASIC语言环境中得到应用。相关的还有一种PAD图&#xff0c;对PASCAL或C语言都极适用。Flowchart 是一种通过文本方…

thinkpad如何屏蔽bios更新 提示电池_有种血赚叫“二手”!3000搞定原价万元ThinkPad小黑本,真省钱...

你会为买种草已久笔记本剁手吗&#xff1f;在这不容易的2020年上半年&#xff0c;准备剁手买新电脑之前都得犹豫好几天吧&#xff0c;毕竟大家的钱包都收紧了。就连闲鱼上带有“年会奖品”、“刚买的老婆让退货”标签的东西都少了&#xff0c;各家厂商推出的新品也都在走极致性…

转载集合

本页链接均可单机跳转&#xff0c;网址过长的只给出超链接 背包九讲 pdfhttps://github.com/tianyicui/pack/blob/master/V2.pdf wzk线段树笔记http://wyfcyx.logdown.com/posts/201802-summary-data-structures-zkw-segment-tree-details 1 #include<cstdio>2 #include&…

c#屏幕录制(经典)(含源码和AForge.Video.FFMPEG.DLL)及填坑办法

一直觉得.net在多媒体处理方面渣得不行。最近需要做一个摄像头的程序&#xff0c;为了方便&#xff0c;用了AForge这个开源项目。AForge项目中有AForge.Video和AForge.Video. DirectShow这两个子项目&#xff0c;可以方便的调用摄像头。但是这两个项目最终只能取得视频帧&#…

drawable文件怎么添加图片_怎么给PDF文件添加书签

现如今我们使用的电子文档逐步都被PDF取代&#xff0c;虽然PDF有很多好处&#xff0c;但相较Word文档打开就能随意修改不同&#xff0c;PDF并不能直接编辑。比如有时我们要给PDF添加书签&#xff0c;这样可以快速找到要的页面&#xff0c;要怎么操作呢&#xff1f;一说到PDF的任…

通过Rancher Desktop在桌面上运行K8s

Rancher 发行的操作系统新选择&#xff1a;Rancher Desktop for Windows&#xff0c;它可以帮助你在Windows桌面上管理Kubernetes和容器。当然他当然会支持Linux&#xff0c;Mac的。准备工作在我们探索全新的Rancher Desktop之前&#xff0c;我们需要准备以下内容&#xff1a;1…

数学家排名,高斯第二牛顿第三?!看完第一的简历,他果然比牛顿还牛逼.........

如果让你给数学家排名&#xff0c;你会怎么排&#xff1f;谁排第一&#xff1f;高斯&#xff1f;阿基米德&#xff1f;还是其他哪位数学神仙&#xff1f;今天早上超模君发现&#xff0c;在国内某排行网站上&#xff0c;由网友投票选出来“世界十大数学家”里&#xff0c;名列前…

oc引导windows蓝屏_跟电脑蓝屏say no!【亲测有效】

​ 01专业解释电脑蓝屏&#xff0c;又叫蓝屏死机&#xff08;Blue Screen of Death&#xff0c;简称BSOD&#xff09;&#xff0c;是微软的 Windows 系列操作系统在无法从一个系统错误中恢复过来时&#xff0c;为保护电脑数据文件不被破坏而强制显示的屏幕图像。 看到了吧&…

hdu 1800 (map)

链接&#xff1a;http://acm.hdu.edu.cn/showproblem.php?pid1800 Flying to the Mars Time Limit: 5000/1000 MS (Java/Others) Memory Limit: 32768/32768 K (Java/Others)Total Submission(s): 10830 Accepted Submission(s): 3472 Problem DescriptionIn the year 8…

.NET 6 中的七个 System.Text.Json 特性

忽略循环引用在 .NET 5 中&#xff0c;如果存在循环依赖, 那么序列化的时候会抛出异常, 而在 .NET 6 中, 你可以选择忽略它。Category dotnet new() {Name ".NET 6", }; Category systemTextJson new() {Name "System.Text.Json",Parent dotnet }; do…

Redis整合Spring结合使用缓存实例

林炳文Evankaka原创作品。转载请注明出处http://blog.csdn.net/evankaka 摘要&#xff1a;本文介绍了如何在Spring中配置redis&#xff0c;并通过Spring中AOP的思想&#xff0c;将缓存的方法切入到有需要进入缓存的类或方法前面。 一、Redis介绍 什么是Redis&#xff1f; redis…

读取无线手柄数据_xbox series x/s 手柄开箱

原标题&#xff1a;xbox series x/s 手柄开箱xbox series x/s 手柄开箱 2020-11-12 08:29:003点赞2收藏4评论小编注&#xff1a;此篇文章来自#原创新人#激励计划&#xff0c;新人发文前三篇文章&#xff0c;篇篇额外奖励50金币。参加超级新人计划活动&#xff0c;新人发文即可瓜…

豆瓣评分9.4!这一部纪录片,探秘中国人迹罕至的未至之境!

全世界只有3.14 % 的人关注了爆炸吧知识Bilibili 联合“美国国家地理”&#xff0c;悄悄出品了一部史诗级动物记录片&#xff0c;忍不住要推荐给大朋友小朋友们——《未至之境》。这部纪录片由B站和国家地理联合创作&#xff0c;从绵延万里的山脉高原到枝繁叶茂的雨林竹海&…