前言
近日,应一位朋友的邀请写了个Asp.Net Core基于JWT认证的数据接口网关Demo。朋友自己开了个公司,接到的一个升级项目,客户要求用Aps.Net Core做数据网关服务且基于JWT认证实现对前后端分离的数据服务支持,于是想到我一直做.Net开发,问我是否对.Net Core有所了解?能不能做个简单Demo出来看看?我说,分道扬镳之后我不是调用别人的接口就是提供接口给别人调用,于是便有了以下示例代码。
示例要求能演示获取Token及如何使用该Token访问数据资源,在Demo中实现了JWT的颁发及验证以及重写一个ActionAuthorizeAttribute实现对具体数据接口的调用权限控制,先看一下项目截图:
项目文件介绍
解决方案下只有一个项目,项目名称就叫Jwt.Gateway,包含主要文件有:
- Controllers目录下的ApiActionFilterAttribute.cs文件,继承Microsoft.AspNetCore.Mvc.Filters.ActionFilterAttribute,用于校验接口调用者对具体接口的访问权限。
- Controllers目录下的ApiBase.cs文件,继承Microsoft.AspNetCore.Mvc.Controller,具有Microsoft.AspNetCore.Authorization.Authorize特性引用,用于让所有数据接口用途的控制器继承,定义有CurrentAppKey属性(来访应用程序的身份标识)并在OnActionExecuting事件中统一分析Claims并赋值。
- Controllers目录下的TokenController.cs控制器文件,用于对调用方应用程序获取及注销Token。
- Controllers目录下的UsersController.cs控制器文件,继承ApiBase.cs,作为数据调用示例。
- MiddleWares目录下的ApiCustomException.cs文件,是一个数据接口的统一异常处理中间件。
- Models目录下的ApiResponse.cs文件,用于做数据接口的统一数据及错误信息输出实体模型。
- Models目录下的User.cs文件,示例数据实体模型。
- Program.cs及Startup.cs文件就不介绍了,随便建个空项目都有。
项目文件代码
ApiActionFilterAttribute.cs
Controllers目录下的ApiActionFilterAttribute.cs文件,继承Microsoft.AspNetCore.Mvc.Filters.ActionFilterAttribute,用于校验接口调用者对具体接口的访问权限。
设想每一个到访的请求都是一个应用程序,每一个应用程序都分配有基本的Key和Password,每一个应用程序具有不同的接口访问权限,所以在具体的数据接口上应该声明该接口所要求的权限值,比如修改用户信息的接口应该在接口方法上声明需要具有“修改用户”的权限,用例: [ApiActionFilter("用户修改")]
。
大部分情况下一个接口(方法)对应一个操作,这样基本上就能应付了,但是不排除有时候可能需要多个权限组合进行验证,所以该文件中有一个对多个权限值进行校验的“与”和“和”枚举,用例: [ApiActionFilter(new string[] { "用户修改", "用户录入", "用户删除" },ApiActionFilterAttributeOption.AND)]
,这样好像就差不多了。
由于在一个接口调用之后可能需要将该接口所声明需要的权限值记入日志等需求,因此权限值集合将被写入到HttpContext.Items["Permissions"]中以方便可能的后续操作访问,看代码:
using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc.Filters; namespace Jwt.Gateway.Controllers { public enum ApiActionFilterAttributeOption { OR,AND } public class ApiActionFilterAttribute : Microsoft.AspNetCore.Mvc.Filters.ActionFilterAttribute { List<string> Permissions = new List<string>(); ApiActionFilterAttributeOption Option = ApiActionFilterAttributeOption.AND; public ApiActionFilterAttribute(string permission) { Permissions.Add(permission); } public ApiActionFilterAttribute(string[] permissions, ApiActionFilterAttributeOption option) { foreach(var permission in permissions) { if (Permissions.Contains(permission)) { continue; } Permissions.Add(permission); } Option = option; } public override void OnActionExecuting(ActionExecutingContext context) { var key = GetAppKey(context); List<string> keyPermissions = GetAppKeyPermissions(key); var isAnd = Option == ApiActionFilterAttributeOption.AND; var permissionsCount = Permissions.Count; var keyPermissionsCount = keyPermissions.Count; for (var i = 0; i < permissionsCount; i++) { bool flag = false; for (var j = 0; j < keyPermissions.Count; j++) { if (flag = string.Equals(Permissions[i], keyPermissions[j], StringComparison.OrdinalIgnoreCase)) { break; } } if (flag) { continue; } if (isAnd) { throw new Exception("应用“" + key + "”缺少“" + Permissions[i] + "”的权限"); } } context.HttpContext.Items.Add("Permissions", Permissions); base.OnActionExecuting(context); } private string GetAppKey(ActionExecutingContext context) { var claims = context.HttpContext.User.Claims; if (claims == null) { throw new Exception("未能获取到应用标识"); } var claimKey = claims.ToList().Find(o => string.Equals(o.Type, "AppKey", StringComparison.OrdinalIgnoreCase)); if (claimKey == null) { throw new Exception("未能获取到应用标识"); } return claimKey.Value; } private List<string> GetAppKeyPermissions(string appKey) { List<string> li = new List<string> { "用户明细","用户列表","用户录入","用户修改","用户删除" }; return li; } } } ApiActionAuthorizeAttribute.cs
ApiBase.cs
Controllers目录下的ApiBase.cs文件,继承Microsoft.AspNetCore.Mvc.Controller,具有Microsoft.AspNetCore.Authorization.Authorize特性引用,用于让所有数据接口用途的控制器继承,定义有CurrentAppKey属性(来访应用程序的身份标识)并在OnActionExecuting事件中统一分析Claims并赋值。
通过验证之后,Aps.Net Core会在HttpContext.User.Claims中将将来访者的身份信息记录下来,我们可以通过该集合得到来访者的身份信息。
using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Filters; namespace Jwt.Gateway.Controllers { [Microsoft.AspNetCore.Authorization.Authorize] public class ApiBase : Microsoft.AspNetCore.Mvc.Controller { private string _CurrentAppKey = ""; public string CurrentAppKey { get { return _CurrentAppKey; } } public override void OnActionExecuting(ActionExecutingContext context) { var claims = context.HttpContext.User.Claims.ToList(); var claim = claims.Find(o => o.Type == "appKey"); if (claim == null) { throw new Exception("未通过认证"); } var appKey = claim.Value; if (string.IsNullOrEmpty(appKey)) { throw new Exception("appKey不合法"); } _CurrentAppKey = appKey; base.OnActionExecuting(context); } } } ApiBase.cs
TokenController.cs
Controllers目录下的TokenController.cs控制器文件,用于对调用方应用程序获取及注销Token。
using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; namespace Jwt.Gateway.Controllers { [Route("api/[controller]/[action]")] public class TokenController : Controller { private readonly Microsoft.Extensions.Configuration.IConfiguration _configuration; public TokenController(Microsoft.Extensions.Configuration.IConfiguration configuration) { _configuration = configuration; } // /api/token/get public IActionResult Get(string appKey, string appPassword) { try { if (string.IsNullOrEmpty(appKey)) { throw new Exception("缺少appKey"); } if (string.IsNullOrEmpty(appKey)) { throw new Exception("缺少appPassword"); } if (appKey != "myKey" && appPassword != "myPassword")//固定的appKey及appPassword,实际项目中应该来自数据库或配置文件 { throw new Exception("配置不存在"); } var key = new Microsoft.IdentityModel.Tokens.SymmetricSecurityKey(System.Text.Encoding.UTF8.GetBytes(_configuration["JwtSecurityKey"])); var creds = new Microsoft.IdentityModel.Tokens.SigningCredentials(key, Microsoft.IdentityModel.Tokens.SecurityAlgorithms.HmacSha256); var claims = new List<System.Security.Claims.Claim>(); claims.Add(new System.Security.Claims.Claim("appKey", appKey));//仅在Token中记录appKey var token = new System.IdentityModel.Tokens.Jwt.JwtSecurityToken( issuer: _configuration["JwtTokenIssuer"], audience: _configuration["JwtTokenAudience"], claims: claims, expires: DateTime.Now.AddMinutes(30), signingCredentials: creds); return Ok(new Models.ApiResponse { status = 1, message = "OK", data = new System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler().WriteToken(token) }); } catch(Exception ex) { return Ok(new Models.ApiResponse { status = 0, message = ex.Message, data = "" }); } } // /api/token/delete public IActionResult Delete(string token) { //code: 加入黑名单,使其无效 return Ok(new Models.ApiResponse { status = 1, message = "OK", data = "" }); } } } TokenController.cs
UsersController.cs
Controllers目录下的UsersController.cs控制器文件,继承ApiBase.cs,作为数据调用示例。
该控制器定义了对User对象常规的明细、列表、录入、修改、删除等操作。
using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; namespace Jwt.Gateway.Controllers { [Produces("application/json")] [Route("api/[controller]/[action]")] public class UsersController : ApiBase { /* * 1.要访问访问该控制器提供的接口请先通过"/api/token/get"获取token * 2.访问该控制器提供的接口http请求头必须具有值为"Bearer+空格+token"的Authorization键,格式参考: * "Authorization"="Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiQXBwIiwiYXBwS2V5IjoibXlLZXkiLCJleHAiOjE1NTE3ODc2MDMsImlzcyI6IkdhdGV3YXkiLCJhdWQiOiJhdWRpZW5jZSJ9.gQ9_Q7HUT31oFyfl533T-bNO5IWD2drl0NmD1JwQkMI" */ /// <summary> /// 临时用户测试数据,实际项目中应该来自数据库等媒介 /// </summary> static List<Models.User> _Users = null; static object _Lock = new object(); public UsersController() { if (_Users == null) { lock (_Lock) { if (_Users == null) { _Users = new List<Models.User>(); var now = DateTime.Now; for(var i = 0; i < 10; i++) { var num = i + 1; _Users.Add(new Models.User { UserId = num, UserName = "name"+num, UserPassword = "pwd"+num, UserJoinTime = now }); } } } } } // /api/users/detail [ApiActionFilter("用户明细")] public IActionResult Detail(long userId) { /* //获取appKey(在ApiBase中写入) var appKey = CurrentAppKey; //获取使用的权限(在ApiActionAuthorizeAttribute中写入) var permissions = HttpContext.Items["Permissions"]; */ var user = _Users.Find(o => o.UserId == userId); if (user == null) { throw new Exception("用户不存在"); } return Ok(new Models.ApiResponse { data = user, status = 1, message = "OK" }); } // /api/users/list [ApiActionFilter("用户列表")] public IActionResult List(int page, int size) { page = page < 1 "OK", total = total }); } var li = new List<Models.User>(); var startIndex = page * size - size; var endIndex = startIndex + size - 1; if (endIndex > total - 1) { endIndex = total - 1; } for(; startIndex <= endIndex; startIndex++) { li.Add(_Users[startIndex]); } return Ok(new Models.ApiResponse { data = li, status = 1, message = "OK", total = total }); } // /api/users/add [ApiActionFilter("用户录入")] public IActionResult Add() { return Ok(new Models.ApiResponse { status = 1, message = "OK" }); } // /api/users/update [ApiActionFilter(new string[] { "用户修改", "用户录入", "用户删除" },ApiActionFilterAttributeOption.AND)] public IActionResult Update() { return Ok(new Models.ApiResponse { status = 1, message = "OK" }); } // /api/users/delete [ApiActionFilter("用户删除")] public IActionResult Delete() { return Ok(new Models.ApiResponse { status = 1, message = "OK" }); } } } UsersController.cs
ApiCustomException.cs
MiddleWares目录下的ApiCustomException.cs文件,是一个数据接口的统一异常处理中间件。
该文件整理并抄袭自:https://www.cnblogs.com/ShenNan/p/10197231.html
在此特别感谢一下作者的先行贡献,并请原谅我无耻的抄袭。
using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.DependencyInjection; namespace Jwt.Gateway.MiddleWares { //参考: https://www.cnblogs.com/ShenNan/p/10197231.html public enum ApiCustomExceptionHandleType { JsonHandle = 0, PageHandle = 1, Both = 2 } public class ApiCustomExceptionMiddleWareOption { public ApiCustomExceptionMiddleWareOption( ApiCustomExceptionHandleType handleType = ApiCustomExceptionHandleType.JsonHandle, IList<PathString> jsonHandleUrlKeys = null, string errorHandingPath = "") { HandleType = handleType; JsonHandleUrlKeys = jsonHandleUrlKeys; ErrorHandingPath = errorHandingPath; } public ApiCustomExceptionHandleType HandleType { get; set; } public IList<PathString> JsonHandleUrlKeys { get; set; } public PathString ErrorHandingPath { get; set; } } public class ApiCustomExceptionMiddleWare { private RequestDelegate _next; private ApiCustomExceptionMiddleWareOption _option; private IDictionary<int, string> _exceptionStatusCodeDic; public ApiCustomExceptionMiddleWare(RequestDelegate next, ApiCustomExceptionMiddleWareOption option) { _next = next; _option = option; _exceptionStatusCodeDic = new Dictionary<int, string> { { 401, "未授权的请求" }, { 404, "找不到该页面" }, { 403, "访问被拒绝" }, { 500, "服务器发生意外的错误" } //其余状态自行扩展 }; } public async Task Invoke(HttpContext context) { Exception exception = null; try { await _next(context); } catch (Exception ex) { context.Response.Clear(); context.Response.StatusCode = 200;//手动设置状态码(总是成功) exception = ex; } finally { if (_exceptionStatusCodeDic.ContainsKey(context.Response.StatusCode) && !context.Items.ContainsKey("ExceptionHandled")) { var errorMsg = string.Empty; if (context.Response.StatusCode == 500 && exception != null) { errorMsg = $"{_exceptionStatusCodeDic[context.Response.StatusCode]}\r\n{(exception.InnerException != null "; } else { errorMsg = _exceptionStatusCodeDic[context.Response.StatusCode]; } exception = new Exception(errorMsg); } if (exception != null) { var handleType = _option.HandleType; if (handleType == ApiCustomExceptionHandleType.Both) { var requestPath = context.Request.Path; handleType = _option.JsonHandleUrlKeys != null && _option.JsonHandleUrlKeys.Count( k => requestPath.StartsWithSegments(k, StringComparison.CurrentCultureIgnoreCase)) > 0 "application/json"; await context.Response.WriteAsync(serialzeStr, System.Text.Encoding.UTF8); } private async Task PageHandle(HttpContext context, Exception ex, PathString path) { context.Items.Add("Exception", ex); var originPath = context.Request.Path; context.Request.Path = path; try { await _next(context); } catch { } finally { context.Request.Path = originPath; } } } public static class ApiCustomExceptionMiddleWareExtensions { public static IApplicationBuilder UseApiCustomException(this IApplicationBuilder app, ApiCustomExceptionMiddleWareOption option) { return app.UseMiddleware<ApiCustomExceptionMiddleWare>(option); } } } ApiCustomException.cs
配置相关
appsettings.json
算法'HS256'要求SecurityKey.KeySize大于'128'位,所以JwtSecurityKey可不要太短了哦。
{ "Urls": "http://localhost:60000", "AllowedHosts": "*", "JwtSecurityKey": "areyouokhhhhhhhhhhhhhhhhhhhhhhhhhhh", "JwtTokenIssuer": "Jwt.Gateway", "JwtTokenAudience": "App" } appsettings.json
Startup.cs
关于JWT的配置可以在通过JwtBearerOptions加入一些自己的事件处理逻辑,共有4个事件可供调用:
OnAuthenticationFailed,OnMessageReceived,OnTokenValidated,OnChallenge, 本示例中是在OnTokenValidated中插入Token黑名单的校验逻辑。黑名单应该是Jwt应用场景中主动使Token过期的主流做法了。
using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Jwt.Gateway.MiddleWares; using Microsoft.Extensions.DependencyInjection; namespace Jwt.Gateway { public class Startup { private readonly Microsoft.Extensions.Configuration.IConfiguration _configuration; public Startup(Microsoft.Extensions.Configuration.IConfiguration configuration) { _configuration = configuration; } public void ConfigureServices(IServiceCollection services) { services.AddAuthentication(Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.Events = new Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerEvents { /*OnMessageReceived = context => { context.Token = context.Request.Query["access_token"]; return Task.CompletedTask; },*/ OnTokenValidated = context => { var token = ((System.IdentityModel.Tokens.Jwt.JwtSecurityToken)context.SecurityToken).RawData; if (InBlacklist(token)) { context.Fail("token in blacklist"); } return Task.CompletedTask; } }; options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters { ValidateIssuer = true, ValidateAudience = true, ValidateLifetime = true, ValidateIssuerSigningKey = true, ValidAudience = _configuration["JwtTokenAudience"], ValidIssuer = _configuration["JwtTokenIssuer"], IssuerSigningKey = new Microsoft.IdentityModel.Tokens.SymmetricSecurityKey(System.Text.Encoding.UTF8.GetBytes(_configuration["JwtSecurityKey"])) }; }); services.AddMvc().AddJsonOptions(option=> { option.SerializerSettings.DateFormatString = "yyyy-MM-dd HH:mm:ss.fff"; }); } public void Configure(IApplicationBuilder app, IHostingEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseApiCustomException(new ApiCustomExceptionMiddleWareOption( handleType: ApiCustomExceptionHandleType.Both, jsonHandleUrlKeys: new PathString[] { "/api" }, errorHandingPath: "/home/error")); app.UseAuthentication(); app.UseMvc(); } bool InBlacklist(string token) { //code: 实际项目中应该查询数据库或配置文件进行比对 return false; } } } Startup.cs
Program.cs
using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore; using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; namespace Jwt.Gateway { public class Program { public static void Main(string[] args) { BuildWebHost(args).Run(); } public static IWebHost BuildWebHost(string[] args) { var config = new ConfigurationBuilder() .SetBasePath(Directory.GetCurrentDirectory()) .AddJsonFile("appsettings.json", optional: true) .Build(); return WebHost.CreateDefaultBuilder(args) .UseKestrel() .UseConfiguration(config) .UseStartup<Startup>() .Build(); } } } Program.cs
运行截图
如果Token校验失败将会返回401错误!
总结
以上就是这篇文章的全部内容了,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,谢谢大家对的支持。
《魔兽世界》大逃杀!60人新游玩模式《强袭风暴》3月21日上线
暴雪近日发布了《魔兽世界》10.2.6 更新内容,新游玩模式《强袭风暴》即将于3月21 日在亚服上线,届时玩家将前往阿拉希高地展开一场 60 人大逃杀对战。
艾泽拉斯的冒险者已经征服了艾泽拉斯的大地及遥远的彼岸。他们在对抗世界上最致命的敌人时展现出过人的手腕,并且成功阻止终结宇宙等级的威胁。当他们在为即将于《魔兽世界》资料片《地心之战》中来袭的萨拉塔斯势力做战斗准备时,他们还需要在熟悉的阿拉希高地面对一个全新的敌人──那就是彼此。在《巨龙崛起》10.2.6 更新的《强袭风暴》中,玩家将会进入一个全新的海盗主题大逃杀式限时活动,其中包含极高的风险和史诗级的奖励。
《强袭风暴》不是普通的战场,作为一个独立于主游戏之外的活动,玩家可以用大逃杀的风格来体验《魔兽世界》,不分职业、不分装备(除了你在赛局中捡到的),光是技巧和战略的强弱之分就能决定出谁才是能坚持到最后的赢家。本次活动将会开放单人和双人模式,玩家在加入海盗主题的预赛大厅区域前,可以从强袭风暴角色画面新增好友。游玩游戏将可以累计名望轨迹,《巨龙崛起》和《魔兽世界:巫妖王之怒 经典版》的玩家都可以获得奖励。