Asp.NET Core 中的 认证授权 详解和使用
阅读要求
- 你需要对 netcore 有一些了解
- 有对 JWT 有一定的了解
什么是 验证 和 授权?
身份验证(authentication):是确定用户身份的过程
授权(authorization ):是确定用户(已经验证成功的用户)是否有权访问资源的过程。
身份验证
职责:
- 对用户进行身份验证。
- 在未经身份验证的用户试图访问受限资源时作出响应。
现在,我们对一个 action 方法上添加 authorize 特性,这表明我们对这个接口进行了授权:
[HttpGet] [Authorize] public IEnumerable Get(){ return new string[] {"数据1", "数据2"}; }
如果我们直接访问这个接口,会报如下错误:
意思是:你定义了授权,但没有指定任何(包括自定义和官方的) 身份验证方案;
授权Authorization 和 认证Authentication 是相辅相成的;两者缺一不可。
解决的方法,其实报错信息已经告诉你了;即:添加认证方案的支持,其实,认证方案有很多,但是现在主要推荐的还是 Jwt Bearer 身份验证方案:
1、Nuget 中安装 Microsoft.AspNetCore.Authentication.JwtBearer 包;
2、然后再 ConfigurationServices 中添加对 身份验证的方案(包括使用什么方案,这个方案需要做什么样子的配置) 做注入容器中处理:
SecurityKey securityKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes("ixkeE8eu2345k4zs")); // 注意:这里的key不能低于16位 services.AddAuthentication("Bearer") // 注入认证服务,认证类型:Bearer .AddJwtBearer(o => // 注入 Jwt Bearer认证 服务,对其进行配置 { // 对 jwt 进行配置 o.TokenValidationParameters = new TokenValidationParameters() // 对Token的认证是哪些参数,这里设置 { // 这里的参数遵循 3(必要) + 2(可选) 个参数的规则 // 1、是否开启秘钥认证,验证秘钥 ValidateIssuerSigningKey = true, // 验证发行者签名秘钥 IssuerSigningKey = securityKey, // 发行者签名秘钥是? // 2、验证发行人 ValidateIssuer = true, // 验证发行者 ValidIssuer = "issuer", // 验证发行者的名称是? // 3、验证订阅人 ValidateAudience = true, // 是否验证订阅者 ValidAudience = "audience", // 验证订阅者的名称是? // 1+1 // 过期时间 和 生命周期 RequireExpirationTime = true, // 使用过期时间 ValidateLifetime = true, // 验证生命周期 }; });
提醒:根据报错信息,他有两种写法,下面是第二种:
services.AddAuthentication(x => { // 认证方案:Bearer x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; // 即:Bearer // 默认Challeng质询方案: Bearer x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }).AddJwtBearer(o =>{ // 内容和上面一样 ... });
配置好后,我们再次访问接口,发现这回不报错了,但是返回的状态码时 401;
401 Unauthorized:未经授权,身份认证不通过,未认证,可能:无令牌,令牌无效、失效(因为你没有使用有效的token,无法通过 身份认证 Authentication)
403 Forbidden:被禁止,即:令牌通过,但是你无权限
说明,我们验证已经起作用了,但因为我们没有传递 jwt token 信息(即没有权限),对于这个接口的访问作出了拒绝响应;
接下来,我们新建一个 action 方法,作用是 创建 有效的 token 令牌,然后用这令牌访问需要授权的api:
// Jwt Token 的生成
[HttpGet] public string GetToekn() { // 注意,必须和上面的 JwtBearer 配置一致,且密钥最少16位,太少会报错! SecurityKey securityKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes("ixkeE8eu2345k4zs")); // 同样,我们在上面的 JwtBearer 配置中,需要验证的是什么,这里也要生成对应的条件,缺了就会导致认证失败,假如这里发行人改成其他,验证那边就不通过 SecurityToken securityToken = new JwtSecurityToken( // 和上面一样,同样遵循 3+2 规则 issuer: "issuer", // 发行人 audience: "audience", // 订阅人 signingCredentials: new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256), // 安全密钥 和 加密算法 expires: DateTime.Now.AddHours(1), // 过期时间 claims: new Claim[] { } // 添加 Claim(声明主体),添加uid、username、role等都放在这里 ); return new JwtSecurityTokenHandler().WriteToken(securityToken); // 返回 Token字符串 }
最后,我们还需要添加 身份验证 UseAuthentication 中间件,授权 Authorization 中间件,要不然http请求管道中没有处理 身份认证 了:
app.UseHttpsRedirection(); // 注意下面 Routing Authentication Authorization 这三个中间件的放置顺序,必须按照这个顺序: app.UseRouting(); // 添加 身份验证 中间件(注意顺序,中间件这里是:先身份验证再授权)、 // 而且 身份验证 和 授权 都要放在Routing 之后 app.UseAuthentication(); // 添加 身份认证中间件 // 添加 授权中间件 app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); });
运行项目,先获取 token:
使用 这个token访问需要授权的api:
或者你也可以这么访问:
至此,验证 和 授权 已经讲完了,你需要好好思考下上面的流程;
上面内容中,大多介绍了身份验证,如何使用,而接下去,我们将着重介绍 授权;
授权
上面例子中,我么用 authorize 这个特性作用于一个 action 方法上了,这就是授权,对这个action进行了权限限制;
但是,这种简单的授权,是只要有效的token(即:身份验证通过),就能访问这个接口,而没有精细化处理(就好比:董事长有这权限、行政也有着权限、而员工没有这权限);
asp.net core 的授权分三种
1、普通的授权(上面已经讲了)
2、基于角色的授权
3、基于策略的授权
基于角色的授权:
[HttpGet] [Authorize(Roles="Vip")] public string Get(){ ... }
之前,我们在编写获取 Jwt Token 时,定义的 Claim 是空数组,现在,我们加点东西下去:
... expires: DateTime.Now.AddHours(1), claims: new Claim[] { new Claim("kuozhan", "kuozhanneirong"), // 可以用我们自己定义的 name new Claim(ClaimTypes.Role, "Vip"), // 也可以用内置的name,如 Role,这里就是我们的授权,给谁 new Claim(ClaimTypes.Email, "123@qq.com") } // 添加 Claim(声明主体),添加uid、username、role等都放在这里 ); ...
ClaimTyps.Role的参数对应的就是 action 上面特性的Role参数;
我们通过这种方式生成的 Token 就可以访问对 Roles=Vip 的action的权限访问了;
基于策略
假如,一个action不止 Vip 这个权限,还有五六个呢?如何把他们合并在一起?这就使用到了 基于策略的 授权机制了:
[HttpGet] [Authorize(Policy="AdminAndUser")] public IEnumerate Get(){ return new string[] {"数据1", "数据2"};l }
然后我们注入服务:
// 策略注入服务: services.AddAuthorization( o => { o.AddPolicy("AdminOrUser", o=>{ o.RequireRole("Admin", "User").Build(); }); });
这样,A用户有Admin、B用户有User,它们两都能访问整个接口;
如果要求,用户必须有Admin 和 User 才能访问,那么可以:
// 策略注入服务: services.AddAuthorization( o => { o.AddPolicy("AdminOrUser", o=>{ o.RequireRole("Admin").RequireRole("User").Build(); }); });
自定义策略的授权
// 新建一个类: /PolicyRequirement/MustRoleAdminHandle.cs using Microsoft.AspNetCore.Authorization; using System.Linq; using System.Threading.Tasks;
// 自定义策略授权,继承至 IAuthorizationHandler 且实现里面的 HandleAsync 方法 即可 public class MustRoleAdminHandle : IAuthorizationHandler { public Task HandleAsync(AuthorizationHandlerContext context) { // 做验证判断,如果验证通过,则 context.Successed(); // 可以看形参 context 里的各种属性 // 比如: //var requirement = context.Requirements.FirstOrDefault(); //context.Succeed(requirement); // 这样设置后就返回 200 // 或者设置 Fail() // context.Fail(); return Task.CompletedTask; // 直接这么返回,会返回 403 } }
// 服务注入(注意这个): using Microsoft.AspNetCore.Authorization; services.AddSingleton<IAuthorizationHandler, MustRoleAdminHandle>();
思考:context.Requirements 里面的值是什么? 是对应 api接口上策略名里面的所有Role集合,即:
action上定义的是 UserAndAdmin时,其策略注册是 o.RequireRole("Admin", "User") 那么,集合就是: Admin和User;
基于 Claim声明 的授权
services.AddAuthorization(o=>{ o.AddPolicy("AdminClaim2", 0 => { o.RequireClaim("Email", "123@qq.com", "456@qq.com"); // JWT Token里面的 Claim 中设置了这些,那么就会通过 AdminClaim2 }); });
基于Requirement需要,大多数开发都是用这种方式:
[HttpGet] [Authorize(Policy="AdminRequireMent")] public string Get(){ return string.Empty();} // 服务注入 services.AddAuthorization(o=>{ o.AddPolicy("AdminRequireMent", o => { var myAdminRequirement = new AdminRequirement(myName = "zhangsanfeng"); // 可以传递参数 o.Requirements.Add(myAdminRequirement); // 完全自定义 }); }); // 需要新建一个 /PolicyRequirement/AdminRequirement.cs 并继承至 IAuthorizationRequirement using using Microsoft.AspNetCore.Authorization; public class AdminRequirement: IAuthorizationRequirement{ public string myName { get; set; }; }
这样,我们重启再次访问有授权的Get接口时,会先进入这里:
public class MustRoleAdminHandle : IAuthorizationHandler { public Task HandleAsync(AuthorizationHandlerContext context) { context.Requirements // 该参数返回的就是 myAdminRequirement 实例 // 这样,我们就可以通过这个自定义判断 // 非常的灵活 } }
但是,上面代码中,IAuthorizationHandler 接口并不是很灵活,微软又抽象了一个 抽象类 AuthorizationHandler ,这样,我们就更加容易去使用了:
public class MustRoleAdminHandle:AuthorizationHandler<AdminRequirement> { protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, AdminRequirement requirement) { // context.Succeed(requirement); return Task.CompletedTask; // 直接这么返回,会返回 403 } }
JwtBearer认证中,默认是通过http的Authorization头来获取的,也是推荐这种做法,但是某些场景下需要通过url或者cookie中来传递Token,如何实现呢?
从url中获取的,可以:
SecurityKey securityKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes("abcdefghijklnmopq")); // 为key不能低于16位 services.AddAuthentication("Bearer") // 注入认证服务,认证类型:Bearer .AddJwtBearer(o => // 注入 Jwt Bearer认证 服务,对其进行配置 { // 主要是这里: o.Events = new JwtBearerEvents(){ OnMessageREceived = context => { context.Token = context.Request.Query["access_token"]; return Task.CompletedTask; } }; o.TokenValidationParameters = new TokenValidationParameters{ //... } });
除了OnMessageReceived外,还提供了如下几个事件:
- TokenValidated:在Token验证通过后调用。
- AuthenticationFailed: 认证失败时调用。
- Challenge: 未授权时调用。
使用OIDC服务(即:OpenId Connect,身份认证(核心部分))
简单来说:OIDC是OpenID Connect的简称,OIDC=(Identity, Authentication) + OAuth 2.0。它在OAuth2上构建了一个身份层,是一个基于OAuth2协议的身份认证标准协议。
在上面的示例中,我们简单模拟的Token颁发,功能非常简单,但是这并不适合在生产环境中使用,可是微软也没有提供OIDC服务的实现,好在.NET社区中提供了几种实现,可供我们选择:
AspNet.Security.OpenIdConnect.Server (ASOS)、IdentityServer4、OpenIddict 和 PwdLess
我们在这里使用IdentityServer4来搭建一个OIDC服务器,具体代码会给大家带来混淆,所以这里忽略了。
services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new OpenApiInfo { Title = "xxxProjectNet5.Api", Version = "v1" }); // =======================================================按照下面的格式即可 // 开启小锁 c.OperationFilter<AddResponseHeadersFilter>(); c.OperationFilter<AppendAuthorizeToSummaryOperationFilter>(); // 在 header 中添加token,传递到后台 c.OperationFilter<SecurityRequirementsOperationFilter>(); c.AddSecurityDefinition("oauth2", new OpenApiSecurityScheme() { Description = "JWT 授权(数据将在请求头中进行传输)直接在下框中输入 Bearer(token)(注意两者之间是一个空格)", Name = "Authorization", // jwt 默认的参数名称 In = ParameterLocation.Header, // jwt 将默认存放 Authorization 信息的位置(请求头中) Type = SecuritySchemeType.ApiKey // 除了ApiKey 外,还有 Http、Oauth2、OpenIdConnect }); });
如果你有任何问题,欢迎留下评论,我们一起探讨