跳至正文

如何认证与授权

内容纲要

前言

    我们在NET Framework时代,最常用的是Form认证。Form认证对于前后端分离或者多前端的系统来说不是太友好,很难相互兼容。很多都是一套后台管理系统,一套前端系统,一套API在物理层面分开,这样做代码复用率非常低,后期维护成本也高很多。 ASP.NET Core 对认证与授权进行了全新的设计,使用基于声明的认证(claims-based authentication)。
Microsoft.AspNetCore.Authentication.JwtBearer
Microsoft.AspNetCore.Authentication.Cookies
Microsoft.AspNetCore.Authentication.OpenIdConnect
Microsoft.AspNetCore.Authentication.Auth
Microsoft.AspNetCore.Authentication.Google
Microsoft.AspNetCore.Authentication.Microsoft
Microsoft.AspNetCore.Authentication.FaceBook
Microsoft.AspNetCore.Authentication.Twitter

以上这些都是.NET SDK已经实现了的,他们都是基于claims的,都是Microsoft.AspNetCore.Authentication.Abstractions的实现。在ASP.NET CORE时代,很容易做到混合认证,也就是同一个Controller里,可以一个Action支持Cookies认证,另外一个可以支持JwtBearer认证。网上很多这样案例,大家可以搜索看一下,很简单的配置就能实现。

然而,如果想实现的更灵活一些,比方目前ADNC的实现,同一个Action可以同时支持多种认证,也可以指定其中一种认证方式。为了实现这种方式,废了不少脑细胞。ADNC增加了Hybrid,Basic两种认证方式(Hybrid是自己取的名,Basic是http标准认证方式,相关代码在Adnc.Shared.WebApi工程的Authentication目录)。

为什么要混合两种认证

为什么呢?
目的是什么?
首先你可以只用JwtBeare认证,Basic认证是可选项目。我们在什么情况下可能需要使用Basic认证呢?
1、微服务之间同步通信,如果客户有A服务X功能的权限,但是X功能会调用B服务的Y功能。也就是如果采用JwtBeare认证,客户必须同时有A服务X功能和B服务Y功能权限才能正常使用。
2、开放接口给第三方合作方对接,也会遇到1同样的问题。当然,如果你的需求就是这样,你完全可以只采用JwtBearer认证。

注册认证服务

public virtual void AddAuthentication()
{
   _services.AddAuthentication(HybridDefaults.AuthenticationScheme)
   .AddHybrid()
   .AddBasic()
   .AddJwtBearer(options => {xxxxx});
}
  • AddHybrid 注册Hybrid认证相关服务
  • AddBasic 注册Basic认证相关服务
  • AddJwtBearer 注册JwtBearer认证相关服务。

    实现认证的核心类是XxxAuthenticationHandler。
    通过上面的代码我们可以知道,系统默认的认证方式Hybrid,HybridAuthenticationHandler的主要作用就是路由,并不做实际的认证,系统所有认证请求都需要通过HybridAuthenticationHandler转发。

public sealed class HybridAuthenticationHandler : AuthenticationHandler<HybridSchemeOptions>
{
    protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        var authHeader = Request.Headers["Authorization"].ToString();
        if (authHeader.IsNotNullOrWhiteSpace())
        {
            //jwtBearer
            if (authHeader.StartsWith(JwtBearerDefaults.AuthenticationScheme, StringComparison.OrdinalIgnoreCase))
                return await Context.AuthenticateAsync(JwtBearerDefaults.AuthenticationScheme);

            //Basic
            if (authHeader.StartsWith(BasicDefaults.AuthenticationScheme, StringComparison.OrdinalIgnoreCase))
                return await Context.AuthenticateAsync(BasicDefaults.AuthenticationScheme);
        }
    }
}
  • BasicAuthenticationHandler

    public class BasicAuthenticationHandler : AuthenticationHandler<BasicSchemeOptions>
    {
    protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
    {
        AuthenticateResult authResult;
        var authHeader = Request.Headers["Authorization"].ToString();
        if (authHeader != null && authHeader.StartsWith(BasicDefaults.AuthenticationScheme, StringComparison.OrdinalIgnoreCase))
        {
            var token = authHeader.Substring($"{BasicDefaults.AuthenticationScheme} ".Length).Trim();
            //校验token是否合法
            if (UnpackFromBase64(token, out string userName, out string appId))
            {
                var claims = new[] { new Claim("name", userName), new Claim(ClaimTypes.Role, "partner") };
                var identity = new ClaimsIdentity(claims, BasicDefaults.AuthenticationScheme);
                var claimsPrincipal = new ClaimsPrincipal(identity);
                authResult = AuthenticateResult.Success(new AuthenticationTicket(claimsPrincipal, BasicDefaults.AuthenticationScheme));
    
                var userContext = Context.RequestServices.GetService<IUserContext>();
                userContext.Id = appId.ToLong().Value;
                userContext.Account = userName;
                userContext.Name = userName;
                userContext.RemoteIpAddress = Context.Connection.RemoteIpAddress.MapToIPv4().ToString();
    
                return await Task.FromResult(authResult);
            }
    
            Response.StatusCode = (int)HttpStatusCode.Forbidden;
            authResult = AuthenticateResult.Fail("Invalid Authorization Token");
            return await Task.FromResult(authResult);
        }
    }
    }

    注册授权服务

    这里要注意,认证是认证,授权是授权。

