# CustomAuthorization.WebApi **Repository Path**: xant77/CustomAuthorization.WebApi ## Basic Information - **Project Name**: CustomAuthorization.WebApi - **Description**: ASP.NET Core 项目简单实现身份验证及鉴权的例子。相关博客参见:https://www.cnblogs.com/wiseant/p/10515842.html - **Primary Language**: C# - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 33 - **Forks**: 15 - **Created**: 2019-03-12 - **Last Updated**: 2024-02-27 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # ASP.NET Core 身份验证及鉴权 **目录** + 项目准备 + 身份验证 - 定义基本类型和接口 - 编写验证处理器 - 实现用户身份验证 + 权限鉴定 - 思路 - 编写过滤器类及相关接口 - 实现属性注入 - 实现用户权限鉴定 + 测试 ## 环境 + VS 2017 + ASP.NET Core 2.2 ## 目标   以相对简单优雅的方式实现用户身份验证和鉴权,解决以下两个问题: + 无状态的身份验证服务,使用请求头附加访问令牌,几乎适用于手机、网页、桌面应用等所有客户端 + 基于功能点的权限访问控制,可以将任意功能点权限集合授予用户或角色,无需硬编码角色权限,非常灵活 ## 项目准备 1. 创建一个ASP.NET Core Web应用程序 + 使用ASP.NET Core 2.2 + 模板选[空] + 不启用HTTPS + 不进行身份验证 2. 通过NuGet安装`Swashbuckle.AspNetCore`程序包,并在Startup类中启用Swagger支持 *因为这个示例项目不打算编写前端网页,所以直接使用Swagger来调试,真的很方便。* 3. 添加一个空的MVC控制器(HomeController)和一个空的API控制器(AuthController) `HomeController.Index()`方法中只写一句简单的跳转代码即可: `return new RedirectResult("~/swagger");` `AuthController`类中随便写一两个骨架方法,方便看效果。 4. 运行项目,会自动打开浏览器并跳转到Swagger页面。 ## 身份验证 ### 定义基本类型和接口 1. ClaimTypes 定义一些常用的声明类型常量 2. IClaimsSession 表示当前会话信息的接口 3. ClaimsSession 会话信息实现类 根据声明类型从ClaimsPrincipal.ClaimsIdentity属性中读取用户ID、用户名等信息。 *实际项目中可从此类继承或完全重新实现自己的Session类,以添加更多的会话信息(例如工作部门)* 4. IToken 登录令牌接口 包含访问令牌、刷新令牌、令牌时效等令牌 5. IIdentity 身份证明接口 包含用户基本信息及令牌信息 6. IAuthenticationService 验证服务接口 抽象出来的验证服务接口,仅规定了四个身份验证相关的方法,如需扩展可定义由此接口派生的接口。 | 方法名 | 返回值类型 | 说明 | | -------------------------- | ---------- | ---------------------------------------------- | | Login(userName, password) | IIdentity | 根据用户名及密码验证其身份,成功则返回身份证明 | | Logout() | void | 注销本次登录,即使未登录也不报错 | | RefreshToken(refreshToken) | Token | 刷新登录令牌,如果当前用户未登录则报错 | | ValidateToken(accessToken) | IIdentity | 验证访问令牌,成功则返回身份证明 | 7. SimpleToken 登录令牌的简化实现 *这个类提不提供都可以,实际项目中大家生成Token的算法肯定是各不相同的,提供简单实现仅用于演示* ### 编写验证处理器 1. BearerDefaults 定义了一些与身份验证相关的常量 如:AuthenticationScheme 2. BearerOptions 身份验证选项类 从`AuthenticationSchemeOptions`继承而来 3. BearerValidatedContext 验证结果上下文 4. BearerHandler 身份验证处理器 <= **关键类** 覆盖了`HandleAuthenticateAsync()`方法,实现自定义的身份验证逻辑,简述如下: 1. 获取访问令牌。从请求头中获取`authorization`信息,如果没有则从请求的参数中获取 2. 如果访问令牌为空,则终止验证,但不报错,直接返回`AuthenticateResult.NoResult()` 3. 调用从构造函数注入的`IAuthenticationService`实例的`ValidateToken()`方法,验证访问令牌是否有效,如果该方法触发异常(例如令牌过期)则捕获后通过`AuthenticateResult.Fail()`返回错误信息,如果该方法返回值为空(例如访问令牌根本不存在)则返回`AuthenticateResult.NoResult()`,不报错。 4. 到这一步说明身份验证已经通过,而且拿到身份证明信息,根据该信息创建`Claim`数组,然后再创建一个包含这些`Claim`数据的`ClaimsPrincipal`实例,并将Thread.CurrentPrincipal设置为该实例。 **重点:**其实,`HttpContext.User`属性的类型正是`CurrentPrincipal`,而其值应该就是来自于`Thread.CurrentPrincipal`。 5. 构造`BearerValidatedContext`实例,并将其`Principal`属性赋值为上面创建的`ClaimsPrincipal`实例,然后调用`Success()`方法,表示验证成功。最后返回该实例的`Result`属性值。 5. BearerExtensions 包含一些扩展方法,提供使用便利 重点在于`AddBearer()`方法内调用`builder.AddScheme()`泛型方法时,分别使用了前面编写的`BearerOptions`、`BearerHandler`类作为泛型参数。 ```C# public static AuthenticationBuilder AddBearer(...) { return builder.AddScheme(...); } ``` *如果想要自己实现`BearerHandler`类的验证逻辑,可以抛弃此类,重新编写使用新Handler类的扩展方法* ### 实现用户身份验证 #### 说明   这部分是身份验证的落地,实际项目中应该将上面两步(定义基本类型和接口、编写验证处理器)的代码抽象出来,成为独立可复用的软件包,利用该软件包进行身份验证的实现逻辑可参照此示例代码。 #### 实现步骤 1. Identity 身份证明实现类 2. SampleAuthenticationService 验证服务的简单实现 *出于演示方便,固化了三个用户(admin/123456、user/123、tester/123)* 3. AuthController 通过HTTP向前端提供验证服务的控制器类 *提供了用户登录、令牌刷新、令牌验证等方法。* 4. 还需要修改项目中`Startup.cs`文件,添加依赖注入规则、身份验证,并启用身份验证中间件。 在`ConfigureServices`方法内添加代码: ``` //添加依赖注入规则 services.AddScoped(); services.AddScoped(); //添加身份验证 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]按钮,系统验证成功并返回身份证明信息。 ![](https://img2018.cnblogs.com/blog/19184/201903/19184-20190312150721993-2124207382.png) *记下访问令牌`2ad43df2c11d48a18a88441adbf4994a`和刷新令牌`9bbaf811ed8b4d29b638777d4f89238e`* + 测试刷新登录令牌 点击`/api/Auth/Refresh`方法,在输入框中输入上面获取到的刷新令牌`9bbaf811ed8b4d29b638777d4f89238e`,然后点击[Execute]按钮,返回401错误码。原因是因为我们并未提供访问令牌。 点击方法名右侧的[锁]图标,在弹出框中输入之前获取的访问令牌`2ad43df2c11d48a18a88441adbf4994a`并点击[Authorize]按钮后关闭对话框,重新点击[Execute]按钮,成功获取到新的登录令牌。 ![](https://img2018.cnblogs.com/blog/19184/201903/19184-20190312150846449-1823703499.png) + 测试验证访问令牌 点击`/api/Auth/Validate`方法,在输入框中输入第一次获取的到访问令牌`2ad43df2c11d48a18a88441adbf4994a`,然后点击[Execute]按钮,返回400错误码,表明发起的请求参数有误。*因为此方法是支持匿名访问的,所以错误码不会是401.* 将输入框内容修改为新的访问令牌`f37542e162ed4855921ddf26b05c3f25`,然后点击[Execute]按钮,验证成功,返回了对应的用户身份证明信息。 ![](https://img2018.cnblogs.com/blog/19184/201903/19184-20190312150905894-626492096.png) ## 权限鉴定   在ASP.NET Core项目中实现基于角色的授权很容易,在一些权限管理并不复杂的项目中,采取这种方式来实现权限鉴定简单可行。有兴趣可以参考这篇博文[ASP.NET Core 认证与授权5:初识授权](https://www.cnblogs.com/RainingNight/p/authorization-in-asp-net-core.html)   但是,对于稍微复杂一些的项目,权限划分又细又多,如果采用这种方式,要覆盖到各种各样的权限组合,需要在代码中定义相当多的角色,大大增加项目维护工作,并且很不灵活。   这里借鉴[ABP框架](https://aspnetboilerplate.com/)中权限鉴定的一些思想,来实现**基于功能点**的权限访问控制。   非常感谢ASP.NET Core和ABP等诸多优秀的开源项目,向你们致敬!   *不得不说ABP框架非常优秀,但是我并不喜欢使用它,因为我没有能力和精力搞清楚它的详细设计思路,而且很多功能我根本不需要。* ### 思路   ASP.NET Core提供了一个`IAuthorizationFilter`接口,如果在控制器类上添加[授权过滤]特性,相应的AuthorizationFilter类的`OnAuthorization()`方法会在控制器的`Action`之前运行,如果在该方法中设置AuthorizationFilterContext.Result为一个错误的response,`Action`将不会被调用。 基于这个思路,我们设计了以下方案: 1. 编写一个Attribute(特性)类,包含以下两个属性: Permissions:需要检查的权限数组 RequireAllPermissions:是否需要拥有数组中全部权限,如果为否则拥有任一权限即可 2. 定义一个`IPermissionChecker`接口,在接口中定义`IsGrantedAsync()`方法,用于执行权限鉴定逻辑 3. 编写一个AuthorizationFilterAttribute特性类(应用目标为class),通过属性注入`IPermissionChecker`实例。然后在`OnAuthorization()`方法内调用`IPermissionChecker`实例的`IsGrantedAsync()`方法,如果该方法返回值为false,则返回403错误,否则正常放行。 ### 编写过滤器类及相关接口 1. ApiAuthorizeAttribute类 ```c# [AttributeUsage(AttributeTargets.Method)] public class ApiAuthorizeAttribute : Attribute, IFilterMetadata { public string[] Permissions { get; } public bool RequireAllPermissions { get; set; } public ApiAuthorizeAttribute(params string[] permissions) { Permissions = permissions; } } ``` 2. IPermissionChecker接口定义 ```c# public interface IPermissionChecker { Task IsGrantedAsync(string permissionName); } ``` 3. AuthorizationFilterAttribute类 ```c# [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 }; } } ``` 4. 配合属性注入提供NullPermissionChecker类,在`IsGrantedAsync()`方法内直接返回true。 ### 实现属性注入   做好上面的准备,我们应该可以开始着手在项目内应用权限鉴定功能了,不过ASP.NET Core内置的DI框架并不支持属性注入,所以还得添加属性注入的功能。 1. 定义InjectionAttribute类,用于显式声明应用了此特性的属性将使用依赖注入 ```c# /// /// 在属性上添加此特性,以声明该属性需要使用依赖注入 /// [AttributeUsage(AttributeTargets.Property)] public class InjectionAttribute : Attribute { } ``` 2. 添加一个`PropertiesAutowiredFilterProvider`类,从`DefaultFilterProvider`类派生 ```c# public class PropertiesAutowiredFilterProvider : DefaultFilterProvider { private static IDictionary> _publicPropertyCache = new Dictionary>(); 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() != 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); } } } ``` 3. 还有非常关键的一步,在`Startup.ConfigureServices()`中添加下面的代码,替换`IFilterProvider`接口的实现类为上面编写的`PropertiesAutowiredFilterProvider`类 ```c# services.Replace(ServiceDescriptor.Singleton()); ``` ### 实现用户权限鉴定   终于,我们可以在项目内应用权限鉴定功能了。 #### 编码 1. 首先,我们定义一些功能点权限常量 ```c# public static class PermissionNames { public const string TestAdd = "Test.Add"; public const string TestEdit = "Test.Edit"; public const string TestDelete = "Test.Delete"; } ``` 2. 接着,添加一个新的用于测试的控制器类 ```c# [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`。 3. 实现一个用于演示的权限检查器类 ```c# public class SamplePermissionChecker : IPermissionChecker { private readonly Dictionary userPermissions = new Dictionary { //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 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); } } ``` 4. 最后还需要修改项目中`Startup.cs`文件,添加依赖注入规则 ```c# services.AddSingleton(); ``` 因为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* + 测试结果 三个测试程序从后台服务所获取到的当前用户信息完成匹配。 ![](https://img2018.cnblogs.com/blog/19184/201903/19184-20190312150938001-366760239.png) ### 测试以不同用户身份访问需要权限的接口 + 测试方法 预设的权限为:admin=>全部权限,user=>除`Test.Add`以外权限,tester=>无。 分别以admin、user、tester三个用户身份请求`/api/test`下的所有接口,并模拟令牌过期的场景。 + 测试结果 可以见到,以过期的令牌发起请求时,后台返回的状态为Unauthorized,当用户未获得足够的授权时后台返回的状态为Forbidden。 测试通过! ![](https://img2018.cnblogs.com/blog/19184/201903/19184-20190312150959365-319178246.png) ## 最后 原文发表在博客园:https://www.cnblogs.com/wiseant/p/10515842.html **欢迎转载,请在明显位置给出出处及链接**。