Netcore认证授权与IdentityServer4(1)鉴权授权源码


主要概要

这个文章主要来聊一聊Net Core中的认证与授权,以及Net Core生态中非常火热的IdentityServer4这个组件的使用,主要内容如下:

  1. 鉴权授权流程变化
  2. 源码解读鉴权
  3. 源码解读多授权策略
  4. JWT和Identity

鉴权授权流程变化

Http协议

  • 无状态&轻量级
  • 请求–响应式–传输是文本
  • 再次请求—不知道你刚来过
  • 如果想识别—带个签名/证明/工卡/Token—就是放在文本里面

基本流程:其实是为了解决无状态下的用户识别问题

第一,如果是集群,cookie和session如何应对?
Session共享,完成集群的session识别。

第二,如果是分布式的,如何解决呢?甚至是分布式的还是跨局域网络的,第三方的,又该如何解决?
会用Token来解决,可以参考下图Id4的架构。

源码解读鉴权

登录验证-授权

  • 常规的Cookie+Session+Filter模式
  • 基于IAuthorizationFilter,
  • 通过OnAuthorization
  • 发生在请求进入MVC伊始

cookie只是实现session的其中一种方案。虽然是最常用的,但并不是唯一的方法。禁用cookie后还有其他方法存储,比如放在url中。
先看这种土办法如何实现:
第一,要在自定义Attribute,需要继承IAuthorizationFilter,如下:
context.HttpContext.Request.Cookies[“CurrentUser”];会检查cookie中是否有current user。

public class CustomAuthorizationFilterAttribute : Attribute, IAuthorizationFilter
    {
        public void OnAuthorization(AuthorizationFilterContext context)
        {
            if (context.ActionDescriptor.EndpointMetadata.Any(item => item is AllowAnonymousAttribute))
            {
                return;//匿名 不检查
            }
            string sUser = context.HttpContext.Request.Cookies["CurrentUser"];

            if (sUser == null)
            {
                context.Result = new RedirectResult("~/Home/Index");
            }
            else
            {
                //还应该检查下权限
                return;
            }
        }
    }

第二,配置管道,如下:使用cookie和Authorization并且关了Authentication.

public void ConfigureServices(IServiceCollection services)
  ...
  services.AddAuthentication()
  .AddCookie();
  ...
}


public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
  ...
  //app.UseAuthentication();
  app.UseAuthorization();
  ...
}

第三,配置验证标签:无论加不加[CustomAuthorizationFilter]这个标签如何都会去执行CustomAuthorizationFilterAttribute 中的OnAuthorization方法。如果是加了AllowAnonymous这个标签,那就回通过。

[CustomAuthorizationFilter]
public class HomeController : Controller
{
   [AllowAnonymous]
   public IActionResult Login(string name, string password)
   {
      if ("chaoqiang".Equals(name, StringComparison.CurrentCultureIgnoreCase)&& password.Equals("123456"))
      {
          #region Filter
          base.HttpContext.Response.Cookies.Append("CurrentUser", "chaoqiang", new Microsoft.AspNetCore.Http.CookieOptions()
          {
            Expires = DateTime.UtcNow.AddMinutes(30)
          });
          #endregion

          return new JsonResult(new
          {
            Result = true,
            Message = "登录成功"
           });
          }
        else
        {
          return new JsonResult(new
          {
            Result = false,
            Message = "登录失败"
          });
      }
    }

  public IActionResult Privacy()
  {
    return View();
  }
}

上述过程是:当通过login的验证之后,会将cookie放入rsponse中,后面请求就可以访问Privacy这个方法了。

Authentication和Authorization

  • 鉴权——->授权
  • 鉴权:鉴定身份,有没有登录,你是谁
  • 授权:判定有没有权限

鉴权授权里面,是通过AuthenticationHttpContextExtensions的5个方法—其实最终还是要写cookie/session/信息

都是调用的IAuthenticationService,ConfigureService注册进去

