ASP.NET Core实现JWT授权与认证(2.实战篇

[删除(380066935@qq.com或微信通知)]

更好的阅读体验请查看原文:https://www.cnblogs.com/green-jcx/p/JWTDemo.html

在上一篇《ASP.NET Core实现JWT授权与认证(1.理论篇)文章当中我们主要介绍了些JWT理论方面的内容,那么在本篇当中会接着上篇的主题,针对JWT如何让在ASP.NET Core当中落地实操进行展开。本篇不会过多使用文字描述,主要的内容体现在代码和注释方面,所以需要根据本文中的步骤,结合代码进行分析和演练,才能更好的掌握JWT的运用。


 

1.创建项目

首先我们创建一个ASP.NET Core WebAPI的项目,本文使用的IDE是VS2022,框架版本是.NET 5.0。另外,在创建项目时记得勾选“启用OpenAPI支持”,以便我们可以使用Swagger进行调试。


 

 2.配置参数

JWT中的Payload(载荷)部分的内容,我们通常会采用配置文件的形式配置一些参数,这便于后期根据需求变动可以灵活更改。在ASP.NET Core中我们通常将这些参数配置在“appsettings.json”文件中,在本文中对于JWT配置的参数包括了:密钥、令牌颁发者、令牌使用者。打开“appsettings.json”文件,新增“Audience”节点字段,配置的参数结构如下:

 1 {
 2   "Logging": {
 3     "LogLevel": {
 4       "Default": "Information",
 5       "Microsoft": "Warning",
 6       "Microsoft.Hosting.Lifetime": "Information"
 7     }
 8   },
 9   "AllowedHosts": "*",
10   "Audience": {
11     "Secret": "XiWangYiQingZaoRiJieShu", //私钥(长度要大于16位,内容自定义)
12     "Issuer": "JWTDemo.API", //颁布者
13     "Audience": "student" //订阅者
14   }
15 }

在配置过程中需要注意的是“Secret”字段,该字段值的长度有一定限制,长度要大于16位,否则程序会产生异常。


3.读取配置

在上步中我们将JWT中的某些参数定义到了配置文件“appsettings.json”中,那么当前我们就需要编写一个可以读取配置文件“appsettings.json”的类,从而获取JWT所需要的参数。

具体实现步骤如下:

1.在解决方案下新建一个类库项目“JWTDemo.Common”,然后在类库项目中新建Helper文件夹并创建一个公共的Appsettings类,该类用于帮助读取“appsettings.json”文件中的系统配置参数。

2.针对当前类库项目“JWTDemo.Common”,使用Nuget安装编码所需要的依赖包,其中包括:Microsoft.Extensions.Configuration.Abstractions、Microsoft.Extensions.Configuration。

3.编码实现程序功能,该类的具体实现如下:

 1 using Microsoft.Extensions.Configuration;
 2 using System;
 3 using System.Collections.Generic;
 4 using System.Linq;
 5 using System.Text;
 6 using System.Threading.Tasks;
 7 
 8 namespace JWTDemo.Common.Helper
 9 {
10 /// <summary>
11 /// 用于帮助读取appsettings.json中的系统配置参数
12 /// </summary>
13 public class Appsettings
14 {
15     static IConfiguration Configuration { get; set; }
16     
17     public Appsettings(IConfiguration configuration)
18     {
19         Configuration = configuration;
20     }
21     
22     /// <summary>
23     /// 获取用appsettings.json某个字段下的值
24     /// </summary>
25     /// <param name="sections">获取值所在的字段(基于JSON层次结构,某个值会存在于多个层级的字段中)</param>
26     /// <returns>JSON字段的值</returns>
27     public static string GetVal(params string [] sections)
28     {
29         try
30         {
31             if (sections.Any())
32             {
33                 string key = string.Join(":", sections);
34                 return Configuration[key];
35             }
36         }
37         catch (Exception) { }
38         
39         return string.Empty;
40     } // END GetVal()
41     
42     /// <summary>
43     /// 获取用appsettings.json某个字段下值(值是一个组数)
44     /// </summary>
45     /// <param name="sections">获取值所在的字段(基于JSON层次结构,某个值会存在于多个层级的字段中)</param>
46     /// <returns>JSON字段的多个值(集合)</returns>
47     public static List<T> GetValues<T>(params string[] sections)
48     {
49         List<T> list = new List<T>();
50         Configuration.Bind(string.Join(":", sections), list);
51         return list;
52     } // END GetValues()
53     
54     
55     
56 }
57 }

当然,操作“appsettings.json”文件的需求不会仅仅只有以上的这些,但是这里只我们针对JWT使用到的部分进行了编写。

 4.找到当前WebAPI项目下的Startup.cs类,并在其中的ConfigureServices方法中“注入”上面编写好的Appsettings类。

 1  public void ConfigureServices(IServiceCollection services)
 2         {
 3 
 4             services.AddControllers();
 5             services.AddSwaggerGen(c =>
 6             {
 7                 c.SwaggerDoc("v1", new OpenApiInfo { Title = "JWTDemo.API", Version = "v1" });
 8             });
 9             services.AddSingleton(new Appsettings(Configuration)); //将Appsettings对象以单例模式进行注入
10 
11         }

4.生成JWT

在实现参数配置相关的功能后,接着需要通过具体的代码创建一个具有一定规则的JWT令牌。在“JWTDemo.Common”项目下的Helper文件夹下新建“JwtHelper”类。新建后通过NuGet安装“System.IdentityModel.Token.Jwt”的包,JwtHelper类具体实现如下:

  1 using System;
  2 using System.Collections.Generic;
  3 using System.Linq;
  4 using System.Text;
  5 using System.Threading.Tasks;
  6 using System.IdentityModel.Tokens.Jwt;
  7 using System.Security.Claims;
  8 using Microsoft.IdentityModel.Tokens;
  9 
 10 namespace JWTDemo.Common.Helper
 11 {
 12 
 13     /// <summary>
 14     /// 用于存储JWT中用户的关键信息
 15     /// </summary>
 16     public class JwtUserInfo
 17     {
 18         /// <summary>
 19         /// ID
 20         /// </summary>
 21         public long Uid { get; set; }
 22 
 23         /// <summary>
 24         /// 角色
 25         /// </summary>
 26         public string Role { get; set; }
 27 
 28 
 29         /// <summary>
 30         /// 职能
 31         /// </summary>
 32         public string Work { get; set; }
 33 
 34     }
 35 
 36     /// <summary>
 37     /// JWT操作帮助类
 38     /// </summary>
 39     public class JwtHelper
 40     {
 41         /// <summary>
 42         /// 颁发JWT
 43         /// </summary>
 44         /// <param name="tokenModel">当前颁发对象的用户信息</param>
 45         /// <returns>JWT字符串</returns>
 46         public static string IssueJwt(JwtUserInfo jwtUserInfo)
 47         {
 48 
 49             #region 【Step1-从配置文件中获取生成JWT所需要的数据】
 50             string iss = Appsettings.GetVal(new string[] { "Audience", "Issuer" });//颁发者
 51             string aud = Appsettings.GetVal(new string[] { "Audience", "Audience" });//使用者
 52             string secret = Appsettings.GetVal(new string[] { "Audience", "Secret" }); //密钥
 53             #endregion
 54 
 55             #region 【Step2-通过Claim创建JWT中的Payload(载荷)信息】
 56 
 57             var claimsIdentity = new List<Claim>
 58                 {
 59                  new Claim(JwtRegisteredClaimNames.Jti, jwtUserInfo.Uid.ToString()), //JWT ID
 60                 new Claim(JwtRegisteredClaimNames.Iat, $"{new DateTimeOffset(DateTime.Now).ToUnixTimeSeconds()}"),//JWT的发布时间
 61                 new Claim (JwtRegisteredClaimNames.Exp,$"{new DateTimeOffset(DateTime.Now.AddSeconds(1000)).ToUnixTimeSeconds()}"),//JWT到期时间
 62                 new Claim(JwtRegisteredClaimNames.Iss,iss), //颁发者
 63                 new Claim(JwtRegisteredClaimNames.Aud,aud)//使用者
 64                };
 65 
 66             //添加用户的角色信息(非必须,可添加多个)
 67             var claimRoleList=jwtUserInfo.Role.Split(',').Select(role => new Claim(ClaimTypes.Role, role)).ToList();
 68             claimsIdentity.AddRange(claimRoleList);
 69             #endregion
 70 
 71             #region 【Step3-签名对象】
 72 
 73             var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secret)); //创建密钥对象
 74             var sigCreds = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256); //创建密钥签名对象
 75 
 76             #endregion
 77 
 78             #region 【Step5-将JWT相关信息封装成对象】
 79             var jwt = new JwtSecurityToken(
 80               issuer: iss,
 81               claims: claimsIdentity,
 82               signingCredentials: sigCreds);
 83             #endregion
 84 
 85             #region 【Step6-将JWT信息对象生成字符串形式】
 86             var jwtHandler = new JwtSecurityTokenHandler();
 87             string token = jwtHandler.WriteToken(jwt);
 88             #endregion
 89 
 90             return token;
 91         } // END IssueJwt()
 92 
 93         /// <summary>
 94         /// 将JWT加密的字符串进行解析
 95         /// </summary>
 96         /// <param name="jwtStr">JWT加密的字符</param>
 97         /// <returns>JWT中的用户信息</returns>
 98         public static JwtUserInfo SerializeJwtStr(string jwtStr)
 99         {
100             JwtUserInfo jwtUserInfo = new JwtUserInfo();
101             var jwtHandler = new JwtSecurityTokenHandler();
102 
103             if (!string.IsNullOrEmpty(jwtStr) && jwtHandler.CanReadToken(jwtStr))  
104             {
105                 //将JWT字符读取到JWT对象
106                 JwtSecurityToken jwtToken = jwtHandler.ReadJwtToken(jwtStr);
107 
108                 //获取JWT中的用户信息
109                 jwtUserInfo.Uid = Convert.ToInt64(jwtToken.Id);
110                 object role;
111                 jwtToken.Payload.TryGetValue(ClaimTypes.Role, out role); //获取角色信息
112                 jwtUserInfo.Role = role == null ? "" : role.ToString();
113             }
114 
115             return jwtUserInfo;
116         } //END SerializeJwt()
117 
118 
119 
120     }
121 }

