ASP.NET Core 项目简单实现身份验证及鉴权
ASP.NET Core 身份验证及鉴权
目录
环境
- VS 2017
- ASP.NET Core 2.2
目标
以相对简单优雅的方式实现用户身份验证和鉴权,解决以下两个问题:
- 无状态的身份验证服务,使用请求头附加访问令牌,几乎适用于手机、网页、桌面应用等所有客户端
- 基于功能点的权限访问控制,可以将任意功能点权限集合授予用户或角色,无需硬编码角色权限,非常灵活
项目准备
创建一个ASP.NET Core Web应用程序
- 使用ASP.NET Core 2.2
- 模板选[空]
- 不启用HTTPS
- 不进行身份验证
通过NuGet安装
Swashbuckle.AspNetCore
程序包,并在Startup类中启用Swagger支持因为这个示例项目不打算编写前端网页,所以直接使用Swagger来调试,真的很方便。
添加一个空的MVC控制器(HomeController)和一个空的API控制器(AuthController)
HomeController.Index()
方法中只写一句简单的跳转代码即可:return new RedirectResult("~/swagger");
AuthController
类中随便写一两个骨架方法,方便看效果。运行项目,会自动打开浏览器并跳转到Swagger页面。
身份验证
定义基本类型和接口
ClaimTypes 定义一些常用的声明类型常量
IClaimsSession 表示当前会话信息的接口
ClaimsSession 会话信息实现类
根据声明类型从ClaimsPrincipal.ClaimsIdentity属性中读取用户ID、用户名等信息。实际项目中可从此类继承或完全重新实现自己的Session类,以添加更多的会话信息(例如工作部门)
IToken 登录令牌接口
包含访问令牌、刷新令牌、令牌时效等令牌IIdentity 身份证明接口
包含用户基本信息及令牌信息IAuthenticationService 验证服务接口
抽象出来的验证服务接口,仅规定了四个身份验证相关的方法,如需扩展可定义由此接口派生的接口。方法名 返回值类型 说明 Login(userName, password) IIdentity 根据用户名及密码验证其身份,成功则返回身份证明 Logout() void 注销本次登录,即使未登录也不报错 RefreshToken(refreshToken) Token 刷新登录令牌,如果当前用户未登录则报错 ValidateToken(accessToken) IIdentity 验证访问令牌,成功则返回身份证明 SimpleToken 登录令牌的简化实现
这个类提不提供都可以,实际项目中大家生成Token的算法肯定是各不相同的,提供简单实现仅用于演示
编写验证处理器
BearerDefaults 定义了一些与身份验证相关的常量
如:AuthenticationScheme
BearerOptions 身份验证选项类
从
AuthenticationSchemeOptions
继承而来BearerValidatedContext 验证结果上下文
BearerHandler 身份验证处理器 <= 关键类
覆盖了
HandleAuthenticateAsync()
方法,实现自定义的身份验证逻辑,简述如下:获取访问令牌。从请求头中获取
authorization
信息,如果没有则从请求的参数中获取如果访问令牌为空,则终止验证,但不报错,直接返回
AuthenticateResult.NoResult()
调用从构造函数注入的
IAuthenticationService
实例的ValidateToken()
方法,验证访问令牌是否有效,如果该方法触发异常(例如令牌过期)则捕获后通过AuthenticateResult.Fail()
返回错误信息,如果该方法返回值为空(例如访问令牌根本不存在)则返回AuthenticateResult.NoResult()
,不报错。到这一步说明身份验证已经通过,而且拿到身份证明信息,根据该信息创建
Claim
数组,然后再创建一个包含这些Claim
数据的ClaimsPrincipal
实例,并将Thread.CurrentPrincipal设置为该实例。重点:其实,
HttpContext.User
属性的类型正是CurrentPrincipal
,而其值应该就是来自于Thread.CurrentPrincipal
。构造
BearerValidatedContext
实例,并将其Principal
属性赋值为上面创建的ClaimsPrincipal
实例,然后调用Success()
方法,表示验证成功。最后返回该实例的Result
属性值。
BearerExtensions 包含一些扩展方法,提供使用便利
重点在于
AddBearer()
方法内调用builder.AddScheme<TOptions,THandler>()
泛型方法时,分别使用了前面编写的BearerOptions
、BearerHandler
类作为泛型参数。public static AuthenticationBuilder AddBearer(...)
{
return builder.AddScheme<BearerOptions, BearerHandler>(...);
}
如果想要自己实现
BearerHandler
类的验证逻辑,可以抛弃此类,重新编写使用新Handler类的扩展方法
实现用户身份验证
说明
这部分是身份验证的落地,实际项目中应该将上面两步(定义基本类型和接口、编写验证处理器)的代码抽象出来,成为独立可复用的软件包,利用该软件包进行身份验证的实现逻辑可参照此示例代码。
实现步骤
Identity 身份证明实现类
SampleAuthenticationService 验证服务的简单实现
出于演示方便,固化了三个用户(admin/123456、user/123、tester/123)
AuthController 通过HTTP向前端提供验证服务的控制器类
提供了用户登录、令牌刷新、令牌验证等方法。
还需要修改项目中
Startup.cs
文件,添加依赖注入规则、身份验证,并启用身份验证中间件。
在ConfigureServices
方法内添加代码://添加依赖注入规则
services.AddScoped<IClaimsSession, ClaimsSession>();
services.AddScoped<IAuthenticationService, SampleAuthenticationService>();
//添加身份验证
services.AddAuthentication(options =>
{
options.DefaultAuthenticateScheme = BearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme = BearerDefaults.AuthenticationScheme;
}).AddBearer();
在
Configure()
方法内添加代码://启用身份验证中间件
app.UseAuthentication();
通过Swagger测试
测试登录功能
启动项目,自动进入[Swagger UI]界面,点击
/api/Auth/Login
方法,不修改输入框中的内容直接点击[Execute]按钮,可以见到返回401错误码。在输入框中输入
{"userName": "admin", "password": "123456"}
,然后点击[Execute]按钮,系统验证成功并返回身份证明信息。
记下访问令牌2ad43df2c11d48a18a88441adbf4994a
和刷新令牌9bbaf811ed8b4d29b638777d4f89238e
测试刷新登录令牌
点击
/api/Auth/Refresh
方法,在输入框中输入上面获取到的刷新令牌9bbaf811ed8b4d29b638777d4f89238e
,然后点击[Execute]按钮,返回401错误码。原因是因为我们并未提供访问令牌。点击方法名右侧的[锁]图标,在弹出框中输入之前获取的访问令牌
2ad43df2c11d48a18a88441adbf4994a
并点击[Authorize]按钮后关闭对话框,重新点击[Execute]按钮,成功获取到新的登录令牌。
测试验证访问令牌
点击
/api/Auth/Validate
方法,在输入框中输入第一次获取的到访问令牌2ad43df2c11d48a18a88441adbf4994a
,然后点击[Execute]按钮,返回400错误码,表明发起的请求参数有误。因为此方法是支持匿名访问的,所以错误码不会是401.将输入框内容修改为新的访问令牌
f37542e162ed4855921ddf26b05c3f25
,然后点击[Execute]按钮,验证成功,返回了对应的用户身份证明信息。
权限鉴定
在ASP.NET Core项目中实现基于角色的授权很容易,在一些权限管理并不复杂的项目中,采取这种方式来实现权限鉴定简单可行。有兴趣可以参考这篇博文ASP.NET Core 认证与授权5:初识授权
但是,对于稍微复杂一些的项目,权限划分又细又多,如果采用这种方式,要覆盖到各种各样的权限组合,需要在代码中定义相当多的角色,大大增加项目维护工作,并且很不灵活。
这里借鉴ABP框架中权限鉴定的一些思想,来实现基于功能点的权限访问控制。
非常感谢ASP.NET Core和ABP等诸多优秀的开源项目,向你们致敬!
不得不说ABP框架非常优秀,但是我并不喜欢使用它,因为我没有能力和精力搞清楚它的详细设计思路,而且很多功能我根本不需要。
思路
ASP.NET Core提供了一个IAuthorizationFilter
接口,如果在控制器类上添加[授权过滤]特性,相应的AuthorizationFilter类的OnAuthorization()
方法会在控制器的Action
之前运行,如果在该方法中设置AuthorizationFilterContext.Result为一个错误的response,Action
将不会被调用。
基于这个思路,我们设计了以下方案:
编写一个Attribute(特性)类,包含以下两个属性:
Permissions:需要检查的权限数组
RequireAllPermissions:是否需要拥有数组中全部权限,如果为否则拥有任一权限即可
定义一个
IPermissionChecker
接口,在接口中定义IsGrantedAsync()
方法,用于执行权限鉴定逻辑编写一个AuthorizationFilterAttribute特性类(应用目标为class),通过属性注入
IPermissionChecker
实例。然后在OnAuthorization()
方法内调用IPermissionChecker
实例的IsGrantedAsync()
方法,如果该方法返回值为false,则返回403错误,否则正常放行。
编写过滤器类及相关接口
ApiAuthorizeAttribute类
[AttributeUsage(AttributeTargets.Method)]
public class ApiAuthorizeAttribute : Attribute, IFilterMetadata
{
public string[] Permissions { get; } public bool RequireAllPermissions { get; set; } public ApiAuthorizeAttribute(params string[] permissions)
{
Permissions = permissions;
}
}
IPermissionChecker接口定义
public interface IPermissionChecker
{
Task<bool> IsGrantedAsync(string permissionName);
}
AuthorizationFilterAttribute类
[AttributeUsage(AttributeTargets.Class)]
public class AuthorizationFilterAttribute : Attribute, IAuthorizationFilter
{
[Injection] //属性注入
public IPermissionChecker PermissionChecker { get; set; } = NullPermissionChecker.Instance; public void OnAuthorization(AuthorizationFilterContext context)
{
if(存在[AllowAnonymous]特性) return;
var authorizeAttribute = 从context.Filters中析出ApiAuthorizeAttribute
foreach (var permission in authorizeAttribute.Permissions)
{
//检查各项权限
var granted = PermissionChecker.IsGrantedAsync(permission).Result;
}
if(检查未通过)
context.Result = new ObjectResult("未授权") { StatusCode = 403 };
}
}
配合属性注入提供NullPermissionChecker类,在
IsGrantedAsync()
方法内直接返回true。
实现属性注入
做好上面的准备,我们应该可以开始着手在项目内应用权限鉴定功能了,不过ASP.NET Core内置的DI框架并不支持属性注入,所以还得添加属性注入的功能。
定义InjectionAttribute类,用于显式声明应用了此特性的属性将使用依赖注入
/// <summary>
/// 在属性上添加此特性,以声明该属性需要使用依赖注入
/// </summary>
[AttributeUsage(AttributeTargets.Property)]
public class InjectionAttribute : Attribute { }
添加一个
PropertiesAutowiredFilterProvider
类,从DefaultFilterProvider
类派生public class PropertiesAutowiredFilterProvider : DefaultFilterProvider
{
private static IDictionary<string, IEnumerable<PropertyInfo>> _publicPropertyCache = new Dictionary<string, IEnumerable<PropertyInfo>>(); public override void ProvideFilter(FilterProviderContext context, FilterItem filterItem)
{
base.ProvideFilter(context, filterItem); //在调用基类方法之前filterItem变量不会有值
var filterType = filterItem.Filter.GetType();
if (!_publicPropertyCache.ContainsKey(filterType.FullName))
{
var ps=filterType.GetProperties(BindingFlags.Public|BindingFlags.Instance)
.Where(c => c.GetCustomAttribute<InjectionAttribute>() != null);
_publicPropertyCache[filterType.FullName] = ps;
} var injectionProperties = _publicPropertyCache[filterType.FullName];
if (injectionProperties?.Count() == 0)
return;
//下面是注入属性实例的关键代码
var serviceProvider = context.ActionContext.HttpContext.RequestServices;
foreach (var item in injectionProperties)
{
var service = serviceProvider.GetService(item.PropertyType);
if (service == null)
{
throw new InvalidOperationException($"Unable to resolve service for type '{item.PropertyType.FullName}' while attempting to activate '{filterType.FullName}'");
}
item.SetValue(filterItem.Filter, service);
}
}
}
还有非常关键的一步,在
Startup.ConfigureServices()
中添加下面的代码,替换IFilterProvider
接口的实现类为上面编写的PropertiesAutowiredFilterProvider
类services.Replace(ServiceDescriptor.Singleton<Microsoft.AspNetCore.Mvc.Filters.IFilterProvider, PropertiesAutowiredFilterProvider>());
实现用户权限鉴定
终于,我们可以在项目内应用权限鉴定功能了。
编码
首先,我们定义一些功能点权限常量
public static class PermissionNames
{
public const string TestAdd = "Test.Add";
public const string TestEdit = "Test.Edit";
public const string TestDelete = "Test.Delete";
}
接着,添加一个新的用于测试的控制器类
[AuthorizationFilter]
[Route("api/[controller]")]
[ApiController]
public class TestController : ControllerBase
{
[Injection]
public IClaimsSession Session { get; set; } [HttpGet]
[Route("[action]")]
public IActionResult CurrentUser() => Ok(Session?.UserName); [ApiAuthorize]
[HttpGet("{id}")]
public IActionResult Get(int id)=> Ok(id); [ApiAuthorize(PermissionNames.TestAdd)]
[HttpPost]
[Route("[action]")]
public IActionResult Create()=> Ok(); [ApiAuthorize(PermissionNames.TestEdit, RequireAllPermissions = false)]
[HttpPost]
[Route("[action]")]
public IActionResult Update()=> Ok(); [ApiAuthorize(PermissionNames.TestAdd, PermissionNames.TestEdit, RequireAllPermissions = false)]
[HttpPost]
[Route("[action]")]
public IActionResult Patch() => Ok(); [ApiAuthorize(PermissionNames.TestDelete)]
[HttpDelete("{id}")]
public IActionResult Delete(int id) => Ok();
}
在控制器类上添加了[AuthorizationFilter]特性,除了
CurrentUser()
方法以外,都添加了[ApiAuthorize]特性,所需的权限各不相同,为简化测试所有的Action
都直接返回OkResult
。实现一个用于演示的权限检查器类
public class SamplePermissionChecker : IPermissionChecker
{
private readonly Dictionary<long, string[]> userPermissions = new Dictionary<long, string[]>
{
//Id=1的用户具有Test模块的全部功能
{ 1, new[] { PermissionNames.TestAdd, PermissionNames.TestEdit, PermissionNames.TestDelete } },
//Id=2的用户具有Test模块的编辑和删除功能
{ 2, new[] { PermissionNames.TestEdit, PermissionNames.TestDelete } }
}; public IClaimsSession Session { get; } //通过构造函数注入IClaimsSession实例,以便在权限鉴定方法中获取用户信息
public SamplePermissionChecker(IClaimsSession session)
{
this.Session = session;
} public Task<bool> IsGrantedAsync(string permissionName)
{
if(!userPermissions.Any(p => p.Key == Session.UserId))
return Task.FromResult(false);
var up = userPermissions.Where(p => p.Key == Session.UserId).First();
var granted = up.Value.Any(permission => permission.Equals(permissionName, StringComparison.InvariantCultureIgnoreCase));
return Task.FromResult(granted);
} }
最后还需要修改项目中
Startup.cs
文件,添加依赖注入规则services.AddSingleton<IPermissionChecker, SamplePermissionChecker>();
因为SamplePermissionChecker类中并没有需要进程间隔离的数据,所以使用单例模式注册就可以了。不过这样一来,因为该类通过构造函数注入了
IClaimsSession
接口实例,在构建Checker类实例时将触发异常。考虑到CliamsSession
类中只有方法没有数据 ,改为单例也并无妨,于是将该接口也改为单例模式注册。
通过Swagger测试
测试未登录时仅可访问
/api/Test/CurrentUser
测试以用户user登录,可以访问
/api/Test/CurrentUser
和GET请求/api/Test/{id}
测试以用户admin登录,可以访问除
/api/Test/Add
以外的接口
测试
编写了命令行程序,用来测试前面实现的Web API服务。
测试不同用户同时访问时Session是否正确
测试方法
同时运行三个测试程序,都选择[测试身份验证],然后分别输入不同的用户身份序号,快速切换三个程序并按下回车键,三个测试程序会各自发起100次请求,每次请求间隔100毫秒。
例如同时打开三个命令行终端执行:dotnet .\CustomAuthorization.test.dll
测试结果
三个测试程序从后台服务所获取到的当前用户信息完全匹配。
测试以不同用户身份访问需要权限的接口
测试方法
预设的权限为:admin=>全部权限,user=>除
Test.Add
以外权限,tester=>无。分别以admin、user、tester三个用户身份请求
/api/test
下的所有接口,并模拟令牌过期的场景。测试结果
可以见到,以过期的令牌发起请求时,后台返回的状态为Unauthorized,当用户未获得足够的授权时后台返回的状态为Forbidden。
测试通过!
重要更新
在实际生产环境中,往往会在控制器方法中调用异步方法来提高并发,例如异步发消息、异步写文件等等。
但是这样一来,之前在HandleAuthenticateAsync()
方法中通过Thread.CurrentPrincipal
属性来保存用户信息的做法就行不通了,因为在调用异步方法后,当前线程已经被改变了。如果获取到的线程是新创建的还好,顶多是Thread.CurrentPrincipal
属性为null
,获取用户失败而已;要是万一从线程池拿到是另一次会话保存的用户信息,那就会发生严重的BUG,导致用户信息混乱。
好在ASP.NET Core提供了另一种获取HTTP上下文的方法,通过注入IHttpContextAccessor
实例,可以读取HttpContextAccessor.HttpContext.User
属性值,也可以修改该属性值,而且不受线程切换的影响。
所以,修改所有读取Thread.CurrentPrincipal
及为该属性赋值的代码,替换为HttpContextAccessor.HttpContext.User
。
ASP.NET Core 2.2之后,必须调用services.AddHttpContextAccessor()
才能注入IHttpContextAccessor实例
从源码仓库中签出一份代码,打开
.\src\Http\Http\src\HttpServiceCollectionExtensions.cs
文件,可以见到代码:services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
原来HttpContextAccessor是以单例模式注册的,所以就能在多线程之间共享同一实例了。
但是它是如何做到不同会话之间隔离的呢(也就是每次请求的HttpContext实例其实不同),通过查看HttpContextAccessor.cs
代码,发现奥秘就在new AsyncLocal<HttpContextHolder>()
。
之后在TestController
控制器中增加了一个AsyncTest
接口方法,进行了多次测试,具体见下图:
可以见到,在异步方法内,使用await调用了两次异步方法,结果发现经过两次异步调用后,当前线程有时会与第一个异步方法内的线程相同,有时会不同,带有一定的随机性。所以千万不能依赖某些表面上看来合理的规律,使用多线程得非常小心,多尝试多总结,做到安全稳定。
最后
源代码托管在gitee.com
欢迎转载,请在明显位置给出出处及链接。
ASP.NET Core 项目简单实现身份验证及鉴权的更多相关文章
- asp.net core 3.1多种身份验证方案,cookie和jwt混合认证授权
开发了一个公司内部系统,使用asp.net core 3.1.在开发用户认证授权使用的是简单的cookie认证方式,然后开发好了要写几个接口给其它系统调用数据.并且只是几个简单的接口不准备再重新部署一 ...
- 坎坷路:ASP.NET Core 1.0 Identity 身份验证(中集)
上一篇:<坎坷路:ASP.NET 5 Identity 身份验证(上集)> ASP.NET Core 1.0 什么鬼?它是 ASP.NET vNext,也是 ASP.NET 5,以后也可能 ...
- asp.net core中使用cookie身份验证
配置 在 Startup.ConfigureServices 方法中,创建具有 AddAuthentication 和 AddCookie 方法的身份验证中间件服务: services.AddAuth ...
- Docker + Jenkins 持续部署 ASP.NET Core 项目
Docker 是个好东西,特别是用它来部署 ASP.NET Core Web 项目的时候,但是仅仅的让程序运行起来远远不能满足我的需求,如果能够像 DaoCloud 提供的持续集成服务那样,检测 gi ...
- ASP.NET Core 项目配置 ( Startup ) - ASP.NET Core 基础教程 - 简单教程,简单编程
原文:ASP.NET Core 项目配置 ( Startup ) - ASP.NET Core 基础教程 - 简单教程,简单编程 ASP.NET Core 项目配置 ( Startup ) 前面几章节 ...
- asp.net 简单的身份验证
1 通常我们希望已经通过身份验证的才能够登录到网站的后台管理界面,对于asp.net 介绍一种简单的身份验证方式 首先在webconfig文件中添加如下的代码 <!--身份验证--> &l ...
- 定制Asp.NET 5 MVC内建身份验证机制 - 基于自建SQL Server用户/角色数据表的表单身份验证
背景 在需要进行表单认证的Asp.NET 5 MVC项目被创建后,往往需要根据项目的实际需求做一系列的工作对MVC 5内建的身份验证机制(Asp.NET Identity)进行扩展和定制: Asp.N ...
- 关于ASP.Net Core Web及API身份认证的解决方案
6月15日,在端午节前的最后一个工作日,想起有段日子没有写过文章了,倒有些荒疏了.今借夏日蒸蒸之气,偷得浮生半日悠闲.闲话就说到这里吧,提前祝大家端午愉快(屈原听了该不高兴了:))!.NetCore自 ...
- 《ASP.NET Core项目开发实战入门》带你走进ASP.NET Core开发
<ASP.NET Core项目开发实战入门>从基础到实际项目开发部署带你走进ASP.NET Core开发. ASP.NET Core项目开发实战入门是基于ASP.NET Core 3.1 ...
随机推荐
- 文件系统的描述信息-/etc/fstab
/etc/fstab文件包含众多文件系统的描述信息.文件中每一行为一个文件系统的描述,每行的选项之间通过tab分隔,#开头的行会被转换为注释,空白行会被忽略./etc/fstab文件中的设备顺序很重要 ...
- 嵌入式linux——说明(零)
之前就学习过嵌入式linux,但是那时候并没有完全投入,学习的也不科学系统,没有笔记,也没有自己写很多的代码来练习,所以到现在是基本归零了,现在比较有富裕的时间来系统的学习,从今天开始要克服每一个学习 ...
- 魔力Python——对象
Python之中,一切皆对象. 本文分为4部分: 1. 面向对象:初识 2. 面向对象:进阶 3. 面向对象:三大特性----继承,多态,封装 4. 面向对象:反射 0. 楔子 面向过程和面向对象的是 ...
- jeecg-boot 简易部署方案
jeecg-boot采用前后端分离的方案,前后端代码不在一起.想要部署 一般是通过反向代理实现. jeecg-boot目前支持更好更简单的解决方案: jeecg 在配置文件里面指定了 webapp的存 ...
- k3生成解决方案时错误处理
F6一键生成,会出现进程使用的错误,关掉了游览器,bos设计器,以及重启了本机iis站点,都没解决,打开任务管理器发现,bos.ide没有关掉
- python之路:数据类型初识
python开发之路:数据类型初识 数据类型非常重要.不过我这么说吧,他不重要我还讲个屁? 好,既然有人对数据类型不了解,我就讲一讲吧.反正这东西不需要什么python代码. 数据类型我讲的很死板.. ...
- MySQL复制相关技术的简单总结
MySQL有很多种复制,至少从概念上来看,传统的主从复制,半同步复制,GTID复制,多线程复制,以及组复制(MGR).咋一看起来很多,各种各样的复制,其实从原理上看,各种复制的原理并无太大的异同.每一 ...
- linux 文件压缩与解压
zip格式: zip -r(源文件是目录) [目标文件] [源文件] unzip -d [解压到的目录] [要解压的文件] gz格式: gzip [源文件] #会删除源文件 gzip -c [源文 ...
- Python来袭,教你用Neo4j构建“复联4”人物关系图谱!
来源商业新知网,原标题:Python来袭,教你用Neo4j构建“复联4”人物关系图谱!没有剧透! 复仇者联盟 之绝对不剧透 漫威英雄们为了不让自己剧透也是使出了浑身解数.在洛杉矶全球首映礼上记者费尽心 ...
- Linux内核d_path函数应用的经验总结
问题背景 一个内核模块中,需要通过d_path接口获取文件的路径,然后与目标文件白名单做匹配. 在生产环境中,获取的文件是存在的,但是与文件白名单中的文件总是匹配失败. 问题定位: 通过打印d_pat ...