#region 最基础认证--自定义Handler
//services.AddAuthenticationCore();
services.AddAuthentication().AddCookie();
#endregion

AddAuthentication又通过AuthenticationCoreServiceCollectionExtensions.AddAuthenticationCore实现,主要是下面三个:

  • IAuthenticationService

  • IAuthenticationHandlerProvider

  • IAuthenticationSchemeProvider

  • 5个方法默认就在AuthenticationService,找handler完成处理

造轮子

理解完源码的脉络,尝试自己写个简化版的鉴权流程:

#region 最基础认证--自定义Handler
services.AddAuthentication().AddCookie();
services.AddAuthenticationCore(options => options.AddScheme<CustomHandler>("CustomScheme", "DemoScheme"));
#endregion

此时对应的中间件app.UseAuthentication();这个就不再需要了,用自定义的处理方式了。

下面看一下对应的 CustomHandler是如何来构造的:

// CustomHandler完成5个动作
// 三个接口,登录/退出分开的原因有远程校验

public class CustomHandler : IAuthenticationHandler, IAuthenticationSignInHandler, IAuthenticationSignOutHandler
{
  public AuthenticationScheme Scheme { get; private set; }
  protected HttpContext Context { get; private set; }

  public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context)
  {
    Scheme = scheme;
    Context = context;
    return Task.CompletedTask;
  }

  public async Task<AuthenticateResult> AuthenticateAsync()
  {
    var cookie = Context.Request.Cookies["CustomCookie"];
    if (string.IsNullOrEmpty(cookie))
    {
      return AuthenticateResult.NoResult();
    }
    return AuthenticateResult.Success(Deserialize(cookie));
  }

  public Task ChallengeAsync(AuthenticationProperties properties)
  {
    //Context.Response.Redirect("/Account/Login");//跳转页面--上端返回json
    return Task.CompletedTask;
  }

  public Task ForbidAsync(AuthenticationProperties properties)
  {
    Context.Response.StatusCode = 403;
    return Task.CompletedTask;
  }

  public Task SignInAsync(ClaimsPrincipal user, AuthenticationProperties properties)
  {
    var ticket = new AuthenticationTicket(user, properties, Scheme.Name);
    Context.Response.Cookies.Append("CustomCookie", Serialize(ticket));
    return Task.CompletedTask;
  }

  public Task SignOutAsync(AuthenticationProperties properties)
  {
    Context.Response.Cookies.Delete("CustomCookie");
    return Task.CompletedTask;
  }

  private AuthenticationTicket Deserialize(string content)
  {
    byte[] byteTicket = System.Text.Encoding.Default.GetBytes(content);
    return TicketSerializer.Default.Deserialize(byteTicket);
    }
  private string Serialize(AuthenticationTicket ticket)
  {
    //需要引入  Microsoft.AspNetCore.Authentication
    byte[] byteTicket = TicketSerializer.Default.Serialize(ticket);
    return Encoding.Default.GetString(byteTicket);
  }
}

第一步:登陆一下 访问login方法,将claim放入到claimIdentity中,再将claimIdentity 放入到ClaimsPrincipal中,然后丢给SignInAsync这个方法去处理,从而找对应的这个CustomHandler。
说明一下:

  • Claim:信息
  • ClaimsIdentity:身份
  • ClaimsPrincipal:一个人可以有多个身份
  • AuthenticationTicket:用户票据
  • 加密一下—写入cookie