public virtual void AddAuthorization<THandler>() where THandler : AbstractPermissionHandler
{
    _services.AddScoped<IAuthorizationHandler, THandler>();
    _services.AddAuthorization(options =>
    {
      options.AddPolicy(AuthorizePolicy.Default, policy =>
      {
        policy.Requirements.Add(new PermissionRequirement());
      });
    });
}
  • 授权服务核心代码在Adnc.Shared.WebApi工程的 AbstractPermissionHandler.cs 文件

    public abstract class AbstractPermissionHandler : AuthorizationHandler<PermissionRequirement>
    {
    protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context, PermissionRequirement requirement)
    {
        if (context.User.Identity.IsAuthenticated && context.Resource is HttpContext httpContext)
        {
            var authHeader = httpContext.Request.Headers["Authorization"].ToString();
            //Basic认证通过后并且Action允许Basic认证,那么默认有该功能的权限
            if (authHeader != null && authHeader.StartsWith(BasicDefaults.AuthenticationScheme, StringComparison.OrdinalIgnoreCase))
            {
                context.Succeed(requirement);
                return;
            }
    
            //JwtBearer认证通过后并且Action允许JwtBearer认证,还需要检查是否有该功能权限
            var userId = long.Parse(context.User.Claims.First(x => x.Type == JwtRegisteredClaimNames.NameId).Value);
            var validationVersion = context.User.Claims.FirstOrDefault(x => x.Type == "version")?.Value;
            var codes = httpContext.GetEndpoint().Metadata.GetMetadata<PermissionAttribute>().Codes;
            var result = await CheckUserPermissions(userId, codes, validationVersion);
            if (result)
            {
                context.Succeed(requirement);
                return;
            }
        }
        context.Fail();
    }
    
    protected abstract Task<bool> CheckUserPermissions(long userId, IEnumerable<string> codes, string validationVersion);
    }

设置Action特性

    ADNC在注册Endpoint中间件时,设置了全局认证拦截,也就是所有方法默认需要认证通过后才能访问。

app.UseEndpoints(endpoints =>
{
    endpoints.MapControllers().RequireAuthorization();
});
  • 允许匿名访问
    [AllowAnonymous]
    public async Task<ActionResult<UserTokenInfoDto>> LoginAsync([FromBody] UserLoginDto input)
  • 需要deptcreate权限,并且必须是JwtBearer认证方式。
    [Permission(PermissionConsts.Dept.Create)]
    public async Task<ActionResult<long>> CreateAsync([FromBody] DeptCreationDto input)
  • 需要deptlist权限,同时支持JwtBearer与Basic两种认证方式
    [Permission(PermissionConsts.Dept.GetList, PermissionAttribute.JwtWithBasicSchemes)]
    public async Task<ActionResult<List<DeptTreeDto>>> GetListAsync()

我们来看下PermissionAttribute的代码,它派生自AuthorizeAttribute。默认认证方式是JwtBearer。

public class PermissionAttribute : AuthorizeAttribute
{
    public const string JwtWithBasicSchemes = $"{JwtBearerDefaults.AuthenticationScheme},{BasicDefaults.AuthenticationScheme}";

    public string[] Codes { get; set; }

    public PermissionAttribute(string code, string schemes = JwtBearerDefaults.AuthenticationScheme)
        : this(new string[] { code }, schemes)
    {
    }

    public PermissionAttribute(string[] codes, string schemes = JwtBearerDefaults.AuthenticationScheme)
    {
        Codes = codes;
        Policy = AuthorizePolicy.Default;
        if (schemes.IsNullOrEmpty())
            throw new ArgumentNullException(nameof(schemes));
        else
            AuthenticationSchemes = schemes;
    }
}

客户端调用时如何指定认证方式

JwtBearer=>Authorization Bearer xxxxxxxxxxxxx
Basic=>Authorization Basic yyyyyyyyyyy
前端调用走的就是JwtBearer认证。
微服务间同步调用除了鉴权服务走的是JwtBearer认证,其它走的都是Basic认证

  • 要求服务端采用JwtBearer认证

    [Headers("Authorization: Bearer", "Cache: 2000")]
    [Get("/usr/users/{userId}/permissions")]
    Task<ApiResponse<List<string>>> GetCurrenUserPermissionsAsync(long userId, [Query(CollectionFormat.Multi)] IEnumerable<string> permissions, string validationVersion);
    }
  • 要求服务端采用Basic认证

    [Get("/usr/depts")]
    [Headers("Authorization: Basic", "Cache: 2000")]
    Task<ApiResponse<List<DeptRto>>> GeDeptsAsync();

    核心的业务代码在Adnc.Infra.Consul工程的SimpleDiscoveryDelegatingHandler.cs与ConsulDiscoverDelegatingHandler.cs文件。

    public class SimpleDiscoveryDelegatingHandler : DelegatingHandler
    {
        public SimpleDiscoveryDelegatingHandler(IEnumerable<ITokenGenerator> tokenGenerators
            , IMemoryCache memoryCache)
        {
            _tokenGenerators = tokenGenerators;
            _memoryCache = memoryCache;
        }
    
        protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
        {
            var auth = headers.Authorization;
            if (auth != null)
            {
                //这里
                var tokenGenerator = _tokenGenerators.FirstOrDefault(x => x.Scheme.EqualsIgnoreCase(auth.Scheme));
                //这里
                var tokenTxt = tokenGenerator?.Create();
    
                if (!string.IsNullOrEmpty(tokenTxt))
                    request.Headers.Authorization = new AuthenticationHeaderValue(auth.Scheme, tokenTxt);
            }
        }
    }

    WELL DONE,记得 star&&fork
    全文完,ADNC一个可以落地的.NET微服务/分布式开发框架。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注