上面代码中Claims的创建部分,很多都是可选的,你可以根据自己的需求参考标准规范文档自行选择:https://datatracker.ietf.org/doc/html/rfc7519


5.获取JWT

在实际的环境中,获取JWT往往是通过登陆接口验证成功后进行的派发,本示例处于演示的目的,登陆部分进行了省略,则直接对JWT的生成函数进行调用。首先在“JWTDemo.API”项目中添加对“JWTDemo.Commom”项目的引用,然后在Controllers目录下新建一个API控制器“LoginController”,并在“LoginController”中创建一个用于获取JWT的Action,具体实现如下:

 1 using JWTDemo.Common.Helper;
 2 using Microsoft.AspNetCore.Http;
 3 using Microsoft.AspNetCore.Mvc;
 4 using System.Threading.Tasks;
 5 
 6 namespace JWTDemo.API.Controllers
 7 {
 8 
 9  
10     [Route("api/[controller]")]
11     [ApiController]
12     public class LoginController : ControllerBase
13     {
14         [HttpGet]
15         public async Task<object> GetJwtStr(string userName,string pwd)
16         {
17             //假设这里已成功验证登录有效性。。。。
18 
19             //登陆成功后,基于当前用户生成JWT令牌字符串
20             JwtUserInfo jwtUserInfo = new JwtUserInfo { Uid = 1, Role = "Admin,Leader" };
21             string jwtStr = JwtHelper.IssueJwt(jwtUserInfo);
22 
23             return Ok(new { success=true,token= jwtStr });
24         }
25 
26 
27     }
28 }