public async Task<IActionResult> Login(string name, string password)
{
  //base.HttpContext.RequestServices.
  //IAuthenticationService

  if ("chaoqiang".Equals(name, StringComparison.CurrentCultureIgnoreCase) && password.Equals("123456"))
  {
    //初始化ClaimsIdentity("Custom")带上参数,否则IsAuthenticated是false
    var claimIdentity = new ClaimsIdentity("Custom");
    claimIdentity.AddClaim(new Claim(ClaimTypes.Name, name));
    claimIdentity.AddClaim(new Claim(ClaimTypes.Email, "chaoqiang@qq.com"));
    //claimIdentity.IsAuthenticated = true;
    await base.HttpContext.SignInAsync("CustomScheme", new ClaimsPrincipal(claimIdentity), new AuthenticationProperties
    {
      ExpiresUtc = DateTime.UtcNow.AddMinutes(30),
    });
    return new JsonResult(new
    {
      Result = true,
      Message = "登录成功"
    });
  }
  else
  {
    await Task.CompletedTask;
    return new JsonResult(new
    {
      Result = false,
      Message = "登录失败"
    });
  }
}

第二步,鉴权,访问AuthenticateAsync方法,通过scheme告诉Authenticate用对应的处理器,比如这里是在cookie中鉴权。这里的scheme对应的handler就是在startup中注入的CustomeScheme对应CustomeHandler,把Principal给Http.Context.User,就完成了鉴权的过程。

public async Task<IActionResult> Authentication()
{
  var result = await base.HttpContext.AuthenticateAsync("CustomScheme");
  if (result?.Principal != null)
  {
    base.HttpContext.User = result.Principal;
    return new JsonResult(new
    {
      Result = true,
      Message = $"认证成功,包含用户{base.HttpContext.User.Identity.Name}"
    });
  }
  else
  {
    return new JsonResult(new
    {
      Result = true,
      Message = $"认证失败,用户未登录"
    });
  }
}

第三步,授权,一般是先鉴权,然后授权,也就是做一些检查。

public async Task<IActionResult> Authorization()
{
  var result = await base.HttpContext.AuthenticateAsync("CustomScheme");
  if (result?.Principal == null)
  {
    return new JsonResult(new
    {
      Result = true,
      Message = $"认证失败,用户未登录"
    });
  }
  else
  {
    base.HttpContext.User = result.Principal;
  }

  //授权
  var user = base.HttpContext.User;
  if (user?.Identity?.IsAuthenticated ?? false)
  {
    if (!user.Identity.Name.Equals("Eleven", StringComparison.OrdinalIgnoreCase))
    {
      await base.HttpContext.ForbidAsync("CustomScheme");
      return new JsonResult(new
      {
        Result = false,
        Message = $"授权失败,用户{base.HttpContext.User.Identity.Name}没有权限"
      });
  }
  else
  {
    return new JsonResult(new
    {
      Result = false,
      Message = $"授权成功,用户{base.HttpContext.User.Identity.Name}具备权限"
      });
    }
  }
  else
  {
    await base.HttpContext.ChallengeAsync("CustomScheme");
    return new JsonResult(new
    {
      Result = false,
      Message = $"授权失败,没有登录"
    });
  }
}

比如,下面有一个需要授权的页面,来访问:

public async Task<IActionResult> Info()
{
  var result = await base.HttpContext.AuthenticateAsync("CustomScheme");
  if (result?.Principal == null)
  {
    return new JsonResult(new
    {
      Result = true,
      Message = $"认证失败,用户未登录"
    });
  }
  else
  {
    base.HttpContext.User = result.Principal;
  }

  //授权
  var user = base.HttpContext.User;
  if (user?.Identity?.IsAuthenticated ?? false)
  {
    if (!user.Identity.Name.Equals("Eleven", StringComparison.OrdinalIgnoreCase))
    {
      await base.HttpContext.ForbidAsync("CustomScheme");
      return new JsonResult(new
      {
        Result = false,
        Message = $"授权失败,用户{base.HttpContext.User.Identity.Name}没有权限"
      });
  }
  else
  {
    //有权限
    return new JsonResult(new
    {
      Result = true,
      Message = $"授权成功,正常访问页面!",
      Html = "Hello Root!"
    });
   }
  }
  else
  {
    await base.HttpContext.ChallengeAsync("CustomScheme");
    return new JsonResult(new
    {
      Result = false,
      Message = $"授权失败,没有登录"
    });
  }
}

