认证授权方案之JwtBearer认证

1.前言

回顾:认证方案之初步认识JWT

在现代Web应用程序中,即分为前端与后端两大部分。当前前后端的趋势日益剧增,前端设备(手机、平板、电脑、及其他设备)层出不穷。因此,为了方便满足前端设备与后端进行通讯,就必须有一种统一的机制。所以导致API架构的流行。而RESTful API这个API设计思想理论也就成为目前互联网应用程序比较欢迎的一套方式。

这种API架构思想的引入,因此,我们就需要考虑用一种标准的,通用的,无状态的,与语言无关的身份认证方式来实现API接口的认证。

HTTP提供了一套标准的身份验证框架:服务端可以用来针对客户端的请求发送质询(challenge),客户端根据质询提供应答身份验证凭证。

质询与应答的工作流程如下:服务端向客户端返回401(Unauthorized,未授权)状态码,并在WWW-Authenticate头中添加如何进行验证的信息,其中至少包含有一种质询方式。然后客户端可以在请求中添加Authorization头进行验证,其Value为身份验证的凭证信息。

在本文中,将要介绍的是以Jwt Bearer方式进行认证。

2.Bearer认证

本文要介绍的Bearer验证也属于HTTP协议标准验证,它随着OAuth协议而开始流行,详细定义见:RFC 6570。

     +--------+                               +---------------+|        |--(A)- Authorization Request ->|   Resource    ||        |                               |     Owner     ||        |<-(B)-- Authorization Grant ---|               ||        |                               +---------------+|        ||        |                               +---------------+|        |--(C)-- Authorization Grant -->| Authorization || Client |                               |     Server    ||        |<-(D)----- Access Token -------|               ||        |                               +---------------+|        ||        |                               +---------------+|        |--(E)----- Access Token ------>|    Resource   ||        |                               |     Server    ||        |<-(F)--- Protected Resource ---|               |+--------+                               +---------------+

A security token with the property that any party in possession of the token (a "bearer") can use the token in any way that any other party in possession of it can. Using a bearer token does not require a bearer to prove possession of cryptographic key material (proof-of-possession).

因此Bearer认证的核心是Token,Bearer验证中的凭证称为BEARER_TOKEN,或者是access_token,它的颁发和验证完全由我们自己的应用程序来控制,而不依赖于系统和Web服务器,Bearer验证的标准请求方式如下:

Authorization: Bearer [BEARER_TOKEN]

那么使用Bearer验证有什么好处呢?

  • CORS: cookies + CORS 并不能跨不同的域名。而Bearer验证在任何域名下都可以使用HTTP header头部来传输用户信息。

  • 对移动端友好: 当你在一个原生平台(iOS, Android, WindowsPhone等)时,使用Cookie验证并不是一个好主意,因为你得和Cookie容器打交道,而使用Bearer验证则简单的多。

  • CSRF: 因为Bearer验证不再依赖于cookies, 也就避免了跨站请求攻击。

  • 标准:在Cookie认证中,用户未登录时,返回一个302到登录页面,这在非浏览器情况下很难处理,而Bearer验证则返回的是标准的401 challenge

3.JWT

上面介绍的Bearer认证,其核心便是BEARER_TOKEN,那么,如何确保Token的安全是重中之重。一种是通过HTTPS的方式,另一种是通过对Token进行加密编码签名,而最流行的Token编码签名方式便是:JSON WEB TOKEN。

Json web token (Jwt), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准(RFC 7519)。该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。

JWT是由.分割的如下三部分组成:

Header.Payload.Signature

还记得之前说个的一篇认证方案之初步认识JWT吗?没有的,可以看看,对JWT的特点和基本原理介绍,可以进一步的了解。

学习了之前的文章后,我们可以发现使用JWT的好处在于通用性、紧凑性和可拓展性。

  • 通用性:因为json的通用性,所以JWT是可以进行跨语言支持的,像JAVA,JavaScript,NodeJS,PHP等很多语言都可以使用。

  • 紧凑性:JWT的构成非常简单,字节占用很小,通过 GET、POST 等放在 HTTP 的 header 中,便于传输。

  • 可扩展性:JWT是自我包涵的,因为有了payload部分,包含了必要的一些其他业务逻辑所必要的非敏感信息,自身存储,不需要在服务端保存会话信息, 非常易于应用的扩展。

