通俗地来讲,JWT是能代表用户身份的令牌,可以使用JWT令牌在api接口中校验用户的身份以确认用户是否有访问api的权限。
授权:这是使用JWT的最常见方案。一旦用户登录,每个后续请求将包括JWT,允许用户访问该令牌允许的路由,服务和资源。

在身份验证中,当用户使用其凭据成功登录时,将返回JSON Web令牌。由于令牌是凭证,因此必须非常小心以防止出现安全问题。一般情况下,您不应该将令牌保留的时间超过要求。
每当用户想要访问受保护的路由或资源时,用户代理应该使用承载模式发送JWT,通常在Authorization标头中,标题的内容应如下所示:
Authorization: Bearer

1、应用程序向授权服务器请求授权;
2、校验用户身份,校验成功,返回token;
3、应用程序使用访问令牌访问受保护的资源。
JWT的实现方式是将用户信息存储在客户端,服务端不进行保存。每次请求都把令牌带上以校验用户登录状态,这样服务就变成了无状态的,服务器集群也很好扩展。
更多理论知识可以查看官网,或者查看相关网友的文章,如下推荐文章:
在nuget里面引用jwt集成的程序包,这里需要注意的是,如果你用的是.NET Core 3.1的框架的话,程序包版本选择3.1.7
Microsoft.AspNetCore.Authentication.JwtBearer
其中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}" };
- }
-
-
- }
- }
这里模拟一下登陆校验,只验证了用户密码不为空即通过校验,真实环境完善校验用户和密码的逻辑。
- 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,//是否验证SecurityKey
- IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Const.SecurityKey))//拿到秘钥SecurityKey
- };
- options.Events = new JwtBearerEvents
- {
- OnAuthenticationFailed = context =>
- {
- //Token expired
- if (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;
-
- //请求Url
- var 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!");
- }
2023-12.19日补充
在需要jwt验证控制器上添加 [ApiAuthorizeFilterAttribute] 特性

- using HBAirWebAPI.Controllers;
- using Microsoft.AspNetCore.Authorization;
- using Microsoft.AspNetCore.Mvc;
- using Microsoft.AspNetCore.Mvc.Controllers;
- using Microsoft.AspNetCore.Mvc.Filters;
- using Microsoft.IdentityModel.Tokens;
- using Sys.Model;
- using System;
- using System.Collections.Generic;
- using System.IdentityModel.Tokens.Jwt;
- using System.Linq;
- using System.Security.Claims;
- using System.Threading.Tasks;
-
- namespace HBAirWebAPI.Common
- {
- public class ApiAuthorizeFilterAttribute : ActionFilterAttribute
- {
- public override void OnActionExecuting(ActionExecutingContext context)
- {
- Microsoft.AspNetCore.Http.IHeaderDictionary dic = context.HttpContext.Request.Headers;
- var token = dic["Authorization"].ToString();
- if (token == null)
- {
- context.Result = new OkObjectResult(new { code = 401, messge = "用户权限已失效!" });
- }
- else
- {
- //token = token.Replace("Bearer", "").Trim();string secretKey, string issuer, string audience
- //检验token JWT 签名有效、时间有效.
- ClaimsPrincipal jwtUsr = ClaimsPrincipals.ValidateToken(token, ConfigurationManager.GetSection("Authentication:SecurityKey"), ConfigurationManager.GetSection("Authentication:Issure"), ConfigurationManager.GetSection("Authentication:Audience"));
- if (jwtUsr == null)
- {
- context.Result = new OkObjectResult(new { code = 401, messge = "用户权限已失效!" });
- }
-
- }
- // 如果不是一个控制器方法则直接返回
- if (!(context.ActionDescriptor is ControllerActionDescriptor action))
- {
- return;
- }
-
- var controllerActionDescriptor = context.ActionDescriptor as Microsoft.AspNetCore.Mvc.Controllers.ControllerActionDescriptor;
- 如果控制器上加了[AllowAnonymous],就跳过权限检查
- if (controllerActionDescriptor.ControllerTypeInfo.IsDefined(typeof(AllowAnonymousAttribute), true))
- {
- return;
- }
- //如果方法上加了[AllowAnonymous],就跳过权限检查
- if (action.MethodInfo.GetCustomAttributes(typeof(AllowAnonymousAttribute), false).Any())
- return;
-
- }
-
-
-
- }
- }
其中ClaimsPrincipals类代码如下:
-
- 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 HBAirWebAPI.Common
- {
- public class ClaimsPrincipals
- {
- public static ClaimsPrincipal ValidateToken(string token, string secretKey, string issuer, string audience)
- {
- var validationParameters = new TokenValidationParameters
- {
- ValidateIssuerSigningKey = true,
- IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(secretKey)),
- ValidateIssuer = true,
- ValidIssuer = issuer,
- ValidateAudience = true,
- ValidAudience = audience,
- ValidateLifetime = true,
- };
-
- var handler = new JwtSecurityTokenHandler();
- ClaimsPrincipal principal;
- try
- {
- principal = handler.ValidateToken(token, validationParameters, out _);
-
- // You can access the claims of the token using:
- //principal.Claims
-
- return principal;
- }
- catch (Exception ex)
- {
- // Token validation failed
- return null;
- }
- }
- }
- }
ClaimsPrincipals.ValidateToken 方法验证是否有效。无效则不通过。不能调用接口。
2023-12.19日补充 结束
在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,//是否验证SecurityKey
- IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Const.SecurityKey))//拿到秘钥SecurityKey
- };
- options.Events = new JwtBearerEvents
- {
- OnAuthenticationFailed = context =>
- {
- //Token expired
- if (context.Exception.GetType() == typeof(SecurityTokenExpiredException))
- {
- context.Response.Headers.Add("Token-Expired", "true");
- }
- return Task.CompletedTask;
- }
- };
- });
-
- //注入授权Handler
- services.AddSingleton<IAuthorizationHandler, PolicyHandler>();
- //注入获取HttpContext
- services.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();
- });
-
- }
- }
- }
在 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
访问成功。

完结。。。