到目前为止,我们就可以启动程序并通过Swagger,调用上面的Controller实现JWT令牌的获取。获取到的JWT内容如下图:


6.配置授权

在实现了JWT令牌的派发后,接下来就需要为进行授权处理,也就是为接口设置一定的访问权限,只有符合接口访问权限规则的用户才能对接口资源进行调用。 本示例使用的是“基于角色的授权”,也就是会赋予某些接口需要指定的角色才能访问,那么用户只要符合了接口要求的角色,就可以对其进行访问。

实现配置步骤如下:

1.在Startup.cs的ConfigureServices方法中写入“基于具体角色授权”的代码:

1            //添加授权策略服务
2             services.AddAuthorization(options => {
3                 options.AddPolicy("Client", policy => policy.RequireRole("Client").Build());//单独角色
4                 options.AddPolicy("Admin", policy => policy.RequireRole("Admin").Build());
5                 options.AddPolicy("SystemOrAdmin", policy => policy.RequireRole("Admin", "System"));//或的关系
6                 options.AddPolicy("SystemAndAdmin", policy => policy.RequireRole("Admin").RequireRole("System"));//且的关系
7             });

对于某些资源的访问,可能会要求你必须同时属于多种角色。所以从上面代码的内容上可以看出,不光可以基于单个角色授权,还可以通过“或的关系”和“且的关系”将多个角色联系在一起。例如,在学校的职员当中,有的人不光是老师还同时是班主任,那么一般开家长会的权利,则通常会要求你即是老师也是班主任。

 

2.根据不同的访问需求,可以为WEBApi中的Controller或Action,标识可以访问的权限(角色),在本示例中我们可以在项目自带的“WeatherForecastController”进行权限的标识。


上面的代码为Action标识了权限,这就是意味着如果用户要访问该Action,首先需要获取到相应API颁发的JWT令牌,然后用户的角色必须为“Admin”,才能对其进行访问。


7.配置认证