小结一下(其实还是那套东西)

  • 登录写入凭证
  • 鉴权就是找出用户
  • 授权就是判断权限
  • 退出就是清理凭证

再回顾一下,Asp .Net Core标准内置的鉴权授权是如何来配置的?
1.configureservice里面

services.AddAuthentication(options =>
{
  options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;//不能少 通过scheme找到handler
  options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
  options.DefaultChallengeScheme = "Cookie/Login";
})
.AddCookie(options =>{});

2.Configure里要启用鉴权授权中间件

app.UseAuthentication();
app.UseAuthorization();

3.在controller或者Action上添加[Authorize]标签

[AllowAnonymous]
public async Task<IActionResult> Login(string name, string password)
{
  if ("chaoqiang".Equals(name, StringComparison.CurrentCultureIgnoreCase) && password.Equals("123456"))
  {
    //var claimIdentity = new ClaimsIdentity("Cookie");
    //claimIdentity.AddClaim(new Claim(ClaimTypes.Name, name));
    //claimIdentity.AddClaim(new Claim(ClaimTypes.Email, "chaoqiang@qq.com"));

    #region 升级JwtClaimTypes  cookie会短一点
    var claimIdentity = new ClaimsIdentity("Cookie", JwtClaimTypes.Name, JwtClaimTypes.Role);
    claimIdentity.AddClaim(new Claim(JwtClaimTypes.Name, name));
    claimIdentity.AddClaim(new Claim(JwtClaimTypes.Email, "chaoqiang@qq.com"));
    #endregion

    await base.HttpContext.SignInAsync(new ClaimsPrincipal(claimIdentity), new AuthenticationProperties
    {
      ExpiresUtc = DateTime.UtcNow.AddMinutes(30),
    });//省略scheme
    return new JsonResult(new
    {
      Result = true,
      Message = "登录成功"
    });
  }
  else
  {
    await Task.CompletedTask;
    return new JsonResult(new
    {
      Result = false,
      Message = "登录失败"
    });
  }
}

4.鉴权授权,一起放在这个AuthenticationAuthorization方法中,其中base.HttpContext.AuthenticateAsync(); 这一步可以不用执行,因为中间件根据标签[Authorize]已经把事情做完了。

public async Task<IActionResult> AuthenticationAuthorization()
{
  var result = await base.HttpContext.AuthenticateAsync();//默认CookieAuthenticationDefaults.AuthenticationScheme
  if (result?.Principal == null)
  {
    return new JsonResult(new
    {
      Result = true,
      Message = $"认证失败,用户未登录"
    });
  }
  else
  {
    base.HttpContext.User = result.Principal;
  }

  //授权
  var user = base.HttpContext.User;
  if (user?.Identity?.IsAuthenticated ?? false)
  {
    if (!user.Identity.Name.Equals("Eleven", StringComparison.OrdinalIgnoreCase))
  {
    await base.HttpContext.ForbidAsync();
    return new JsonResult(new
    {
      Result = false,
      Message = $"授权失败,用户{base.HttpContext.User.Identity.Name}没有权限"
    });
  }
  else
  {
    return new JsonResult(new
    {
      Result = false,
      Message = $"授权成功,用户{base.HttpContext.User.Identity.Name}具备权限"
     });
    }
   }
  else
  {
    await base.HttpContext.ChallengeAsync();
    return new JsonResult(new
    {
      Result = false,
      Message = $"授权失败,没有登录"
    });
  }
}

5.访问有授权要求的页面

[Authorize]
public IActionResult Info()
{
  //base.HttpContext.User.Identities 认证过之后就可以用了
  return View();
}

app.UseAuthentication中间件

可以看到是AuthAppBuilderExtensions这个类有个注册方法:

public static IApplicationBuilder UseAuthentication(
      this IApplicationBuilder app);
  • AuthAppBuilderExtensions—AuthenticationMiddleware
  • 中间件一定是完成用户信息解析赋值给ContextUser
  • 期间会优先执行IAuthenticationRequestHandler—Remote,远程校验(AuthenticationHandler本地基类 和 RemoteAuthenticationHandler远程)

service.AddAuthenication 服务注册

  • 调用AddAuthenicationCore 其实就是通过AuthenicationHandlerProvider 指定具体的Handler
  • 注册handler,如何处理;

AddCookie

使用Cookie,在这里指定了CookieAuthenticationHandler作为处理逻辑

来了个Events,就是以前的管道模型套路–

又来了个SessionStore(将信息存下来,换个更简洁的ticket返回前端)(不是session,只是类似session的思路)

PS: 详细的过程需要研读源码。

可以看下如何做cookie的扩展,
SessionStore—可以MemoryCache 也可以Redis;
Event—类似于Asp.Net 的管道模型—可以扩展下

services.AddScoped<ITicketStore, MemoryCacheTicketStore>();
services.AddMemoryCache();

services.AddAuthentication(options =>
{
  options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;//不能少 通过scheme找到handler
  options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
  options.DefaultChallengeScheme = "Cookie/Login";
  })
  .AddCookie(options =>
  {
    //信息存在服务端--把key写入cookie--类似session
    options.SessionStore = services.BuildServiceProvider().GetService<ITicketStore>();
    options.Events = new CookieAuthenticationEvents()
    {
      OnSignedIn = new Func<CookieSignedInContext, Task>(
      async context =>
      {
        Console.WriteLine($"{context.Request.Path} is OnSignedIn");
        await Task.CompletedTask;
      }),
      OnSigningIn = async context =>
      {
        Console.WriteLine($"{context.Request.Path} is OnSigningIn");
        await Task.CompletedTask;
      },
      OnSigningOut = async context =>
      {
        Console.WriteLine($"{context.Request.Path} is OnSigningOut");
        await Task.CompletedTask;
      }
    };//扩展事件
});

源码解读多授权策略

  • 鉴权解析用户信息
  • 授权要求达成某个条件:
  1. Scheme
  2. Role
  3. Policy

基于Scheme形式的授权

先看一下如何配置:

  • 2个Use
  • 1个Add
  • 指定Scheme
  • 指定Controller/Action

说到底就是:起个名字,保持统一 就可以完成授权。

services.AddAuthentication(options =>
{
  options.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
  //不能少,signin signout Authenticate都是基于Scheme
  options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
{
  options.LoginPath = "/Authorization/Index";
  options.AccessDeniedPath = "/Authorization/Index";
});
#region 基于CookieAuthentication
app.UseAuthentication();//鉴权
#endregion

app.UseAuthorization();
// 需要授权的页面
[Authorize(AuthenticationSchemes = "Cookies")]//
public IActionResult Info()
{
  return View();
}

基于Role的形式授权

看一下具体的配置:管道和注册服务的配置和上面一样,不同的地方在于登录的时候会把角色放到identity里面:

[AllowAnonymous]
public async Task<IActionResult> Login(string name, string password, string role = "Admin")
{
  //base.HttpContext.RequestServices.
  //IAuthenticationService
  if ("chaoqiang".Equals(name, StringComparison.CurrentCultureIgnoreCase) && password.Equals("123456"))
  {
    var claimIdentity = new ClaimsIdentity("Custom");
    claimIdentity.AddClaim(new Claim(ClaimTypes.Name, name));
    //claimIdentity.AddClaim(new Claim(ClaimTypes.Email, "chaoqiang@qq.com"));
    claimIdentity.AddClaim(new Claim(ClaimTypes.Email, "57265177@qq.com"));
    claimIdentity.AddClaim(new Claim(ClaimTypes.Role, role));

    await base.HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, new ClaimsPrincipal(claimIdentity), new AuthenticationProperties
    {
      ExpiresUtc = DateTime.UtcNow.AddMinutes(30),
    });//登录为默认的scheme  cookies
    return new JsonResult(new
    {
      Result = true,
      Message = "登录成功"
    });
  }
  else
  {
    await Task.CompletedTask;
    return new JsonResult(new
    {
      Result = false,
      Message = "登录失败"
    });
  }
}
[Authorize(Roles = "Admin")]
public IActionResult InfoAdmin()
{
  return View();
}

