26.Blazor 身份认证与授权
26.0 基础背景知识 OAuth 2.0 和 OpenID Connect
26.0.1 概说
这两个协议是用于授权和认证的使用最广泛的的协议。OAuth 2.0 用于授权,OpenID Connect 用于认证。OpenID Connect 是 OAuth 2.0 协议之上的标识层,以使 OAuth 适用于认证的用例。
认证(Authentication)是确保通信实体是其所声称的实体。
授权(Authorization)是验证通信实体是否有权访问资源的过程。
换言之,认证关注的是你是谁,授权关注的是你有什么权限。
有两种 OAuth 2.0 授权流程最为常见:服务端应用程序的授权码流程和基于浏览器的应用程序的隐式流程。
26.0.2 OpenID Connect
OpenID Connect 是在 OAuth2.0 协议之上的标识层。它拓展了 OAuth2.0,使得认证方式标准化。
OAuth 不会立即提供用户身份,而是会提供用于授权的访问令牌。 OpenID Connect 使客户端能够通过认证来识别用户,其中,认证在授权服务端执行。它是这样实现的:在向授权服务端发起用户登录和授权告知的请求时,定义一个名叫openid的授权范围。在告知授权服务器需要使用 OpenID Connect 时,openid是必须存在的范围。
客户端发起的用于 OpenID Connect 认证请求 URI 会是如下的形式:
https://accounts.google.com/o/oauth2/v2/auth?
response_type=code&
client_id=your_client_id&
scope=openid%20contacts&
redirect_uri=https%3A//oauth2.example.com/code
该请求的返回结果是客户端可以用来交换访问令牌和 ID 令牌的授权码。如果 OAuth 流程是隐式的,那么授权服务端将直接返回访问令牌和 ID 令牌。
ID 令牌是 JWT,或者又称 JSON Web Token。JWT 是一个编码令牌,它由三部分组成:头部,有效负载和签名。在获得了 ID 令牌后,客户端可以将其解码,并且得到被编码在有效负载中的用户信息,如以下例子所示:
{
"iss": "https://accounts.google.com",
"sub": "10965150351106250715113082368",
"email": "johndoe@example.com",
"iat": 1516239022,
"exp": 1516242922
}
26.0.3 声明(Claim)
ID 令牌的有效负载包括了一些被称作声明的域。基本的声明有:
iss:令牌发布者
sub:用户的唯一标识符
email:用户的邮箱
iat:用 Unix 时间表示的令牌发布时间
exp:Unix 时间表示的令牌到期时间
然而,声明不仅限于上述这些域。由授权服务器对声明进行编码。客户端可以用这些信息来认证用户。
如果客户端需要更多的用户信息,客户端可以指定标准的 OpenID Connect 范围,来告知授权服务端将所需信息包括在 ID 令牌的有效负载中。这些范围包括个人主页(profile)、邮箱(email)、地址(address)和电话(phone)。
26.1 Microsoft 标识平台中的 OAuth 2.0 和 OpenID Connect (OIDC)
26.1.1 身份验证和授权交换通常涉及四方
身份验证和授权交换通常涉及四方。 这些交换通常称为身份验证流。
- Authorazition Server
- Resource Server
- Resource Owner
- Client
26.1.2 持有者令牌 (Bearer Token 或叫不记名令牌)
身份验证流中的各方使用持有者令牌来确保、确认和验证某个主体(用户、主机或服务)并授予或拒绝对受保护资源的访问权限(授权)。 Microsoft 标识平台中的持有者令牌已格式化为 JSON Web 令牌 (JWT)。
标识平台使用三种类型的持有者令牌作为安全令牌:
- 访问令牌 - 访问令牌由授权服务器颁发给客户端应用程序。 客户端将访问令牌传递给资源服务器。 访问令牌包含授权服务器授予客户端的权限。
- ID 令牌 - ID 令牌由授权服务器颁发给客户端应用程序。 客户端在登录用户时使用 ID 令牌,可获取有关用户的基本信息。
- 刷新令牌 - 客户端使用刷新令牌(简称 RT)从授权服务器请求新的访问令牌和 ID 令牌。 代码应将刷新令牌及其字符串内容视为敏感数据,因为它们仅供授权服务器使用。
26.1.3 应用注册
客户端应用需要一种方法来信任 Microsoft 标识平台颁发给自己的安全令牌。 建立信任的第一步是注册应用。 当你注册应用时,标识平台会自动为其分配一些值,而其他值则由你根据应用程序的类型进行配置。
两个最常引用的应用注册设置为:
- 应用程序(客户端)ID - 也称为“应用程序 ID”和“客户端 ID”,此值由标识平台分配给你的应用。 客户端 ID 可对标识平台中的应用进行唯一标识,包含在平台颁发的安全令牌中。
- 重定向 URI - 授权服务器使用重定向 URI,将资源所有者的用户代理(Web 浏览器、移动应用)定向到另一个目标(在完成其交互后)。 例如,在最终用户通过授权服务器进行身份验证之后。 并非所有客户端类型都使用重定向 URI。
应用的注册还保存有关身份验证和授权终结点的信息,这些信息可以在代码中用于获取 ID 和访问令牌。
26.1.4 终结点(理解为就是访问地址)
符合标准的实现提供身份验证和授权服务。 符合标准的授权服务器(例如标识平台)提供一组 HTTP 终结点,供身份验证流中的各方用于执行流。
当你注册或配置应用时,系统会自动生成应用的终结点 URI。 在应用的代码中使用的终结点取决于应用程序的类型以及应支持的标识(帐户类型)。
两个常用的终结点是授权终结点和令牌终结点。 下面是 authorize 和 token 终结点的示例:
# 1.授权终结点-由客户端用于从资源所有者获取授权
https://login.microsoftonline.com/<issuer>/oauth2/v2.0/authorize
# 2.令牌终结点-由客户端用于交换访问令牌的授权授予或刷新令牌
https://login.microsoftonline.com/<issuer>/oauth2/v2.0/token
26.2 框架中的IIdentity和IPrincipal
26.2.1 接口定义
命名空间:System.Security.Principal
- IIdentity 接口有三个属性:
//认证类型,如:微信、支付宝、验证码、jwt、windows等
AuthenticationType,
IsAuthenticated,//是否已经通过认证
Name //认证用户名称
- IPrincipal(已鉴别用户 委托人 负责人)
包括一个属性,一个方法
IIdentity? Identity{get;}
//是否属于指定角色
bool IsInRole(string role);
26.2.1 官方库中的三个实现
- windows 系统实现
var identity = new WindowsIdentity.GetCurrent();
IPrincipal principal = new WindowsPrincipal(identity);
//程序中就可以使用:
Thread.CurrentPrincipal = principal;
//或 如果是asp.net MVC 应用程序
HttpContext.User = principal;
- GenericIdentity 通用实现,较简单
var identity = new GenericIdentity("张三");
IPrincipal principal = new GenericPrincipal(identity,new[]{"admin","role2"});
3. 基于声明的 ClaimsIdentity (重要)
可以通过AddClaims(IEnumberable<Claim?>)
方法自定义扩展内部信息
var identity = new CliamsIdentity("微信");
identity.AddClaim(new(ClaimTypes.Email,"root@qq.com"));
identity.AddClaim(new(ClaimTypes.Name,"张三"));
identity.AddClaim(new(ClaimTypes.Role,"Admin"));
identity.AddClaim(new(ClaimTypes.Role,"Users"));
//自定义增加需要的声明键值对
identity.AddClaim("TypeKey","TypeValue"));
IPrincipal principal = new GenericPrincipal(identity,new[]{"admin","role2"});
//判断当前用户是认证或说登录
bool isAuthenticated = principal.Identity.IsAuthenticated;
//判断当前用户是否拥有某角色
bool isInRole = principal.Identity.IsInRole("Admin");
26.3 Microsoft.Identity 详解
26.3.1 引入包
核心包:Microsoft.Extensions.Identity.Core
仓储抽像包:Microsoft.Extensions.Identity.Stores
仓储EF实现包:Microsoft.AspNetCore.Identity.EntityFrameworkCore
官方UI实现包:Microsoft.AspNetCore.Identity.UI
26.3.2 添加服务
- 1.几个服务介绍
builder.Services.AddMvc();
builder.Services.AddIdentity<TUser,TRole>();
或
builder.Services.AddIdentityCore<TUser>();
或带UI的
builder.Services.AddDefaultIdentity<TUser>();
TUser有官方的默认定义类型:IdentityUser Id为GUID类型
TRole有官方的默认定义类型:IdentityRole Id为GUID类型
两者都可以由开发者继承后进行进一步扩充 - 2.用项目模版创建的项目中主要代码
builder.Services.AddDefaultIdentity<IdentityUser>(options=>
options.SignIn.RequireConfirmedAccount=true)
.AddEntityFrameworkStores<ApplicationsDbContext>();
builder.Service.AddRazorPages();
//授权中间件
app.UseAuthorization();
若需扩展则数据库上下文ApplicationsDbContext代码如下:
public class ApplicationsDbContext:IdentityDbContext<User,Role,string>
{
public ApplicationsDbContext(DbContextOptions options) : base(options)
{ }
protected override void OnModelCreating(ModelBuilder builder)
{
base.OnModelCreating(builder);
builder.Entity<User>().ToTable("MyUsers");
}
}
生成的数据库表有:
AspNetRoleClaims
AspNetRoles
AspNetUserClaims
AspNetRoles
AspNetUsers
//后面这两张表是有关做微信、QQ等第三方登录即Auth2.0时用到的
AspNetUserLogins //相关第三方登录配置
AspNetUserTokens //相关第三方登录Token
26.4 Blazor官方身份认证与授权组件
- 引入Nuget包
Microstoft.AspNetCore.Components.Authorization
- 在_Imports.razor文件中增加全局引用
@using Microstoft.AspNetCore.Components.Authorization
- 组件的使用
该组件结合了微软OpenId认证与授权系统
@context?.User?.Identity
就是接口System.Security.Principal.IIdentity
也是实现System.Security.Claims.ClaimsPrincipal.Identity
再定义一个 AuthenticationStateProvider 的实现(它来自Microstoft.AspNetCore.Components.Authorization)
using Microstoft.AspNetCore.Components.Authorization;
namespace BlazorApp.Client.Pages;
public class MyAuthenticationStateProvider:AuthenticationStateProvider
{
public override Task<AuthenticationState> GetAuthenticationStateAsync()
{//获得用户身份信息,并组装成AuthticationState类型返回
var identity = new ClaimsIdentity("custom",ClaimTypes.Name,ClaimTypes.Role);
identity.AddClaim(new(ClaimTypes.Name,"admin"));
return Task.FromResult(new AuthenticationState(new(identity)));
}
}
然后,在Program.cs文件中注册三个服务(如果是呈现模式是Auto,则需要在Server和Client端都注册该服务):
builder.Serivces.AddScoped<AuthenticationStateProvider,MyAuthenticationStateProvider>();
builder.Services.AddCascadingAuthenticationState();
builder.Services.AddAuthrizationCore();
接下来是UI部分的实现
3.1 使用 AuthorizeView 组件做UI部分
<AuthorizeView>
<NotAuthorized>
<button>登录</button>
</NotAuthorized>
<Authorized>
@context?.User?.Identity?.Name
</Authorized>
</AuthorizeView>
3.2 不使用AuthorizeView 组件做UI
if(User is not null)
{
if(User.Identity.IsAuthenticated)
{
@User?.Identity?.Name
}
else
{
<button>登录</button>
}
}
@code
{
[CascadingParameter]Task<AuthenticationState> StateTask{get;set;}
ClaimsPrincipal? User{get;set;}
protected override asyncTask OnInitializedAsync()
{
User = (await StateTask).User;
}
}
3.3 使用 AuthorizeRouteView 组件,集成在Blazor的路由组件中完成验证
<Router AppAssembly="@typeof(Program).Assembly">
<Found Context="routeData">
<RouteView RouteData="@uouteData" DefaultLayout="@typeof(Layout.MainLayout)" />
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
</Found>
<NotFound>
<p> 没有找到相关路由 </p>
</NotFound>
</Router>
把上面的路由视图组件 RouteView 改为 AuthorizeRouteView 即可变为在整个路由切换过程中要求每个终结点都必须经过认证验证。