随着本文的步骤到目前为止,我们实现的WebAPI示例已经具备令牌的发放和授权机制,此时就要需要一个最重要的认证环节。为什么说它重要,因为认证相当于API的“把关口”,如果没有它,前面做的令牌和授权也都是白费。

这就好比如时下疫情高发时刻下的核酸检查一样,核酸证明就好比派发的“令牌”。进入商场、图书馆、地铁等公共场所要求做了核酸证明的人才能进入,这就相当于“授权”。有了“令牌”和“授权”,如果不在出入口指派检查人员进行把关认证,那么这就等同于形同虚设了。

 

回到API中的授予与认证体系也是如此,所以就必须实现认证服务配置,如果不进行认证服务配置程序会产生异常。异常现象如下图所示:


 

 Bearer认证

本文在ASP.NET Core中采用的JWT认证方案是Bearer认证,它定义了一套认证逻辑,对JWT中内容的三个部分(消息头、载荷、签名)进行处理和效验。Bearer认证属于HTTP协议标准认证,它是随着OAuth协议而开始流行。

实现Bearer认证的步骤具体如下:

1.使用NuGet安装Bearer认证所依赖的包:Microsoft.AspNetCore.Authentication.JwtBearer

2.在Startup.cs的ConfigureServices方法中写入以下代码:

 1              //添加认证
 2             services.AddAuthentication(x =>
 3             {
 4                 // 仔细看这个单词 上图中错误的提示里的那个
 5                 x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
 6                 x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
 7 
 8             }).AddJwtBearer(o => {
 9 
10                 //读取配置文件
11                 var audienceConfig = Configuration.GetSection("Audience");
12                 var symmetricKeyAsBase64 = audienceConfig["Secret"];
13                 var keyByteArray = Encoding.ASCII.GetBytes(symmetricKeyAsBase64);
14                 var signingKey = new SymmetricSecurityKey(keyByteArray);
15 
16                 o.TokenValidationParameters = new TokenValidationParameters
17                 {
18                     ValidateIssuerSigningKey = true,
19                     IssuerSigningKey = signingKey,
20                     ValidateIssuer = true,
21                     ValidIssuer = audienceConfig["Issuer"],//发行人
22                     ValidateAudience = true,
23                     ValidAudience = audienceConfig["Audience"],//订阅人
24                     ValidateLifetime = true,
25                     ClockSkew = TimeSpan.Zero,//这个是缓冲过期时间,也就是说,即使我们配置了过期时间,这里也要考虑进去,过期时间+缓冲,默认好像是7分钟,你可以直接设置为0
26                     RequireExpirationTime = true,
27                 };
28             });

3.在Startup.cs的Configure方法中配置中间件

 


8.验证结果

完成对WebAPI的JWT授权与认证后,我们就需要通过Swagger来调试验证下对应功能的有效性。为了方便测试,通常采用的方式是:先单独在Swagger上调用“登陆”接口获取JWT令牌,然后在将获取到的令牌手动配置到Swagger中,最后在调用需要访问的数据接口。

基于这种测试形式,我们则需要为Swagger工具配置支持手动录入Token令牌的功能,首先需要通过NuGet安装“Swashbuckle.AspNetCore.Filters”依赖包,并在Startup.cs的ConfigureServices方法中的AddSwagerGen部分中增加配置代码,Swagger完整的配置代码如下:

 1  services.AddSwaggerGen(c =>
 2             {
 3                 c.SwaggerDoc("v1", new OpenApiInfo { Title = "JWTDemo.API", Version = "v1" });
 4              
 5                 // 开启小锁
 6                 c.OperationFilter<AddResponseHeadersFilter>();
 7                 c.OperationFilter<AppendAuthorizeToSummaryOperationFilter>();
 8 
 9                 // 在header中添加token,传递到后台
10                 c.OperationFilter<SecurityRequirementsOperationFilter>();
11 
12                 c.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme
13                 {
14 
15                     Description = "JWT授权(数据将在请求头中进行传输) 直接在下框中输入Bearer {token}(注意两者之间是一个空格)\"",
16                     Name = "Authorization",//jwt默认的参数名称
17                     In = ParameterLocation.Header,//jwt默认存放Authorization信息的位置(请求头中)
18                     Type = SecuritySchemeType.ApiKey
19                 });
20             });  // END AddSwaggerGen

 

如果调用接口不使用Swagger手动录入Token令牌,那么API会返回401(未授权)状态码。

 

 Swagger界面上配置Token令牌是点击“锁”按钮来触发,并且这个按钮分布在两块,一处是页面右上角(针对所有的方法设置),另一处是每个方法的右上角(仅针对该方法的设置)。

 

另外,在Swagger中输入令牌时需要注意的是:在令牌字符的签名需要加一个“Bearer”和一个空格。

 

完整的验证演示如下: