当前位置: 首页 > news >正文

.NET CORE 3.1 集成JWT鉴权和授权2

JWT:全称是JSON Web Token是目前最流行的跨域身份验证、分布式登录、单点登录等解决方案。

 通俗地来讲,JWT是能代表用户身份的令牌,可以使用JWT令牌在api接口中校验用户的身份以确认用户是否有访问api的权限。

授权:这是使用JWT的最常见方案。一旦用户登录,每个后续请求将包括JWT,允许用户访问该令牌允许的路由,服务和资源。

在身份验证中,当用户使用其凭据成功登录时,将返回JSON Web令牌。由于令牌是凭证,因此必须非常小心以防止出现安全问题。一般情况下,您不应该将令牌保留的时间超过要求。

每当用户想要访问受保护的路由或资源时,用户代理应该使用承载模式发送JWT,通常在Authorization标头中,标题的内容应如下所示:

Authorization: Bearer <token>

1、应用程序向授权服务器请求授权;

2、校验用户身份,校验成功,返回token;

3、应用程序使用访问令牌访问受保护的资源。

 JWT的实现方式是将用户信息存储在客户端,服务端不进行保存。每次请求都把令牌带上以校验用户登录状态,这样服务就变成了无状态的,服务器集群也很好扩展。

更多理论知识可以查看官网,或者查看相关网友的文章,如下推荐文章:

  • asp.net core 集成JWT(一):https://www.cnblogs.com/7tiny/archive/2019/06/13/11012035.html
  • 五分钟带你了解啥是JWT:https://zhuanlan.zhihu.com/p/86937325
  • C#分布式登录——jwt:https://www.cnblogs.com/yswenli/p/13510050.html

在nuget里面引用jwt集成的程序包,这里需要注意的是,如果你用的是.NET Core 3.1的框架的话,程序包版本选择3.1.7

Microsoft.AspNetCore.Authentication.JwtBearer

添加数据访问模拟api,新建控制器ValuesController

其中api/value1是可以直接访问的,api/value2添加了权限校验特性标签 [Authorize]

