理解 ASP.NET Core 自定义身份认证与授权
一、身份认证
1.1 什么是身份认证?
初学者很难理解身份认证。微软官方文档只有简单一句话:身份验证是确定用户身份的过程。
以笔者的理解,顾名思义,身份认证就是对一个访问者的身份判别、识别、辨识的过程。识别一个访问者,要么识别出来了,你是某某某,要么没识别出来,你是陌生人,至于怎么识别,就是身份认证的事情。
举个例子:
1)我(访问者)要去一栋大楼(应用系统)某个会议室开会。
2)门口保安(身份认证中间件)拦住我。
3)保安(身份认证中间件)不认识我,身份认证失败。至于我能否进去是授权的事了,这点稍后再叙。
4)这时,我拿出了我的邀请函(访问令牌),保安一看,哦,原来是参加会议的人员,随即登记了我的信息(生成身份认证成功结果--身份认证票据)并给了我一张凭票(访问票据)。
5)票上面有门牌号、访问事由、我的姓名、电话等信息(身份证明、身份信息声明信息)。
至此,身份认证过程就结束了。
这个例子中,身份识别主要是保安做的事情,我是领导,保安认识我。保安见我眼生,不认识我,但是我有工牌或邀请函,他也会成功识别我。假如我啥都没有,我就是陌生人,匿名访问者。
1.2 登录与身份认证的关系?
初学者很容易被登录和身份认证框架绕晕,笔者也是如此。
其实登录和身份认证没有关系。
身份认证是针对所有请求,登录只是其中一个请求,只不过这个请求的功能是拿访问令牌。
再接着上面的例子举例:
4)保安不认识我的情况下,根据领导规定(授权方案),不认识的人一律不能进入大楼(授权策略)。
5)我没办法,我给大楼领导打了电话,领导微信给我发了邀请函。(这个过程就是登录过程,登录授权策略要求可以匿名访问,即在没有票据的情况下访问)
如果我不知道领导电话,电话都打不进去,那么我就别想进了,我就不能在大楼办业务。
说到底,登录也是一次普通访问,也要经历身份认证、授权的过程,至于认证和授权细节,由方案而定。
比如,领导规定:成年人才能访问这个网站,哦不。。。。这栋大楼。
这时,保安拦住我不让我进去,我给领导打电话,领导晓得我不是成年人,不给我发送邀请函,我也进不去。(这就是登录接口的授权策略)
二、授权
2.1 什么是授权?
身份认证负责识别,至于能不能进,是授权的事情。 微软官方文档就只有一句话:授权是确定用户是否有权访问资源的过程。
比较形象的说,授权就是用身份认证的结果(无结果、成功、失败),来判定访问者是否可以进入的过程。
接着前面的例子:
5)会议规定(授权方案):必须要凭参会票据才能进入会场,参加会议(授权策略)。
6)我拿着保安给的票据,就进入会议室了。
7)假设我拿的不是邀请函,我拿的是工作牌,保安也认得我,给我登记的是工作凭据,但是不符合会议规定(禁止、ForBidden、无权限参加),所以不能参会,保安不会让我进去,直接就把我处理了。
8)假设我是陌生人,保安认不出我,我偏偏要参会(这就是‘挑战’),保安肯定不会让我进去的,保安(身份认证方案)会处理我。
有的小伙伴就说了,我直接拿邀请函进去不就行了嘛,为啥还要换票据呢,不是多此一举嘛?
这个问题就是,为什么要把身份认证和授权分开的问题。
笔者认为,是为了将组件进行解耦,方便更好的扩展和维护。不一定是Http服务程序,其他类型的程序也有用到身份认证和授权。
身份验证和授权之间,通过实体人(身份票据)进行关联。身份验证生成身份票据,授权则根据实体人的信息进行相应的策略规定。
官方文档是这样叙述的:
授权是指判断用户可执行的操作的过程。授权与身份验证相互独立。 但是,授权需要一种身份验证机制。 身份验证是确定用户标识的一个过程。 身份验证可为当前用户创建一个或多个标识。
三、结合代码理解,自定义身份验证方案
本文代码示例环境:Asp.NetCore 7
这里引用官方文档的叙述:
3.1 身份验证
身份验证处理程序:
- 是一种实现方案行为的类型。
- 派生自 IAuthenticationHandler 或 AuthenticationHandler<TOptions>。
- 具有对用户进行身份验证的主要责任。
根据身份验证方案的配置和传入的请求上下文,身份验证处理程序:
- 构造表示用户身份的 AuthenticationTicket 对象(若身份验证成功)。
- 返回“无结果”或“失败”(若身份验证失败)。
- 具有用于挑战和禁止操作的方法,供用户在下述情况下访问资源时使用:
- 他们未获得访问授权(禁止)。
- 他们未经过身份验证(挑战)。
1 using Microsoft.AspNetCore.Authentication; 2 using Microsoft.Extensions.Options; 3 using System.Security.Claims; 4 using System.Text.Encodings.Web; 5 6 namespace WebApplication1.Authentication 7 { 8 9 10 11 /// <summary> 12 /// 自定义身份认证方案 13 /// </summary> 14 public class CustomAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions> 15 { 16 public CustomAuthenticationHandler(IOptionsMonitor<AuthenticationSchemeOptions> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock) : base(options, logger, encoder, clock) 17 { 18 } 19 20 21 /// <summary> 22 /// 身份验证处理 主要逻辑 23 /// </summary> 24 /// <returns></returns> 25 /// <exception cref="NotImplementedException"></exception> 26 protected async override Task<AuthenticateResult> HandleAuthenticateAsync() 27 { 28 var token = Request.Headers["authentication-token"].ToString(); 29 30 //无结果,当然这里也可以作为验证失败 31 if (string.IsNullOrEmpty(token)) 32 { 33 var noResult = AuthenticateResult.NoResult(); 34 return await Task.FromResult(noResult); 35 } 36 37 //身份验证成功 38 //这里有个复杂的验证过程,通常是加解密操作 39 //也可以从数据源读取来判断,自行研究 40 //这里以相等来举例 41 if ("12932894384934".Equals(token)) 42 { 43 44 //通过复杂的判断,这个人是个老师 45 var principal = new ClaimsPrincipal(); 46 principal.AddIdentity(new ClaimsIdentity(new List<Claim> 47 { 48 //添加老师的角色 49 new Claim(ClaimTypes.Role, "Teacher") 50 })); 51 52 var success = AuthenticateResult.Success(new AuthenticationTicket(principal, "CustomAuthentication")); 53 return await Task.FromResult(success); 54 } 55 56 57 //身份验证失败 58 var fail = AuthenticateResult.Fail(new Exception("token错误!")); 59 return await Task.FromResult(fail); 60 } 61 62 63 /// <summary> 64 /// 处理挑战 主要逻辑 65 /// </summary> 66 /// <param name="properties"></param> 67 /// <returns></returns> 68 protected override async Task HandleChallengeAsync(AuthenticationProperties properties) 69 { 70 //这里可以拿到认证结果 71 var result = await HandleAuthenticateOnceSafeAsync(); 72 73 //默认返回401状态代码 74 await base.HandleChallengeAsync(properties); 75 } 76 77 78 /// <summary> 79 /// 处理禁止 主要逻辑 80 /// </summary> 81 /// <param name="properties"></param> 82 /// <returns></returns> 83 protected override Task HandleForbiddenAsync(AuthenticationProperties properties) 84 { 85 //默认返回403状态代码 86 return base.HandleForbiddenAsync(properties); 87 } 88 } 89 }
3.2 授权
授权主要涉及组件:
授权组件(包括 AuthorizeAttribute
和 AllowAnonymousAttribute
属性)
在最基本的使用中,将 [Authorize]
属性应用于控制器、操作或 Razor 页面,将对该组件的访问权限限制为经过身份验证的用户,意思就是通过身份验证的才能访问
首先来看AuthorizeAttribute
的定义:
1 using System; 2 3 namespace Microsoft.AspNetCore.Authorization 4 { 5 // 6 // 摘要: 7 // Specifies that the class or method that this attribute is applied to requires 8 // the specified authorization. 9 [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true, Inherited = true)] 10 public class AuthorizeAttribute : Attribute, IAuthorizeData 11 { 12 // 13 // 摘要: 14 // Initializes a new instance of the Microsoft.AspNetCore.Authorization.AuthorizeAttribute 15 // class. 16 public AuthorizeAttribute(); 17 // 18 // 摘要: 19 // Initializes a new instance of the Microsoft.AspNetCore.Authorization.AuthorizeAttribute 20 // class with the specified policy. 21 // 22 // 参数: 23 // policy: 24 // The name of the policy to require for authorization. 25 public AuthorizeAttribute(string policy); 26 27 // 28 // 摘要: 29 // Gets or sets the policy name that determines access to the resource. 30 public string? Policy { get; set; } 31 // 32 // 摘要: 33 // Gets or sets a comma delimited list of roles that are allowed to access the resource. 34 public string? Roles { get; set; } 35 // 36 // 摘要: 37 // Gets or sets a comma delimited list of schemes from which user information is 38 // constructed. 39 public string? AuthenticationSchemes { get; set; } 40 } 41 }
传入不同的策略名称,可以应用不同的授权策略,角色也是如此。
还可以使用 AllowAnonymous
属性来允许未经身份验证的用户访问单个操作,例如:
1 [Authorize] 2 public class AccountController : Controller 3 { 4 [AllowAnonymous] 5 public ActionResult Login() 6 { 7 } 8 9 public ActionResult Logout() 10 { 11 } 12 }
3.2.1 添加授权策略
在Program.cs 文件中,添加所示代码,添加一个老师或管理员角色才能访问的策略
1 builder.Services.AddAuthorization(option => 2 { 3 //添加一个授权策略,名称是 requiredAdminOrTeacher 4 option.AddPolicy("requiredAdminOrTeacher", policyBuilder => 5 { 6 //这个策略是: 角色为Admin和Teacher的访问者可以访问 7 policyBuilder.RequireRole("Admin", "Teacher"); 8 }); 9 });
将该策略应用到方法或控制器
1 using Microsoft.AspNetCore.Authorization; 2 using Microsoft.AspNetCore.Mvc; 3 4 namespace WebApplication1.Controllers 5 { 6 [ApiController] 7 [Route("[controller]")] 8 public class WeatherForecastController : ControllerBase 9 { 10 11 12 13 [Authorize(Policy = "requiredAdminOrTeacher")] 14 public IActionResult Get() 15 { 16 var var1 = new JsonResult(new 17 { 18 code = 200, 19 msg = "OK" 20 }); 21 22 return var1; 23 } 24 } 25 }
四、后记
前面的思想和理解叙述了很多,核心代码就几行。授权框架替我们做了很多事,又给了足够的自定义和灵活性。
当认真理解了身份认证和授权中的核心思想,代码写起来就得心应手了。
难的是,如何把实际的业务需求转换为代码,与这个框架结合起来。
更多内容请参阅aspnetcore源码。
笔者技术有限,不当之处请广大读者批评指正!
4.1 参考资料
主要参阅了微软官方文档:
1. https://learn.microsoft.com/zh-cn/aspnet/core/security/authentication/?view=aspnetcore-7.0