授权在.NET 中是指确定经过身份验证的用户是否有权访问特定资源或执行特定操作的过程。这就好比一个公司,身份验证(鉴权)是检查你是不是公司的员工,而授权则是看你这个员工有没有权限进入某个特定的办公室或者使用某台设备。
两个非常容易混淆的单词
鉴权使用体验 创建一个web api程序 在program.cs中使用鉴权授权中间件。builder.Services.AddAuthentication是告诉框架,如何进行鉴权,app.UseAuthentication();告诉框架,需要做鉴权。 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 builder.Services.AddAuthentication("Cookies" ) .AddCookie("Cookies" , opt => { opt.LoginPath = "/test/login" ; opt.AccessDeniedPath = "/test/NotLoggedIn" ; }); var app = builder.Build();... app.UseAuthentication(); app.UseAuthorization(); app.MapControllers(); app.Run();
新建一个Controller。在Action方法上面标记[Authorize]
表示使用该接口需要鉴权,也可以直接将[Authorize]
特性放在Controller上面,对于不需要鉴权的Action上面放置[AllowAnonymous]
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 [HttpGet ] [Authorize ] public IActionResult UseCookie (){ return Ok("初步使用-使用cookie" ); } [HttpGet ] public async Task<IActionResult> Login (string name,string password ){ var claimsIdentity = new ClaimsIdentity("user" ); claimsIdentity.AddClaim(new Claim(ClaimTypes.Name, name)); claimsIdentity.AddClaim(new Claim(ClaimTypes.Role, "user" )); var principal = new ClaimsPrincipal(claimsIdentity); await HttpContext.SignInAsync(principal,new AuthenticationProperties { ExpiresUtc = DateTimeOffset.UtcNow.AddMinutes(30 ) }); return Ok("登录成功" ); }
未登录时直接访问/Test/UseCookie
,报错,原因是未登录直接跳转到/test/login
,而没有name和password参数,导致报400错误。
使用/Test/Login?name=1&password=1
登录后,调用/test/login
,访问成功。
理解鉴权授权 常见的鉴权方式有Cookie、JWT,其实无论是什么鉴权方式,都是一个套路,其本质就是HTTP是无状态的,每次请求都是新的,服务器不知道他是不是之前的那个请求,所以鉴权授权都是分3步:
请求服务端,获取凭证 再次请求服务端,带上第一步获取到的凭证 服务端识别凭证, 判断是否允许访问 自定义鉴权 其实框架给我们封装好了很多鉴权方式,为了搞清原理,我们来个自定义鉴权。整个鉴权流程就是读取凭证—解析凭证—检验凭证—-保存并向下一个管道传递。
新建一个类,继承自IAuthenticationHandler
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 public class CustomAuthenticationHandler : IAuthenticationHandler { private AuthenticationScheme _AuthenticationScheme = null ; private HttpContext _HttpContext = null ; public Task InitializeAsync (AuthenticationScheme scheme, HttpContext context ) { Console.WriteLine($"初始化自定义鉴权方法" ); _AuthenticationScheme = scheme; _HttpContext = context; return Task.CompletedTask; } public Task<AuthenticateResult> AuthenticateAsync () { Console.WriteLine($"开始鉴权的核心动作-找到凭证-解析凭证-检测有效" ); string userInfo = _HttpContext.Request.Query["UrlToken" ].ToString(); if (string .IsNullOrWhiteSpace(userInfo)) { return Task.FromResult(AuthenticateResult.NoResult()); } else if ("abc" .Equals(userInfo)) { var claimsIdentity = new ClaimsIdentity("custom" ); claimsIdentity.AddClaim(new Claim(ClaimTypes.Name, "Test" )); claimsIdentity.AddClaim(new Claim(ClaimTypes.Role, "Admin" )); ClaimsPrincipal claimsPrincipal = new ClaimsPrincipal(claimsIdentity); return Task.FromResult<AuthenticateResult>(AuthenticateResult.Success(new AuthenticationTicket(claimsPrincipal, null , _AuthenticationScheme.Name))); } else { return Task.FromResult<AuthenticateResult>(AuthenticateResult.Fail("登录凭证失败" )); } } public Task ChallengeAsync (AuthenticationProperties? properties ) { Console.WriteLine($"没有登录" ); string redirectUri = "/test/login" ; _HttpContext.Response.Redirect(redirectUri); return Task.CompletedTask; } public Task ForbidAsync (AuthenticationProperties? properties ) { Console.WriteLine($"没有权限" ); _HttpContext.Response.StatusCode = 403 ; return Task.CompletedTask; } }
在program.cs中删除其他鉴权设定,增加以下设定 1 2 3 4 5 builder.Services.AddAuthentication(opt => { opt.AddScheme<CustomAuthenticationHandler>("custom" , "custom-Demo" ); opt.DefaultScheme = "custom" ; });
在控制器中新增一个Action来验证登录 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 [Authorize ] public async Task<IActionResult> CustomLogin (){ var userOrigin = base .HttpContext.User; var result = await HttpContext.AuthenticateAsync("custom" ); if (result.Principal is null ) { return Forbid("认证失败" ); } else { base .HttpContext.User = result.Principal; foreach (var item in result.Principal.Claims) { Console.WriteLine($"{item.Type} :{item.Value} " ); } return Ok("登录成功" ); } }
当访问/CustomLogin?UrlToken=abc
时,正确访问,并打印出
授权的使用 授权检测可以有两层,1:没有任何要求,只要登录有凭证就行。2:要求用户有相应的权限才行
授权的三个属性
Role角色 Policy策略 AuthenticationSchemes方案 角色Role 单角色 直接加上Roles =”Admin”
1 2 [Authorize(Roles ="Admin" ) ] public IActionResult RoleAdmin ()
多角色 Admin和User都可以,满足一个就可以
1 2 [Authorize(Roles ="Admin,User" ) ] public IActionResult RoleAdmin ()
如果是需要既能满足Roles =”Admin”又能满足Roles =”User”,则】
1 2 3 [Authorize(Roles ="Admin" ) ] [Authorize(Roles ="User" ) ] public IActionResult RoleAdmin ()
注意:这里的Roles需要在生成凭证的时候使用ClaimTypes.Name
的方式,而不能是new Claim(“Role”, “user”)
1 2 3 4 var claimsIdentity = new ClaimsIdentity("user" );claimsIdentity.AddClaim(new Claim(ClaimTypes.Name, name)); claimsIdentity.AddClaim(new Claim(ClaimTypes.Role, "user" ));
角色是最通用最简单的使用方式,但是不能满足个性化需求
策略Policy Policy支持更灵活的自定义策略
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 builder.Services.AddAuthorization(opt => { opt.AddPolicy("AdminPolicy" , config => { config.RequireRole("Admin" ) .RequireUserName("abc" ) .RequireClaim(ClaimTypes.Email) .RequireAssertion(context => { return context.User.Claims.FirstOrDefault(c => c.Type.Equals(ClaimTypes.Email))?.Value.EndsWith("@qq.com" ) ?? false ; }); }); });
直接使用特性标注[Authorize(Policy = "AdminPolicy")]
自定义Requirement 可以单独写一个Requirement
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 public class CustomRequirement : AuthorizationHandler <CustomRequirement >,IAuthorizationRequirement { public CustomRequirement (string requiredName ) { if (requiredName == null ) { throw new ArgumentNullException(nameof (requiredName)); } RequiredName = requiredName ?? "@qq.com" ; } private string RequiredName { get ; } protected override Task HandleRequirementAsync (AuthorizationHandlerContext context, CustomRequirement requirement ) { if (context.User is not null && context.User.HasClaim(c => c.Type == ClaimTypes.Email)) { var emailClaimList = context.User.FindAll(c => c.Type == ClaimTypes.Email); if (emailClaimList.Any(c => c.Value.EndsWith(RequiredName))) { context.Succeed(requirement); } else { context.Fail(); } } return Task.CompletedTask; } }
直接使用AddRequirements
1 2 3 4 5 6 7 builder.Services.AddAuthorization(opt => { opt.AddPolicy("CustomPolicy" , config => { config.AddRequirements(new CustomRequirement("@163.com" )); }); });
使用
1 2 3 4 5 6 [HttpGet ] [Authorize(Policy = "CustomPolicy" ) ] public IActionResult CustomRequirement (){ return Ok("初步使用-自定义Requirement,要求使用163邮箱" ); }
Policy的多条件组合 第一种方式
1 2 3 4 5 6 7 8 9 policyBuilder.RequireRole("Admin" ) .RequireUserName("Admin" ) .RequireClaim(ClaimTypes.Country) .RequireClaim(ClaimTypes.DateOfBirth) .RequireAssertion(context => { return context.User.Claims.Any(c => c.Type.Equals(ClaimTypes.Country)); }) .Require....
1 2 3 4 .RequireAssertion(context => { return 条件1 || 条件2 ; });
第二种方式(推荐)
注意 :这种方式的“与,或”是通过写不写context.Fail()
来确定的。
结合自定义Requirement的方式,先建立一个父类 1 public class EmailRequirement : IAuthorizationRequirement {}
集成父类,设置条件,此处我们可以验证QQ邮箱和搜狗邮箱 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public class QQMailHandler : AuthorizationHandler <EmailRequirement >{ protected override Task HandleRequirementAsync (AuthorizationHandlerContext context, EmailRequirement requirement ) { if (context.User is not null && context.User.HasClaim(c => c.Type == ClaimTypes.Email)) { var emailClaimList = context.User.FindAll(c => c.Type == ClaimTypes.Email); if (emailClaimList.Any(c => c.Value.EndsWith("@qq.com" ))) { context.Succeed(requirement); } else { context.Fail(); } } return Task.CompletedTask; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public class SouGouMailHandler : AuthorizationHandler <EmailRequirement >{ protected override Task HandleRequirementAsync (AuthorizationHandlerContext context, EmailRequirement requirement ) { if (context.User is not null && context.User.HasClaim(c => c.Type == ClaimTypes.Email)) { var emailClaimList = context.User.FindAll(c => c.Type == ClaimTypes.Email); if (emailClaimList.Any(c => c.Value.EndsWith("@sougou.com" ))) { context.Succeed(requirement); } else { context.Fail(); } } return Task.CompletedTask; } }
在Program.cs中使用ioc进行注册 1 2 builder.Services.AddSingleton<IAuthorizationHandler,QQMailHandler>(); builder.Services.AddSingleton<IAuthorizationHandler,SouGouMailHandler>();
在策略中增加 1 2 3 4 5 opt.AddPolicy("OrPolicy" , config => { config.AddRequirements(new EmailRequirement()); });
使用 1 2 3 4 5 6 [HttpGet ] [Authorize(Policy = "OrPolicy" ) ] public IActionResult OrRequirement (){ return Ok("初步使用-使用Requirement,要求使用QQ或者sougou邮箱" ); }
多Scheme 在progress.cs中增加两个鉴权设置,一定要注意前后顺序,后面的会覆盖前面的 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 builder.Services.AddAuthentication(opt => { opt.AddScheme<CustomAuthenticationHandler>("custom" , "custom-Demo" ); opt.DefaultScheme = "custom" ; }); builder.Services.AddAuthentication("Cookies" ) .AddCookie("Cookies" , opt => { opt.LoginPath = "/test/login" ; opt.AccessDeniedPath = "/test/NotLoggedIn" ; });
增加Action 1 2 3 4 5 6 7 8 9 10 11 12 [HttpGet ] [Authorize(Policy = "CountryPolicy" , AuthenticationSchemes = "Cookies,UrlTokenScheme" ) ] public async Task<IActionResult> MultiToken (){ var r1 = await base .HttpContext.AuthenticateAsync("Cookies" ); var r2 = await base .HttpContext.AuthenticateAsync("custom" ); Console.WriteLine($"cookies:{r1?.Principal?.Claims.First().Value} " ); Console.WriteLine($"custom:{r2?.Principal?.Claims.First().Value} " ); return Ok("访问成功" ); }
先登录获取cookie,/Test/Login?name=1&password=1
,再访问/Test/MultiToken?UrlToken=abc
这样就能得到两个Scheme的认证信息
JWT的鉴权与授权 nuget安装Microsoft.AspNetCore.Authentication.JwtBearer
。 Program中定义 1 2 3 4 5 6 7 8 9 10 11 12 13 builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(opt => { opt.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = false , ValidateAudience = false , ValidateLifetime = false , ValidateIssuerSigningKey = true , IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("MyTestCustomSecurityKeySymmetricSecurityKey" )) }; });
在登录Action中生成一个token 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public IActionResult GetToken (string name, string password ){ var claims = new [] { new Claim("Name" , name), new Claim("id" , "11" ), new Claim(ClaimTypes.Name, "Test" ), new Claim("EMail" , "test@qq.com" ), new Claim("Account" , "Administrator" ), new Claim(ClaimTypes.Role,"Admin" ), new Claim("Sex" , "1" ) }; var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("MyTestCustomSecurityKeySymmetricSecurityKey" )); var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); var token = new JwtSecurityToken( claims: claims, expires: DateTime.Now.AddSeconds(60 * 10 ), signingCredentials: creds ); string returnToken = new JwtSecurityTokenHandler().WriteToken(token); return Ok(returnToken); }
使用 1 2 3 [Authorize ] public IActionResult JWTTest ()
请求时,需要在请求头中Authorization:
中带上Bearer 生成的token
,注意Bearer后面有一个空格