在上一篇《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”和一个空格。
完整的验证演示如下:
知识改变命运