主要概要
这个文章主要来聊一聊Net Core中的认证与授权,以及Net Core生态中非常火热的IdentityServer4这个组件的使用,主要内容如下:
- 鉴权授权流程变化
- 源码解读鉴权
- 源码解读多授权策略
- JWT和Identity
鉴权授权流程变化
Http协议
- 无状态&轻量级
- 请求–响应式–传输是文本
- 再次请求—不知道你刚来过
- 如果想识别—带个签名/证明/工卡/Token—就是放在文本里面
Cookie/Session
基本流程:其实是为了解决无状态下的用户识别问题
第一,如果是集群,cookie和session如何应对?
Session共享,完成集群的session识别。
第二,如果是分布式的,如何解决呢?甚至是分布式的还是跨局域网络的,第三方的,又该如何解决?
会用Token来解决,可以参考下图Id4的架构。
源码解读鉴权
登录验证-授权
- 常规的Cookie+Session+Filter模式
- 基于IAuthorizationFilter,
- 通过OnAuthorization
- 发生在请求进入MVC伊始
Cookie+Session方式
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;
}
};//扩展事件
});
源码解读多授权策略
- 鉴权解析用户信息
- 授权要求达成某个条件:
- Scheme
- Role
- 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,使得两种邮箱都能支持: