聊聊 asp.net core 认证和授权-LMLPHP

使用asp.net core 开发应用系统过程中,基本上都会涉及到用户身份的认证,及授权访问控制,因此了解认证和授权流程也相当重要,下面通过分析asp.net core 框架中的认证和授权的源码来分析认证、授权的原理及认证和授权的关系。

认证是什么?

认证是应用系统识别当前访问者的身份的一个过程,当应用系统接收到浏览器的请求后,通常会根据请求中携带的一些用户的的关键信息来识别当前登录用户的身份,通过解析这些信息,对用户进行合法性校验并进行解密,如果校验通过,则表示认证通过,应用系统会将认证通过后的用户信息存储到Http请求上下文中,以便后续业务使用及授权流程中使用。

asp.net core中通常将认证信息加密后存储到cookie中,每次访问需要认证的页面时将这些cookie信息发送到应用系统,以便应用系统识别访问者的身份,也就是经典的Cookie认证。

需要注意的是:认证仅仅只是识别当前访问用户的身份,并不负责具体的访问权限控制逻辑,如不具备某个资源的访问权限返回403,未登录返回401等,这些均由授权流程来控制。

asp.net core 中负责认证流程的中间件是AuthenticationMiddleware 类,以下是asp.net core 3.1 的源代码,可以看到,先遍历所有实现了IAuthenticationRequestHandler接口的认证方案,并调用IAuthenticationRequestHandler接口的HandleRequestAsync方法,如果认证通过,则不再继续往下执行,并且此时HttpContext.User已经包含认证后的用户信息,如果所有实现 IAuthenticationRequestHandler 接口的认证方案,都未能对当前访问用户进行身份认证,则使用默认的认证方案进行认证(也就是:GetDefaultAuthenticateSchemeAsync返回的认证方案),可以看到认证流程即使没能识别当前访问者的用户身份,也会继续执行下一个流程,(尾部:await _next(context);

public class AuthenticationMiddleware
{
	private readonly RequestDelegate _next;

	public IAuthenticationSchemeProvider Schemes
	{
		get;
		set;
	}

	public AuthenticationMiddleware(RequestDelegate next, IAuthenticationSchemeProvider schemes)
	{
		if (next == null)
		{
			throw new ArgumentNullException("next");
		}
		if (schemes == null)
		{
			throw new ArgumentNullException("schemes");
		}
		_next = next;
		Schemes = schemes;
	}

	public async Task Invoke(HttpContext context)
	{
		context.Features.Set((IAuthenticationFeature)new AuthenticationFeature
		{
			OriginalPath = context.Request.Path,
			OriginalPathBase = context.Request.PathBase
		});
		IAuthenticationHandlerProvider handlers = context.RequestServices.GetRequiredService<IAuthenticationHandlerProvider>();
		foreach (AuthenticationScheme item in await Schemes.GetRequestHandlerSchemesAsync())
		{
			IAuthenticationRequestHandler authenticationRequestHandler = (await handlers.GetHandlerAsync(context, item.Name)) as IAuthenticationRequestHandler;
			bool flag = authenticationRequestHandler != null;
			if (flag)
			{
				flag = await authenticationRequestHandler.HandleRequestAsync();
			}
			if (flag)
			{
				return;
			}
		}

		AuthenticationScheme authenticationScheme = await Schemes.GetDefaultAuthenticateSchemeAsync();
		if (authenticationScheme != null)
		{
                        //内部调用IAuthenticationService进行认证。
			AuthenticateResult authenticateResult = await context.AuthenticateAsync(authenticationScheme.Name);
			if (authenticateResult?.Principal != null)
			{
				context.User = authenticateResult.Principal;
			}
		}
		await _next(context);
	}
}

授权是什么?

授权是确定当前访问用户是否具备访问某个系统资源权限的过程,对于需要授权才能访问的系统资源,通常通过[Authorize]特性来标识,通过该特性,可以指定该资源需要哪个用户角色才能访问、必须符合哪个授权策略才能访问,以及访问该资源时采用的用户认证方案是什么,当用户访问系统的某个API或者页面时,授权流程会检查当前用户是否具备该API或者页面的访问权限,如果授权检查失败,那么会判断当前用户是否已经认证通过,如果认证通过,但无访问该资源的权限,那么返回403(禁止访问),如果未认证,那么直接返回401(未认证),表示需要用户登录认证后在进行访问,需要注意的是:检查是否具备访问权限之前会先进行用户身份的认证,至于用什么认证方案就看AuthorizeAttribute有没有指定特定的认证方案,如果没有,则直接采用认证流程的认证成功的身份信息。

asp.net core 中,授权流程的执行是通过AuthorizationMiddleware类来完成的,以下是asp.net core 3.1中的源码。

// Microsoft.AspNetCore.Authorization.AuthorizationMiddleware
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization.Policy;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;

public class AuthorizationMiddleware
{
	private const string AuthorizationMiddlewareInvokedWithEndpointKey = "__AuthorizationMiddlewareWithEndpointInvoked";

	private static readonly object AuthorizationMiddlewareWithEndpointInvokedValue = new object();

	private readonly RequestDelegate _next;

	private readonly IAuthorizationPolicyProvider _policyProvider;

	public AuthorizationMiddleware(RequestDelegate next, IAuthorizationPolicyProvider policyProvider)
	{
		_next = next ?? throw new ArgumentNullException("next");
		_policyProvider = policyProvider ?? throw new ArgumentNullException("policyProvider");
	}

	public async Task Invoke(HttpContext context)
	{
		if (context == null)
		{
			throw new ArgumentNullException("context");
		}
		Endpoint endpoint = context.GetEndpoint();
		if (endpoint != null)
		{
			context.Items["__AuthorizationMiddlewareWithEndpointInvoked"] = AuthorizationMiddlewareWithEndpointInvokedValue;
		}
                //获取访问当前资源所需要的所有角色权限,及授权策略,以及访问该资源时需要使用的认证方案列表,并统一合并到一个AuthorizationPolicy对象中。
		IReadOnlyList<IAuthorizeData> authorizeData = endpoint?.Metadata.GetOrderedMetadata<IAuthorizeData>() ?? Array.Empty<IAuthorizeData>();
		AuthorizationPolicy policy = await AuthorizationPolicy.CombineAsync(_policyProvider, authorizeData);
		if (policy == null)
		{
			await _next(context);
			return;
		}
		IPolicyEvaluator policyEvaluator = context.RequestServices.GetRequiredService<IPolicyEvaluator>();
                //通过IPolicyEvaluator.AuthenticateAsync()方法,对当前访问者进行认证,至于使用哪种方案认证,根据该资源要求使用的认证方案来,如果没有指定,
                //则使用默认认证方案进行认证。
		AuthenticateResult authenticationResult = await policyEvaluator.AuthenticateAsync(policy, context);
                //如果包含实现了IAllowAnonymous接口的特性,则不进行授权检查。
		if (endpoint?.Metadata.GetMetadata<IAllowAnonymous>() != null)
		{
			await _next(context);
			return;
		}
                //这里调用AuthorizeAsync进行授权检查,注意,这里将上一步认证结果authenticationResult也传到了授权检查方法内部。
		PolicyAuthorizationResult policyAuthorizationResult = await policyEvaluator.AuthorizeAsync(policy, authenticationResult, context, endpoint);
		//检查授权结果,如果是未登录,则返回401未认证,让用户进行登录,如果该资源指定了特定的认证方案,则调用特定认证方案的Challenge方法,
                //否则调用默认认证方案的Challenge方法,通常Challenge做的事情就是重定向用户的浏览器到登录页面或者对于ajax异步请求返回401.
                if (policyAuthorizationResult.Challenged)
		{
			if (policy.AuthenticationSchemes.Any())
			{
				foreach (string authenticationScheme in policy.AuthenticationSchemes)
				{
					await context.ChallengeAsync(authenticationScheme);
				}
			}
			else
			{
				await context.ChallengeAsync();
			}
		}
                //如果当前访问者用户身份认证通过,但是不被允许访问该资源的权限,那么默认返回401(禁止访问)给浏览器端,通常对于未授权的访问请求,应用常常的做法是将用户的浏览器重定向到禁止访问的提示页面,或者对于ajax异步请求来说,通常返回403状态码,和上面未认证情况一样,如果该资源指定了特定的认证方案,那么会调用特定认证方案的Forbid方法,否则调用默认认证方案的Forbid方法。
		else if (policyAuthorizationResult.Forbidden)
		{
			if (policy.AuthenticationSchemes.Any())
			{
				foreach (string authenticationScheme2 in policy.AuthenticationSchemes)
				{
					await context.ForbidAsync(authenticationScheme2);
				}
			}
			else
			{
				await context.ForbidAsync();
			}
		}
		else
		{
			await _next(context);
		}
	}
}

IPolicyEvaluator接口实现类 PolicyEvaluator类代码如下,该类主要是负责授权流程中的认证和授权。

// Microsoft.AspNetCore.Authorization.Policy.PolicyEvaluator
using System;
using System.Security.Claims;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Authorization.Policy;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Internal;

public class PolicyEvaluator : IPolicyEvaluator
{
	private readonly IAuthorizationService _authorization;

	public PolicyEvaluator(IAuthorizationService authorization)
	{
		_authorization = authorization;
	}

	public virtual async Task<AuthenticateResult> AuthenticateAsync(AuthorizationPolicy policy, HttpContext context)
	{
                //这里去判断当前资源是否有要求特定的认证方案进行认证,如果有指定特定的认证方案,则分别对每个认证方案进行认证,并把认证后的用户信息进行合并
                //最终存储到HttpContext.User属性中,并返回认证成功,如果没有指定认证方案,则使用认证流程中已经认证的用户信息作为认证结果返回,
                //从这里可以看出,认证流程还是很有必要的,在资源没有指定认证方案的前提下,认证流程为授权流程提供当前访问者的身份信息,以便执行是否具备相应资源的访问权限检查,否则就直接进入Challenge流程将要求用户先进行身份认证了
		if (policy.AuthenticationSchemes != null && policy.AuthenticationSchemes.Count > 0)
		{
			ClaimsPrincipal newPrincipal = null;
			foreach (string authenticationScheme in policy.AuthenticationSchemes)
			{
				AuthenticateResult authenticateResult = await context.AuthenticateAsync(authenticationScheme);
				if (authenticateResult != null && authenticateResult.Succeeded)
				{
					newPrincipal = SecurityHelper.MergeUserPrincipal(newPrincipal, authenticateResult.Principal);
				}
			}
			if (newPrincipal != null)
			{
				context.User = newPrincipal;
				return AuthenticateResult.Success(new AuthenticationTicket(newPrincipal, string.Join(";", policy.AuthenticationSchemes)));
			}
			context.User = new ClaimsPrincipal(new ClaimsIdentity());
			return AuthenticateResult.NoResult();
		}
		return (context.User?.Identity?.IsAuthenticated).GetValueOrDefault() ? AuthenticateResult.Success(new AuthenticationTicket(context.User, "context.User")) : AuthenticateResult.NoResult();
	}
        //resource为EndPoint对象。
	public virtual async Task<PolicyAuthorizationResult> AuthorizeAsync(AuthorizationPolicy policy, AuthenticateResult authenticationResult, HttpContext context, object resource)
	{
		if (policy == null)
		{
			throw new ArgumentNullException("policy");
		}
                //这里调用IAuthorizationService.AuthorizeAsync方法进行授权检查,默认实现类为:DefaultAuthorizationService。
		if ((await _authorization.AuthorizeAsync(context.User, resource, policy)).Succeeded)
		{
			return PolicyAuthorizationResult.Success();
		}
                //下面这句表示如果授权检查失败的情况下是进入Forbid流程还是进入Challenge流程,可以看到如果认证成功,那么表示无权限访问进入Forbid流程。
                //如果未认证,则进入Challenge流程,引导用户登录认证。
		return authenticationResult.Succeeded ? PolicyAuthorizationResult.Forbid() : PolicyAuthorizationResult.Challenge();
	}
}

 

认证和授权的关系?

授权检查之前都会先执行用户身份的认证,不过这里的认证流程只有在被访问的资源有指定特定的认证方案时才会执行,否则直接采用统一认证流程中的产生的认证信息。

可以理解为认证流程一方面是为了告诉应用系统当前访问者的身份,一方面是为了给授权检查时识别用户的身份信息,当资源没有指定采用何种认证方案时,授权流程将会采用统一认证流程里认证通过产生的用户信息,如果不启用认证流程,并且被访问的资源也没有指定特定的认证方案对访问者身份进行认证时,那么最终访问该资源时还是会被要求先登录认证,因此认证流程的另外一个用途就是为授权流程提供默认的用户认证信息。

总结起来说,

认证流程主要有如下几个作用:

  1. 识别系统访问者的身份信息,认证通过后提供给后续业务使用。
  2. 给授权流程提供访问者身份信息(资源没有指定特定认证方案时,采用默认认证方案认证通过的用户信息)。
  3. 实现授权失败后的处理逻辑,比如授权检查失败后返回的 401(未认证),403(禁止访问)等最终都是认证方案的 ChallegeAsync方法以及ForbidAsync方法来处理,这些方法是IAuthenticationHandler里面定义的,这些流程在授权失败为401/403的时候分别被授权流程调用。

授权流程主要如下几个作用:

  1. 授权流程主要是检查当前用户是否具备指定资源的访问权限,如果授权检查失败,如401(未认证),403(禁止访问),那么最终会分别调用认证方案的ChallegenAsync和ForbidAsync方法,也就是说,授权流程侧重于授权失败后的流程控制。
  2. 授权流程另外一个主要的任务是检查授权策略是否均能检验通过,如果一个资源通过AuthorizeAttribute的Policy属性指定了一个或者多个授权策略,那么必须所有授权策略都验证通过才算授权成功,如果未指定授权策略,那么就验证默认的授权策略是否能检验通过,默认的授权策略则是要求必须用户认证通过才允许访问资源。

授权流程本质上就是遍历所有注入到容器中的IAuthorizationHandler(微软默认在AddAuthorization的时候向容器注入了:PassThroughAuthorizationHandler,这个授权处理程序遍历AuthorizationHandlerContext.Requirements中所有实现了IAuthorizationHandler的Requirement类,并调用其HandleAsync方法来检查当前Requirement是否能校验通过),并对访问指定资源所要满足的所有策略中包含的Requirement进行验证,如果所有策略包含的Requirement都验证通过,那么表示授权成功,这里的Requirement是指实现了IAuthorizationRequirement的类,这个接口是一个空接口,用于标记Requirement使用。

09-09 15:16