using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;namespace jwtWebAPI.Controllers
{[ApiController]public class ValuesController : ControllerBase{[HttpGet][Route("api/values1")]public ActionResult<IEnumerable<string>> values1(){return new string[] { "value1", "value1" };}/*** 该接口用Authorize特性做了权限校验,如果没有通过权限校验,则http返回状态码为401* 调用该接口的正确姿势是:* 1.登陆,调用api/Auth接口获取到token* 2.调用该接口 api/value2 在请求的Header中添加参数 Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOiIxNTYwMzM1MzM3IiwiZXhwIjoxNTYwMzM3MTM3LCJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoiemhhbmdzYW4iLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjUwMDAiLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjUwMDAifQ.1S-40SrA4po2l4lB_QdzON_G5ZNT4P_6U25xhTcl7hI* Bearer后面有空格,且后面是第一步中接口返回的token值* */[HttpGet][Route("api/value2")][Authorize]public ActionResult<IEnumerable<string>> value2(){//这是获取自定义参数的方法var auth = HttpContext.AuthenticateAsync().Result.Principal.Claims;var userName = auth.FirstOrDefault(t => t.Type.Equals(ClaimTypes.NameIdentifier))?.Value;return new string[] { "访问成功:这个接口登陆过的用户都可以访问", $"userName={userName}" };}}
}

添加模拟登陆生成Token的api,新建控制器AuthController

这里模拟一下登陆校验,只验证了用户密码不为空即通过校验,真实环境完善校验用户和密码的逻辑。

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.IdentityModel.Tokens;
using System;
using System.Collections.Generic;
using System.IdentityModel.Tokens.Jwt;
using System.Linq;
using System.Security.Claims;
using System.Text;
using System.Threading.Tasks;namespace jwtWebAPI.Controllers
{[ApiController]public class AuthController : Controller{/// <summary>/// 通过账号+密码获取Token/// </summary>/// <param name="userName"></param>/// <param name="pwd"></param>/// <returns>Token</returns>[AllowAnonymous][HttpGet][Route("api/auth")]public IActionResult GetToken(string userName, string pwd){if (!string.IsNullOrEmpty(userName)){//每次登陆动态刷新Const.ValidAudience = userName + pwd + DateTime.Now.ToString();// push the user’s name into a claim, so we can identify the user later on.//这里可以随意加入自定义的参数,key可以自己随便起var claims = new[]{new Claim(JwtRegisteredClaimNames.Nbf,$"{new DateTimeOffset(DateTime.Now).ToUnixTimeSeconds()}") ,new Claim (JwtRegisteredClaimNames.Exp,$"{new DateTimeOffset(DateTime.Now.AddMinutes(3)).ToUnixTimeSeconds()}"),new Claim(ClaimTypes.NameIdentifier, userName)};//sign the token using a secret key.This secret will be shared between your API and anything that needs to check that the token is legit.var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Const.SecurityKey));var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);//.NET Core’s JwtSecurityToken class takes on the heavy lifting and actually creates the token.var token = new JwtSecurityToken(//颁发者issuer: Const.Domain,//接收者audience: Const.ValidAudience,//过期时间(可自行设定,注意和上面的claims内部Exp参数保持一致)expires: DateTime.Now.AddMinutes(3),//签名证书signingCredentials: creds,//自定义参数claims: claims);return Ok(new{token = new JwtSecurityTokenHandler().WriteToken(token)});}else{return BadRequest(new { message = "username or password is incorrect." });}}}
}

Startup添加JWT验证的相关配置

using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Tokens;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace jwtWebAPI
{public class Startup{public Startup(IConfiguration configuration){Configuration = configuration;}public IConfiguration Configuration { get; }// This method gets called by the runtime. Use this method to add services to the container.public void ConfigureServices(IServiceCollection services){//添加jwt验证:services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(options => {options.TokenValidationParameters = new TokenValidationParameters{ValidateLifetime = true,//是否验证失效时间ClockSkew = TimeSpan.FromSeconds(30),  //时间偏移量(允许误差时间)ValidateAudience = true,//是否验证Audience(验证之前的token是否失效)//ValidAudience = Const.GetValidudience(),//Audience//这里采用动态验证的方式,在重新登陆时,刷新token,旧token就强制失效了AudienceValidator = (m, n, z) =>{return m != null && m.FirstOrDefault().Equals(Const.ValidAudience);},ValidateIssuer = true,//是否验证Issuer(颁发者)ValidAudience = Const.Domain,//Audience    【Const是新建的一个常量类】  接收者 ValidIssuer = Const.Domain,//Issuer,这两项和前面签发jwt的设置一致      颁发者ValidateIssuerSigningKey = true,//是否验证SecurityKeyIssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Const.SecurityKey))//拿到秘钥SecurityKey};options.Events = new JwtBearerEvents{OnAuthenticationFailed = context =>{//Token expiredif (context.Exception.GetType() == typeof(SecurityTokenExpiredException)){context.Response.Headers.Add("Token-Expired", "true");}return Task.CompletedTask;}};});services.AddControllers();}// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.public void Configure(IApplicationBuilder app, IWebHostEnvironment env){ //添加jwt验证app.UseAuthentication();if (env.IsDevelopment()){app.UseDeveloperExceptionPage();}app.UseHttpsRedirection();app.UseRouting();app.UseAuthorization();app.UseEndpoints(endpoints =>{endpoints.MapControllers();});}}
}

创建常量类Const

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;namespace jwtWebAPI
{public class Const{/// <summary>/// 这里为了演示,写死一个密钥。实际生产环境可以从配置文件读取,这个是用网上工具随便生成的一个密钥(md5或者其他都可以)/// </summary>public const string SecurityKey = "48754F4C58F9EA428FE09D714E468211";/// <summary>/// 站点地址(颁发者、接受者),这里测试和当前本地运行网站相同,实际发到正式环境应为域名地址/// </summary>public const string Domain = "https://localhost:44345";/// <summary>/// 受理人,之所以弄成可变的是为了用接口动态更改这个值以模拟强制Token失效/// 真实业务场景可以在数据库或者redis存一个和用户id相关的值,生成token和验证token的时候获取到持久化的值去校验/// 如果重新登陆,则刷新这个值/// </summary>public static string ValidAudience;}
}

JWT登录授权测试成功

返回了状态码401,也就是未经授权:访问由于凭据无效被拒绝。 说明JWT校验生效了,我们的接口收到了保护。

调用模拟登陆授权接口:https://localhost:44345/api/auth?userName=xiongze&pwd=123456

这里的用户密码是随便写的,因为我们模拟登陆只是校验了下非空,因此写什么都能通过。

然后我们得到了一个xxx.yyy.zzz 格式的 token 值。我们把token复制出来。

在刚才401的接口(https://localhost:44345/api/values2)请求header中添加JWT的参数,把我们的token加上去

再次调用我们的模拟数据接口,但是这次我们加了一个header,KEY:Authorization     Value:Bearer Tokne的值

这里需要注意 Bearer 后面是有一个空格的,然后就是我们上一步获取到的token,

得到返回值,正确授权成功,我们是支持自定义返回参数的,上面代码里面有相关内容,比如用户名这些不敏感的信息可以带着返回。

等token设置的过期时间到了,或者重新生成了新的Token,没有及时更新,那么我们的授权也到期,401,

升级操作:接口权限隔离

上面的操作是所有登录授权成功的角色都可以进行调用所有接口,那么我们现在想要进行接口隔离限制,

也就是说,虽然授权登录了,但是我这个接口是指定权限访问的。

比如说:删除接口只能管理员角色操作,那么其他角色虽然授权登录了,但是没有权限调用删除接口。

我们在原来的操作进行改造升级看一下。

添加类

新建一个AuthManagement文件夹,添加PolicyRequirement类PolicyHandler类

PolicyRequirement类:

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;namespace jwtWebAPI.AuthManagement
{/// <summary>/// 权限承载实体/// </summary>public class PolicyRequirement : IAuthorizationRequirement{/// <summary>/// 用户权限集合/// </summary>public List<UserPermission> UserPermissions { get; private set; }/// <summary>/// 无权限action/// </summary>public string DeniedAction { get; set; }/// <summary>/// 构造/// </summary>public PolicyRequirement(){//没有权限则跳转到这个路由DeniedAction = new PathString("/api/nopermission");//用户有权限访问的路由配置,当然可以从数据库获取UserPermissions = new List<UserPermission> {new UserPermission {  Url="/api/values3", UserName="admin"},};}}/// <summary>/// 用户权限承载实体/// </summary>public class UserPermission{/// <summary>/// 用户名/// </summary>public string UserName { get; set; }/// <summary>/// 请求Url/// </summary>public string Url { get; set; }}
}

PolicyHandler类(注意2.x和3.x的区别)

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;namespace jwtWebAPI.AuthManagement
{public class PolicyHandler : AuthorizationHandler<PolicyRequirement>{private readonly IHttpContextAccessor _httpContextAccessor;public PolicyHandler(IHttpContextAccessor httpContextAccessor){_httpContextAccessor = httpContextAccessor;}protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, PolicyRequirement requirement){//赋值用户权限var userPermissions = requirement.UserPermissions;var httpContext = _httpContextAccessor.HttpContext;//请求Urlvar questUrl = httpContext.Request.Path.Value.ToUpperInvariant();//是否经过验证var isAuthenticated = httpContext.User.Identity.IsAuthenticated;if (isAuthenticated){if (userPermissions.GroupBy(g => g.Url).Any(w => w.Key.ToUpperInvariant() == questUrl)){//用户名var userName = httpContext.User.Claims.SingleOrDefault(s => s.Type == ClaimTypes.NameIdentifier).Value;if (userPermissions.Any(w => w.UserName == userName && w.Url.ToUpperInvariant() == questUrl)){context.Succeed(requirement);}else{无权限跳转到拒绝页面//httpContext.Response.Redirect(requirement.DeniedAction);return Task.CompletedTask;}}else{context.Succeed(requirement);}}return Task.CompletedTask;}}
}

添加指定角色

在 AuthController 控制器的GetToken授权加入自定义的参数,如下

new Claim("Role", userName)  //这里是角色,我使用登录账号admin代替

 在 AuthController 控制器里面添加无权限访问的方法

[AllowAnonymous]
[HttpGet]
[Route("api/nopermission")]
public IActionResult NoPermission()
{return Forbid("No Permission!");
}

修改Startup配置

在startup.cs的ConfigureServices 方法里面添加策略鉴权模式、添加JWT Scheme、注入授权Handler 

修改后的文件如下

using jwtWebAPI.AuthManagement;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.HttpsPolicy;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.IdentityModel.Tokens;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;namespace jwtWebAPI
{public class Startup{public Startup(IConfiguration configuration){Configuration = configuration;}public IConfiguration Configuration { get; }// This method gets called by the runtime. Use this method to add services to the container.public void ConfigureServices(IServiceCollection services){services//添加策略鉴权模式.AddAuthorization(options =>{options.AddPolicy("Permission", policy => policy.Requirements.Add(new PolicyRequirement()));})//添加JWT Scheme.AddAuthentication(s =>{s.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;s.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;s.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;})//添加jwt验证:.AddJwtBearer(options => {options.TokenValidationParameters = new TokenValidationParameters{ValidateLifetime = true,//是否验证失效时间ClockSkew = TimeSpan.FromSeconds(30),  //时间偏移量(允许误差时间)ValidateAudience = true,//是否验证Audience(验证之前的token是否失效)//ValidAudience = Const.GetValidudience(),//Audience//这里采用动态验证的方式,在重新登陆时,刷新token,旧token就强制失效了AudienceValidator = (m, n, z) =>{return m != null && m.FirstOrDefault().Equals(Const.ValidAudience);},ValidateIssuer = true,//是否验证Issuer(颁发者)ValidAudience = Const.Domain,//Audience    【Const是新建的一个常量类】  接收者 ValidIssuer = Const.Domain,//Issuer,这两项和前面签发jwt的设置一致      颁发者ValidateIssuerSigningKey = true,//是否验证SecurityKeyIssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Const.SecurityKey))//拿到秘钥SecurityKey};options.Events = new JwtBearerEvents{OnAuthenticationFailed = context =>{//Token expiredif (context.Exception.GetType() == typeof(SecurityTokenExpiredException)){context.Response.Headers.Add("Token-Expired", "true");}return Task.CompletedTask;}};});//注入授权Handlerservices.AddSingleton<IAuthorizationHandler, PolicyHandler>();//注入获取HttpContextservices.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();services.AddControllers();}// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.public void Configure(IApplicationBuilder app, IWebHostEnvironment env){ //添加jwt验证app.UseAuthentication();if (env.IsDevelopment()){app.UseDeveloperExceptionPage();}app.UseHttpsRedirection();app.UseRouting();app.UseAuthorization();app.UseEndpoints(endpoints =>{endpoints.MapControllers();});}}
}

添加api访问的方法

在 ValuesController控制器添加指定权限访问的方法,如下:

/*** 这个接口必须用admin**/[HttpGet][Route("api/values3")][Authorize("Permission")]public ActionResult<IEnumerable<string>> values3(){//这是获取自定义参数的方法var auth = HttpContext.AuthenticateAsync().Result.Principal.Claims;var userName = auth.FirstOrDefault(t => t.Type.Equals(ClaimTypes.NameIdentifier))?.Value;var role = auth.FirstOrDefault(t => t.Type.Equals("Role"))?.Value;return new string[] { "访问成功:这个接口有管理员权限才可以访问", $"userName={userName}", $"Role={role}" };}

 不同权限测试访问

我们同样的方法去模拟登录,https://localhost:44345/api/auth?userName=xiongze&pwd=123

注意,账号先不用admin登录,然后用返回的token去请求我们刚刚添加的指定权限访问的接口,这个时候是没有权限访问的,因为这个是admin权限访问。

我们同样的方法去模拟登录,https://localhost:44345/api/auth?userName=xiongze&pwd=123

注意,账号先不用admin登录,然后用返回的token去请求我们刚刚添加的指定权限访问的接口,这个时候是没有权限访问的,因为这个是admin权限访问。

我们同样的方法去模拟登录,https://localhost:44345/api/auth?userName=admin&pwd=123

访问成功。

完结。。。

相关文章:

  • 【Oracle】Navicat Premium 连接 Oracle的两种方式
  • Python--练习:报数字(数7)
  • 【2023MathorCup大数据竞赛】B题完整解答过程(思路+模型文档+代码+结果)
  • 【C语言】文件操作详解
  • 性能测试工具——Jmeter的安装【超详细】
  • Pytorch:model.train()和model.eval()用法和区别,以及model.eval()和torch.no_grad()的区别
  • C语言之判断与循环语句知识点总结
  • 基于群居蜘蛛算法的无人机航迹规划
  • PostgreSQL 的 Replication Slot分析研究
  • 数据结构实验3
  • 树与二叉树(考研版)
  • 基于Kubesphere容器云平台物联网云平台Devops实践
  • RabbitMQ的交换机(原理及代码实现)
  • WPF:自定义按钮模板
  • python基础语法(十一)
  • [译]前端离线指南(上)
  • 【面试系列】之二:关于js原型
  • CSS相对定位
  • ES2017异步函数现已正式可用
  • leetcode98. Validate Binary Search Tree
  • MYSQL 的 IF 函数
  • Quartz初级教程
  • TypeScript实现数据结构(一)栈,队列,链表
  • webpack4 一点通
  • 关于 Linux 进程的 UID、EUID、GID 和 EGID
  • 前端面试总结(at, md)
  • 译有关态射的一切
  • 原生JS动态加载JS、CSS文件及代码脚本
  • 【运维趟坑回忆录】vpc迁移 - 吃螃蟹之路
  • scrapy中间件源码分析及常用中间件大全
  • shell使用lftp连接ftp和sftp,并可以指定私钥
  • 翻译 | The Principles of OOD 面向对象设计原则
  • 哈罗单车融资几十亿元,蚂蚁金服与春华资本加持 ...
  • ​DB-Engines 12月数据库排名: PostgreSQL有望获得「2020年度数据库」荣誉?
  • ​queue --- 一个同步的队列类​
  • ###项目技术发展史
  • #我与Java虚拟机的故事#连载13:有这本书就够了
  • ( )的作用是将计算机中的信息传送给用户,计算机应用基础 吉大15春学期《计算机应用基础》在线作业二及答案...
  • (0)Nginx 功能特性
  • (1)(1.13) SiK无线电高级配置(六)
  • (MIT博士)林达华老师-概率模型与计算机视觉”
  • (个人笔记质量不佳)SQL 左连接、右连接、内连接的区别
  • (原创)Stanford Machine Learning (by Andrew NG) --- (week 9) Anomaly DetectionRecommender Systems...
  • (转)Android中使用ormlite实现持久化(一)--HelloOrmLite
  • (转)机器学习的数学基础(1)--Dirichlet分布
  • (自用)learnOpenGL学习总结-高级OpenGL-抗锯齿
  • .java 指数平滑_转载:二次指数平滑法求预测值的Java代码
  • .NET Core MongoDB数据仓储和工作单元模式封装
  • .NET 反射的使用
  • .net经典笔试题
  • @AliasFor注解
  • @vue/cli 3.x+引入jQuery
  • [ vulhub漏洞复现篇 ] Grafana任意文件读取漏洞CVE-2021-43798
  • [145] 二叉树的后序遍历 js
  • [2016.7 day.5] T2