【ASP.NET Core 认证】认证流程
注册服务
在Startup.ConfigureServices执行services.AddAuthentication()
services.AddAuthentication()
注册如下服务,便于理解省略了部分辅助服务
services.TryAddScoped<IAuthenticationService, AuthenticationService>();
services.TryAddScoped<IAuthenticationHandlerProvider, AuthenticationHandlerProvider>();
services.TryAddSingleton<IAuthenticationSchemeProvider, AuthenticationSchemeProvider>();
AddAuthentication(Action)重载
此重载同样会先注册上面的核心服务。然后设置初始化AuthenticationOptions的委托
services.AddAuthentication(authenticationOptions=> {
authenticationOptions.AddScheme<CookieAuthenticationHandler>("cookie", "显示名cookie");
authenticationOptions.AddScheme<JwtBearerHandler>("jwt","显示名jwtToken");
authenticationOptions.DefaultAuthenticateScheme = "cookie";
//...其它配置
});
AddAuthentication(string defaultScheme)重载
内部也是调用上面的方法,只是只设置了AuthenticationOptions.DefaultScheme
AddAuthentication方法始终返回AuthenticationBuilder,它允许我们通过链式调用方式来向AuthenticationOptions添加多个身份验证方案,所以更常见的方式如下:
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie().AddJwtBearer();
AddCookie()等同于:
authenticationOptions.AddScheme<CookieAuthenticationHandler>(CookieAuthenticationDefaults.AuthenticationScheme, "显示名cookie",);
service.Configre<CookieAuthenticationOptions>(options=>{
options.Cookie.Name = CookieAuthenticationDefaults.CookiePrefix + name;
options.CookieManager = new ChunkingCookieManager();
options.LoginPath = CookieAuthenticationDefaults.LoginPath;
options.LogoutPath = CookieAuthenticationDefaults.LogoutPath;
options.AccessDeniedPath = CookieAuthenticationDefaults.AccessDeniedPath;
}
});
身份验证流程
为了便于理解后续的概念,下面先以最简单常见的 【用户密码+cookie】 的身份验证方式说说核心流程
登录:
用户输入账号密码提交
服务端验证账号密码
若验证成功,则创建一个包含用户标识的票证(下面会说)
将票证加密成字符串写入cookie
携带cookie请求:
用户发起请求
身份验证中间件尝试获取并解密cookie,进而得到含用户标识的票证(下面会说)
将用户标识设置到HttpContext.User属性
注意:若身份验证中间件即使没有解析得到用户标识,请求也会继续执行,此时以匿名用户的身份在访问系统
AuthenticationMiddleware身份验证中间件
当我们调用app.UseAuthentication();
将注册身份验证中间件AuthenticationMiddleware,它负责在请求阶段尝试从请求中获取用户标识,若获取到则赋值给HttpContext.User属性,否则以匿名用户身份继续执行下一个中间件。
流程大致如下:
- 遍历程序中所有实现IAuthenticationRequestHandler身份验证处理器,调用其HandleRequestAsync方法,此方法若返回true则终止本次请求
- 通过AuthenticationSchemeProvider获取默认身份验证方案
- 调用httpContext.AuthenticateAsync(默认方案名)尝试从请求中获取当前用户,内部从IOC容器中获取AuthenticationService并调用其同名方法
- 如果前一步获取到用户,则设置到httpContext.User属性上
- 无论是否成功获取用户请求都会进入下一个中间件继续后续执行
步骤1中允许AuthenticationHandler在特定情况下终止请求,如果我们将来自定义AuthenticationHandler时实现IAuthenticationRequestHandler可以达到这种目的
AuthenticationMiddleware源码
public class AuthenticationMiddleware
{
private readonly RequestDelegate _next;
public AuthenticationMiddleware(RequestDelegate next, IAuthenticationSchemeProvider schemes)
{
_next = next;
Schemes = schemes;
}
public IAuthenticationSchemeProvider Schemes { get; set; }
public async Task Invoke(HttpContext context)
{
context.Features.Set<IAuthenticationFeature>(new AuthenticationFeature
{
OriginalPath = context.Request.Path,
OriginalPathBase = context.Request.PathBase
});
// 给每个IAuthenticationRequestHandler方案一个处理请求的机会
var handlers = context.RequestServices.GetRequiredService<IAuthenticationHandlerProvider>();
foreach (var scheme in await Schemes.GetRequestHandlerSchemesAsync())
{
var handler = await handlers.GetHandlerAsync(context, scheme.Name) as IAuthenticationRequestHandler;
if (handler != null && await handler.HandleRequestAsync())
{
return;
}
}
var defaultAuthenticate = await Schemes.GetDefaultAuthenticateSchemeAsync();
if (defaultAuthenticate != null)
{
var result = await context.AuthenticateAsync(defaultAuthenticate.Name);
if (result?.Principal != null)
{
context.User = result.Principal;
}
}
await _next(context);
}
}
AuthenticationService.AuthenticateAsync
大致执行如下步骤:
- 通过AuthenticationHandlerProvider获取指定或默认的身份验证方案名获取关联的身份验证处理器AuthenticationHandler
- 调用AuthenticationHandler.AuthenticateAsync()尝试从请求中获取用户
- 返回前尝试将用户标识转换成另一个用户标识
步骤2针对Cookie身份验证来说最终体现为CookieAuthenticationHandler.HandleAuthenticateAsync()
步骤3在将用户标识设置到httpContext.User之前允许我们添加修改用户标识的某些数据。实现方式是定义类实现IClaimsTransformation并将其(推荐单例)注册到依赖注入容器。
CookieAuthenticationHandler.HandleAuthenticateAsync
大致流程如下:
- 通过Options.CookieManager获取cookie
- 若Options.SessionStore不为空则从其获取票证,否则直接解密cookie得到票证
- 处理票证过期(如Option设置了滑动过期,在过期时响应时重新将票证写入cookie)
- 回调Events.ValidatePrincipal允许我们修改或替换用户标识
将票证存储到自定义的存储:services.AddAuthentication().AddCookie(opt=> opt.SessionStore = new MySessionStore());
在前一步骤AuthenticationService.AuthenticateAsync的步骤3中允许我们的代码替换/修改用户标识,在这里的步骤4也允许做类似的事,区别在于前者是针对所有身份验证方案有效的,如默认身份验证方案为JwtBeaerToken时也有效。而后者只是针对cookie身份验证有效。
CookieAuthenticationHandler.HandleChallengeAsync
默认大致流程如下:
- 组织登录页地址:Options.LoginPath + QueryString.Create(Options.ReturnUrlParameter, 当前地址);
- 回调Events.RedirectToLogin,若是ajax请求则返回401状态码,否则跳转到登录页
默认登录页地址"/Account/Login"
默认处理流程如下:
public Func<RedirectContext<CookieAuthenticationOptions>, Task> OnRedirectToLogin { get; set; } = context =>
{
if (IsAjaxRequest(context.Request))
{
context.Response.Headers[HeaderNames.Location] = context.RedirectUri;
context.Response.StatusCode = 401;
}
else
{
context.Response.Redirect(context.RedirectUri);
}
return Task.CompletedTask;
};
我们也可以通过在应用启动时通过选项对象来修改。
services.AddAuthentication().AddCookie(opt =>
{
opt.LoginPath = "account/mylogin";
opt.Events.OnRedirectToLogin = context =>
{
context.Response.Redirect(context.RedirectUri);
return Task.CompletedTask;
};
});
CookieAuthenticationHandler.HandleSignInAsync(登录)
大致步骤如下:
- 准备一个CookieSigningInContext(=当前上下文+当前身份验证方案+当前身份验证方案关联的选项对象+需要登录用户的标识+cookie选项+额外数据)
- 处理过期时间等属性
- 回调Events.SigningIn,允许我们在写入cookie之前做调整
- 准备票证(=用户 + 身份验证相关属性 + 身份验证方案名 )
- 若此身份验证处理器对应的选项配置了Options.SessionStore则使用此存储,否则加密票证写入cookie
- 回调Events.SignedIn
- 最后回调Events.RedirectToReturnUrl尝试调整会最初的访问页面
CookieAuthenticationHandler.HandleSignOutAsync(注销)
在Action中调用Context.SingOutAsync可以实现注销,在为传入方案名的情况下使用默认身份验证方案进行注销,假设默认是cookie身份验证,则CookieAuthenticationHandler.HandleSignOutAsync将被执行。大致流程如下:
若选项对象配置了Options.SessionStore则从其清空用户标识
回调Events.SigningOut
清除cookie(可能是含sessionKey或包含用户标识的cookie)
通过回调Events.RedirectToReturnUrl跳转到Options.ReturnUrlParameter页面