4.开始

1. 注册认证服务

在这里,我们用微软给我们提供的JwtBearer认证方式,实现认证服务注册 。

引入nuget包:Microsoft.AspNetCore.Authentication.JwtBearer

注册服务,将服务添加到容器中,

    public void ConfigureServices(IServiceCollection services){services.AddControllers();var Issurer = "JWTBearer.Auth";  //发行人var Audience = "api.auth";       //受众人var secretCredentials = "q2xiARx$4x3TKqBJ";   //密钥//配置认证服务services.AddAuthentication(x =>{x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;}).AddJwtBearer(o=>{o.TokenValidationParameters = new TokenValidationParameters{//是否验证发行人ValidateIssuer = true,ValidIssuer = Issurer,//发行人//是否验证受众人ValidateAudience = true,ValidAudience = Audience,//受众人//是否验证密钥ValidateIssuerSigningKey = true,IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(secretCredentials)),ValidateLifetime = true, //验证生命周期RequireExpirationTime = true, //过期时间};});}

注意说明:

一. TokenValidationParameters的参数默认值:
1. ValidateAudience = true,  ----- 如果设置为false,则不验证Audience受众人
2. ValidateIssuer = true ,   ----- 如果设置为false,则不验证Issuer发布人,但建议不建议这样设置
3. ValidateIssuerSigningKey = false,
4. ValidateLifetime = true,  ----- 是否验证Token有效期,使用当前时间与Token的Claims中的NotBefore和Expires对比
5. RequireExpirationTime = true, ----- 是否要求Token的Claims中必须包含Expires
6. ClockSkew = TimeSpan.FromSeconds(300), ----- 允许服务器时间偏移量300秒,即我们配置的过期时间加上这个允许偏移的时间值,才是真正过期的时间(过期时间 +偏移值)你也可以设置为0,ClockSkew = TimeSpan.Zero

调用方法,配置Http请求管道:

    public void Configure(IApplicationBuilder app, IWebHostEnvironment env){if (env.IsDevelopment()){app.UseDeveloperExceptionPage();}app.UseRouting();//1.先开启认证app.UseAuthentication();//2.再开启授权app.UseAuthorization();app.UseEndpoints(endpoints =>{endpoints.MapControllers();});}

JwtBearerOptions的配置中,通常IssuerSigningKey(签名秘钥), ValidIssuer(Token颁发机构), ValidAudience(颁发给谁) 三个参数是必须的,后两者用于与TokenClaims中的IssuerAudience进行对比,不一致则验证失败。

2.接口资源保护

创建一个需要授权保护的资源控制器,这里我们用建立API生成项目自带的控制器,WeatherForecastController.cs, 在控制器上使用Authorize即可

[ApiController]
[Route("[controller]")]
[Authorize]
public class WeatherForecastController : ControllerBase
{private static readonly string[] Summaries = new[]{"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"};private readonly ILogger<WeatherForecastController> _logger;public WeatherForecastController(ILogger<WeatherForecastController> logger){_logger = logger;}[HttpGet]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();}
}

3. 生成Token

因为微软为我们内置了JwtBearer验证,但是没有提供Token的发放,所以这里我们要实现生成Token的方法

引入Nugets包:System.IdentityModel.Tokens.Jwt

这里我们根据IdentityModel.Tokens.Jwt文档给我们提供的帮助类,提供了方法WriteToken创建Token,根据参数SecurityToken,可以实例化,JwtSecurityToken,指定可选参数的类。

        /// <summary>/// Initializes a new instance of the <see cref="JwtSecurityToken"/> class specifying optional parameters./// </summary>/// <param name="issuer">If this value is not null, a { iss, 'issuer' } claim will be added, overwriting any 'iss' claim in 'claims' if present.</param>/// <param name="audience">If this value is not null, a { aud, 'audience' } claim will be added, appending to any 'aud' claims in 'claims' if present.</param>/// <param name="claims">If this value is not null then for each <see cref="Claim"/> a { 'Claim.Type', 'Claim.Value' } is added. If duplicate claims are found then a { 'Claim.Type', List&lt;object&gt; } will be created to contain the duplicate values.</param>/// <param name="expires">If expires.HasValue a { exp, 'value' } claim is added, overwriting any 'exp' claim in 'claims' if present.</param>/// <param name="notBefore">If notbefore.HasValue a { nbf, 'value' } claim is added, overwriting any 'nbf' claim in 'claims' if present.</param>/// <param name="signingCredentials">The <see cref="SigningCredentials"/> that will be used to sign the <see cref="JwtSecurityToken"/>. See <see cref="JwtHeader(SigningCredentials)"/> for details pertaining to the Header Parameter(s).</param>/// <exception cref="ArgumentException">If 'expires' &lt;= 'notbefore'.</exception>public JwtSecurityToken(string issuer = null, string audience = null, IEnumerable<Claim> claims = null, DateTime? notBefore = null, DateTime? expires = null, SigningCredentials signingCredentials = null){if (expires.HasValue && notBefore.HasValue){if (notBefore >= expires)throw LogHelper.LogExceptionMessage(new ArgumentException(LogHelper.FormatInvariant(LogMessages.IDX12401, expires.Value, notBefore.Value)));}Payload = new JwtPayload(issuer, audience, claims, notBefore, expires);Header = new JwtHeader(signingCredentials);RawSignature = string.Empty;}

这样,我们可以根据参数指定内容:

1. string iss = "JWTBearer.Auth";  // 定义发行人
2. string aud = "api.auth";       //定义受众人audience
3. IEnumerable<Claim> claims = new Claim[]
{
new Claim(JwtClaimTypes.Id,"1"),
new Claim(JwtClaimTypes.Name,"i3yuan"),
};//定义许多种的声明Claim,信息存储部分,Claims的实体一般包含用户和一些元数据
4. var nbf = DateTime.UtcNow;  //notBefore  生效时间
5. var Exp = DateTime.UtcNow.AddSeconds(1000);  //expires 过期时间
6. string sign = "q2xiARx$4x3TKqBJ"; //SecurityKey 的长度必须 大于等于 16个字符var secret = Encoding.UTF8.GetBytes(sign);var key = new SymmetricSecurityKey(secret);var signcreds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);

好了,通过以上填充参数内容,进行传参赋值得到,完整代码如下:

新增AuthController.cs控制器:

    [HttpGet]public IActionResult GetToken(){try{//定义发行人issuerstring iss = "JWTBearer.Auth";//定义受众人audiencestring aud = "api.auth";//定义许多种的声明Claim,信息存储部分,Claims的实体一般包含用户和一些元数据IEnumerable<Claim> claims = new Claim[]{new Claim(JwtClaimTypes.Id,"1"),new Claim(JwtClaimTypes.Name,"i3yuan"),};//notBefore  生效时间// long nbf =new DateTimeOffset(DateTime.Now).ToUnixTimeSeconds();var nbf = DateTime.UtcNow;//expires   //过期时间// long Exp = new DateTimeOffset(DateTime.Now.AddSeconds(1000)).ToUnixTimeSeconds();var Exp = DateTime.UtcNow.AddSeconds(1000);//signingCredentials  签名凭证string sign = "q2xiARx$4x3TKqBJ"; //SecurityKey 的长度必须 大于等于 16个字符var secret = Encoding.UTF8.GetBytes(sign);var key = new SymmetricSecurityKey(secret);var signcreds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);var jwt = new JwtSecurityToken(issuer: iss, audience: aud, claims:claims,notBefore:nbf,expires:Exp, signingCredentials: signcreds);var JwtHander = new JwtSecurityTokenHandler();var token = JwtHander.WriteToken(jwt);return Ok(new{access_token = token,token_type = "Bearer",});}catch (Exception ex){throw;}}
注意:
1.SecurityKey 的长度必须 大于等于 16个字符,否则生成会报错。(可通过在线随机生成密钥)

5. 运行

访问获取Token方法,获取得到access_token:

再访问,授权资源接口,可以发现,再没有添加请求头token值的情况下,返回了401没有权限。

这次,在请求头通过Authorization加上之前获取的token值后,再次进行访问,发现已经可以获取访问资源控制器,并返回对应的数据。

6.扩展说明

在HTTP标准验证方案中,我们比较熟悉的是"Basic"和"Digest",前者将用户名密码使用BASE64编码后作为验证凭证,后者是Basic的升级版,更加安全,因为Basic是明文传输密码信息,而Digest是加密后传输。

一、Basic基础认证

Basic认证是一种较为简单的HTTP认证方式,客户端通过明文(Base64编码格式)传输用户名和密码到服务端进行认证,通常需要配合HTTPS来保证信息传输的安全。

客户端请求需要带Authorization请求头,值为“Basic xxx”,xxx为“用户名:密码”进行Base64编码后生成的值。若客户端是浏览器,则浏览器会提供一个输入用户名和密码的对话框,用户输入用户名和密码后,浏览器会保存用户名和密码,用于构造Authorization值。当关闭浏览器后,用户名和密码将不再保存。

凭证为“YWxhzGRpbjpvcGVuc2VzYWl1”,是通过将“用户名:密码”格式的字符串经过的Base64编码得到的。而Base64不属于加密范畴,可以被逆向解码,等同于明文,因此Basic传输认证信息是不安全的。

Basic基础认证图示:

缺陷汇总1.用户名和密码明文(Base64)传输,需要配合HTTPS来保证信息传输的安全。2.即使密码被强加密,第三方仍可通过加密后的用户名和密码进行重放攻击。3.没有提供任何针对代理和中间节点的防护措施。4.假冒服务器很容易骗过认证,诱导用户输入用户名和密码。

二、Digest摘要认证

Digest认证是为了修复基本认证协议的严重缺陷而设计的,秉承“绝不通过明文在网络发送密码”的原则,通过“密码摘要”进行认证,大大提高了安全性。

Digest认证步骤如下:第一步:客户端访问Http资源服务器。由于需要Digest认证,服务器返回了两个重要字段nonce(随机数)和realm。第二步:客户端构造Authorization请求头,值包含username、realm、nouce、uri和response的字段信息。其中,realm和nouce就是第一步返回的值。nouce只能被服务端使用一次。uri(digest-uri)即Request-URI的值,但考虑到经代理转发后Request-URI的值可能被修改、因此实现会复制一份副本保存在uri内。response也可叫做Request-digest,存放经过MD5运算后的密码字符串,形成响应码。第三步:服务器验证包含Authorization值的请求,若验证通过则可访问资源。Digest认证可以防止密码泄露和请求重放,但没办法防假冒。所以安全级别较低。Digest和Basic认证一样,每次都会发送Authorization请求头,也就相当于重新构造此值。所以两者易用性都较差。

Digest认证图示:

7.注意

  1. 在进行JwtBearer认证时,在生成token之后,还需要与刷新token配合使用,因为当用户执行了退出,修改密码等操作时,需要让该token无效,无法再次使用,所以,会给access_token设置一个较短的有效期间,(JwtBearer认证默认会验证有效期,通过notBeforeexpires来验证),当access_token过期后,可以在用户无感知的情况下,使用refresh_token重新获取access_token,但这就不属于Bearer认证的范畴了,但是我们可以通过另一种方式通过IdentityServer的方式来实现,在后续中会对IdentityServer进行详细讲解。

  2. 在生成token的时候,需要用的secret,主要是用来防止token被伪造与篡改。因为当token被劫取的时候,可以得到你的令牌中带的一些个人不重要的信息明文,但不用担心,只要你不在生成token里把私密的个人信息放出去的话,就算被动机不良的人得到,也做不了什么事情。但是你可能会想,如果用户自己随便的生成一个 token ,带上你的信息,那不就可以随便访问你的资源服务器了,因此这个时候就需要利用secret 来生成 token,来确保数字签名的正确性。而且在认证授权资源,进行token解析的时候,通过微软的源码发现,已经帮我们封装了方法,对secret进行了校验了,确保了token的安全性,从而保证api资源的安全。

8.总结

  1. JwtToken在认证时,无需Security token service安全令牌服务器的参与,都是基于Claim的,默认会验证有效期,通过notBeforeexpires来验证,这在分布式中提供给了极大便利。

  2. JwtToken与平台、无言无关,在前端也可以直接解析出Claims。

  3. 如果有不对的或不理解的地方,希望大家可以多多指正,提出问题,一起讨论,不断学习,共同进步。

  4. 后面会对认证授权方案中的授权这一块进行说明分享。

参考JwtBearer源码

往期精彩回顾

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

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

相关文章

使用过滤器模式,让客户关怀中的代码更加干净整洁

一&#xff1a;实际场景介绍我们在给用户做订单催付通知的时候&#xff0c;会有这样的一种场景&#xff0c;用户在系统后台设置一组可以催付的规则&#xff0c;比如说订单金额大于xx元&#xff0c;非黑名单用户&#xff0c;来自哪个地区&#xff0c;已购买过某个商品&#xff0…

C++实现各种排序以及复杂度,稳定性分析

代码如下: #include<iostream> using namespace std;void Bubble_Sort(int *a, int n) {bool flag;int tmp 0;for (int i n - 1; i > 0; i--){flag false;for (int j 0; j < i; j){if (a[j] > a[j 1]){swap(a[j], a[j 1]);flag true;}}if (!flag) break…

Webapi管理和性能测试工具WebBenchmark

WebBenchmark是一款基于开源通讯组件Beetlex扩展的Webapi管理和性能测试工具&#xff0c;在传统工具中一般管理工具缺乏性能压测能力或有性能压测的缺少管理功能&#xff1b;WebBenchmark的设计目标是就管理和性能测试能力同时具备。接下来介绍一下工具的功能和使用&#xff1a…

Abstract Factory(抽象工厂)--对象创建模式

Abstract Factory &#xff08;抽象工厂&#xff09;–对象创建模式 一、意图 提供一个创建一系列相关或者相互依赖的接口&#xff0c;而无需指定它们具体的类。 二、动机 1.在软件系统中&#xff0c;经常面临着“一系列相互依赖的对象”的创建工 作;同时&#xff0c;由于需求…

Builder(生成器)--对象创建型模式

Builder&#xff08;生成器&#xff09;–对象创建型模式 一、意图 将一个复杂的对象构建与它的表示分离&#xff0c;使得同样的构建过程可以创建不同的表示。 二、动机 1.在软件系统中&#xff0c;有时候面临着“一个复杂对象”的创建工作&#xff0c;其通常由各个部分的子对…

Gartner:缺乏技术人才将影响企业数字化转型

导语大多数公司在数字化转型的阶段对所需的技能方面都处于“盲目”状态。正文随着COVID-19响应加快了数字化转型的速度和规模&#xff0c;缺乏数字化技能可能会危害人才计划不统一的公司。甚至在冠状病毒大流行之前&#xff0c;董事会就将数字/技术中断列为2020年的头等大事&am…

DEBUG org.springframework.web.servlet.DispatcherServlet - Error rendering view [org.thymeleaf.spring

报错信息如下: 报错原因: thymeleaf有一些限制&#xff0c;使用th语言&#xff0c;内容为空就会报错 改成这样解决问题:

Factory Method(工厂方法)--对象创建型模式

Factory Method&#xff08;工厂方法&#xff09;–对象创建型模式 一、意图 定义一个用于创建对象的接口&#xff0c;让子类决定实例化哪一个类。Factory Method使一个类的实例化延迟到其子类。 二、动机 1.在软件系统中&#xff0c;经常面临着创建对象的工作;由于需求的变化…

全内存的redis用习惯了?那能突破内存限制类redis产品ssdb呢?

首先说一下背景&#xff0c;在双十一的时候&#xff0c;我们系统接受X宝的订单推送&#xff0c;原先的实现方式是使用 redis 的 List 作为推送数据的承载&#xff0c;在非大促的场景下&#xff0c;一切运行正常&#xff0c;内存占用大概3-4G&#xff0c;机器是16G内存。由于提前…

Prototype(原型)--对象创建模式

Prototype&#xff08;原型&#xff09;–对象创建模式 一、意图 用原型实例指定创建对象的种类&#xff0c;并且通过拷贝这些原型创建新的对象。 二、动机 1.在软件系统中&#xff0c;经常面临着“某些结构复杂的对象”的创建工作&#xff1b;由于需求的变化&#xff0c;这些…

认证授权方案之授权揭秘 (上篇)

一、前言回顾&#xff1a;认证授权方案之授权初识从上一节中&#xff0c;我们在对授权系统已经有了初步的认识和使用&#xff0c;可以发现&#xff0c;asp.net core为我们提供的授权策略是一个非常强大丰富且灵活的认证授权方案&#xff0c;能够满足大部分的授权场景。在Config…

Singleton(单件)--对象创建模式

Singleton&#xff08;单件&#xff09;–对象创建模式 一、意图 保证一个类仅有一个实例&#xff0c;并提供一个访问它的全局访问点。 二、动机 1.在软件系统中&#xff0c;经常有这样一些特殊的类&#xff0c;必须保证它们在系统中只存在一个实例&#xff0c;才能确保它们的…

龙芯团队 在移值 MIPS64 下的.NET Core 进度速报

写在开始前我们的主要业务基于 dotnet core 2.x 与 3.1 完成&#xff0c;目前 dotnet core 3.1 支持的 CPU 架构列表中还不包含龙芯&#xff0c;且在 gitlab issue 中表示官方当前没有对 MIPS 的支持计划。更具体操作系统与 CPU 架构列表见 [Download .NET Core 3.1](https://d…

Adapter(适配器)--类对象结构型模式

Adapter&#xff08;适配器&#xff09;–类对象结构型模式 一、意图 将一个类的接口转换成客户希望的另外一个接口。Adapter模式使得原本接口不兼容而不能一起工作的那些类可以一起工作。 二、动机 1.在软件系统中&#xff0c;由于应用环境的变化&#xff0c;常常需要将“一…

如何使用ABP框架(2)三层架构与领域驱动设计的对比

本文来自长沙.NET技术社区&#xff0c;原创&#xff1a;邹溪源。全文共有8500字&#xff0c;读完需耗时10分钟。题图来自pixabay简述上一篇简述了ABP框架中的一些基础理论&#xff0c;包括ABP前后端项目的分层结构&#xff0c;以及后端项目中涉及到的知识点&#xff0c;例如DTO…

Bridge(桥接)--对象结构模式

Bridge&#xff08;桥接&#xff09;–对象结构模式 一、意图 将抽象部分与它的实现部分分离&#xff0c;使它们都可以独立的变化。 二、动机 1.由于某些类型的固有的实现逻辑&#xff0c;使得它们具有两个变化的维度&#xff0c;乃至多个纬度的变化。 2.如何应对这种“多维度…

Composite(组合)--对象结构型模式

Composite&#xff08;组合&#xff09;–对象结构型模式 一、意图 将对象组合成树形结构以表示“部分-整体”的层次结构。Composite使得用户对单个对象和组合对象的使用具有一致性。 二、动机 1.软件在某些情况下&#xff0c;客户代码过多的依赖于对象容器复杂的内部实现结构…

[SpringBoot2]ajax函数中data参数的数据设置方式

ajax函数中data参数的数据设置方式&#xff1a;

Blazor带我重玩前端(二)

概览Blazor目前有两种托管模式&#xff0c;一种是Server-Side模式&#xff0c;一种是WebAssembly模式。官方首先支持的是Service-Side模式&#xff0c;使用WebAssembly模式&#xff0c;需要更新到最新版VS2019。小编目前的精力是更多的专注于Blazor-WebAssembly模式的研究&…

Decorator(装饰)--对象结构型模式

Decorator&#xff08;装饰&#xff09;–对象结构型模式 一、意图 1.动态地给一个对象添加一些额外的职责。就增加功能来说&#xff0c;Decorator模式相比生成子类更为灵活。 二、动机 1.在某些情况下我们可能会“过度地使用继承来扩展对象的功能”&#xff0c; 由于继承为类…