认证方式
aspnetcore的基本认证方式是:携带着认证信息的数据进入应用程序,经过认证中间件。如果验证通过,会把信息钟携带的用户的claim集中生成ClaimsPrincipal,然后就可以在程序中访问用户信息了。
在本系统中使用了JWT的方式进行认证,关于jwt可以看这里。aspnetcore使用jwt也很简单,官方文档有很直接的例子。在系统中实现了这样几项和jwt相关的功能。
- jwt是有时效性的,为了防止token过期影响用户操作。在客户端创建了http请求的管道,在进行任何服务器请求前先对token的expire进行判断是否过期。如果将要过期或已经过期,就去请求后台的刷新token接口。后台接口会读取旧token,刷新服务是允许token过期一小段时间的,验证通过则会根据旧token信息派发新token。用户取得新token后,才会继续后面的处理。这样就完成了在用户无感知情况下保持登录状态。
- 为了防止token被滥用,在服务端生成token的同时会以用户名为key,token数据为value记录在缓存中,相当于白名单。如果账号在别地登录,或者管理员修改用户信息,都会导致token缓存发生变化。在认证过程中,就会失败并返回错误信息。这时客户端可以根据错误信息,强制用户注销。jwt本身的认证方式和白名单并不冲突,因为白名单不仅可以记录token本身,还可以记录ip等信息来防止异地访问。下面是白名单验证的代码:
services.AddAuthentication(ops =>
{
ops.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
ops.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(ops =>
{
ops.TokenValidationParameters = new TokenValidationParameters
{
ValidIssuer = option.Issuer,
ValidateIssuer = true,
ValidAudience = option.Audience,
ValidateAudience = true,
IssuerSigningKey = option.SecurityKey,
ValidateIssuerSigningKey = true,
ValidateLifetime = true,
};
ops.Events = new JwtBearerEvents()
{
OnMessageReceived = context => { return Task.CompletedTask; },
OnTokenValidated = context =>
{
var memoryCache = context.HttpContext.RequestServices.GetRequiredService<IMemoryCache>();
var userName = context.Principal.GetUserName();
var exist = memoryCache.TryGetValue(userName, out string token);
if (exist)
{
var bearerToken = context.Request.Headers["Authorization"].ToString();
if (!bearerToken.Contains(token))
{
context.Fail("TokenNoExist");
}
}
else
{
context.Fail("TokenNoExist");
}
return Task.CompletedTask;
},
OnChallenge = context =>
{
if (context.AuthenticateFailure.Message == "TokenNoExist")
{
context.HandleResponse();
context.Response.StatusCode = 470;
}
return Task.CompletedTask;
}
};
});
授权方式
在上边的认证中获取了用户的信息,所以这里就需要根据用户的信息去给用户进行授权。微软提供了丰富的授权方式,我们的需求要做的是API的访问授权,需要自定授权策略。
这里的步骤就简单了,我们先建立ApiAuthorizationHandler这个类,对请求进行处理判断。首先我们获取请求的api路径和http方法。然后判断此请求是否在用户的角色中的元素的api的合集中,如果在说明用户有权限访问此处理,如果没有则禁止授权。
然后将ApiAuthorizationRequirement和ApiAuthorizationHandler注册到服务中。
//【授权】
services.AddAuthorization(options =>
{
options.AddPolicy("ApiPermission", policy => policy.Requirements.Add(new ApiAuthorizationRequirement()));
});
// 注入权限处理器
services.AddTransient<IAuthorizationHandler, ApiAuthorizationHandler>();
最后一步,绑定控制器
/// <summary>
/// 授权访问API控制器
/// </summary>
[Route("api/[controller]/[action]")]
[ApiController]
[Authorize]
[Authorize("ApiPermission")]
public abstract class AuthorizeController : ControllerBase
{
}
然后所有需要进行认证和授权的控制器继承此类就完成了。
授权方式
下边说一下系统的登录流程,画了一个简单的图
在token中只保存了用户名和角色等简单的信息。页面访问的Identifycation和route是放在response中返回到web端,由web端自行处理。角色的api的访问权限放在缓存中,授权时根据用户的角色进行判断。用户的数据过滤器放在缓存中,数据的仓储类实例化时会自动加载缓存中的过滤器。这样就完成了整个的权限认证过程。
除了用户名密码登录也可以任意更改成其他类型的登录方式,譬如手机登录,微信第三方登录。所有登录方式都需要去取得用户数据,然后后面的流程就都一样了。
总结
这三篇文章讲了一下系统思路,有可能有错误和繁琐的地方。如果您觉得写的不咋地,也请轻喷。要是觉得有点帮助,可以star一下。有时间会继续完善重构现在的功能,未来计划添加更多模块,做成一个大系统。