创建常量类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登录授权测试成功
把程序编译运行起来,打开postman,输入地址,首先测试不需要任何授权的
正确地返回了数据,那么接下来我们测试JWT的流程。
首先我们什么都不加调用接口:https://localhost:44345/api/values2,注意,我创建的时候是https的,大家注意看是http还是https
返回了状态码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!"); }
修改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,//是否验证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(); }); } } }
添加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=admin&pwd=123
访问成功。
源码下载地址
Gitee:https://gitee.com/xiongze/jwtWebAPI.git