[Authorize(Roles = "User")]
public IActionResult InfoUser()
{
  return View();
}

[Authorize(Roles = "Admin,User")]
public IActionResult InfoAdminUser()
{
  return View();
}

基于Policy的授权

管道配置与之前两种一致,不同之处在于service注入这有一个关于Authorization的注入。

#region 基于策略授权
services.AddAuthentication(options =>
{
  options.DefaultAuthenticateScheme =CookieAuthenticationDefaults.AuthenticationScheme;//不能少
  options.DefaultSignInScheme = CookieAuthenticationDefaults.AuthenticationScheme;
})
.AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options =>
{
  options.LoginPath = "/Authorization/Index";
  options.AccessDeniedPath = "/Authorization/Index";
});
services.AddAuthorization(options =>
{
  options.AddPolicy("AdminPolicy",
    policyBuilder => policyBuilder
    .RequireRole("Admin")   //Claim的Role是Admin
    .RequireUserName("Eleven")  //Claim的Name是Eleven
    .RequireClaim(ClaimTypes.Email) //必须有某个Cliam
  );//内置

  options.AddPolicy("UserPolicy",
    policyBuilder => policyBuilder.RequireAssertion(context =>
    context.User.HasClaim(c => c.Type == ClaimTypes.Role)
    && context.User.Claims.First(c => c.Type.Equals(ClaimTypes.Role)).Value == "Admin")
  );//自定义
});
#region Policy
[Authorize(AuthenticationSchemes = "Cookies", Policy = "AdminPolicy")]
public IActionResult InfoAdminPolicy()
{
  return View();
}

[Authorize(AuthenticationSchemes = "Cookies", Policy = "UserPolicy")]
public IActionResult InfoUserPolicy()
{
  return View();
}

[Authorize(AuthenticationSchemes = "Cookies", Policy = "QQEmail")]
public IActionResult InfoQQEmail()
{
  return View();
}

[Authorize(AuthenticationSchemes = "Cookies", Policy = "DoubleEmail")]
public IActionResult InfoDoubleEmail()
{
  return View();
}
#endregion

溯源:

  • 从AddMVC说起– AddAuthorization
  • PolicyServiceCollectionExtensions
  • AuthorizationServiceCollectionExtensions
  • 常规的注册多个对象
  • AddAuthorizationPolicyEvaluator

AddAuthorization会看到Option里面就是字典存储Policy和提供名称获取

AuthorizationPolicy:

  • CombineAsync:把特性里面的三个属性,转成Policy,
  • 其实Scheme/Role其实都是转换成Policy
  • 发现里面有一组Requirements,是校验规则的实现
  • 生成一个Policy—PolicyBuilder+Requirements
  • AuthorizationPolicyBuilder:建造者,组装Policy

下面自己定义一个Requirement,使得两种邮箱都能支持:

JWT和Identity


文章作者: Chaoqiang
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Chaoqiang !
评论
 上一篇
Netcore认证授权与IdentityServer4(2)JWT Netcore认证授权与IdentityServer4(2)JWT
主要概要这个文章主要来聊一聊Net Core中的认证与授权,以及Net Core生态中非常火热的IdentityServer4这个组件的使用,主要内容如下: 鉴权授权流程变化 源码解读鉴权 源码解读多授权策略 JWT和Identity
下一篇 
NetCore 开发实战(2)——微服务实战 NetCore 开发实战(2)——微服务实战
26 | 工程结构概览:定义应用分层及依赖关系从这一节开始进入微服务实战部分 这一节主要探讨工程的结构和应用的分层 在应用的分层这里定义了四个层次: 领域模型层 基础设施层 应用层 共享层 可以通过代码来看一下 共享层一共建立三
  目录