Identity Server系列目录
Blazor WebAssembly项目提供了丰富的认证和授权支持,参考微软官网两篇文章,编写一个Blazor WebAssembly项目访问之前已经建好的Identity Server 4服务器。
https://docs.microsoft.com/zh-cn/aspnet/core/blazor/security/webassembly/standalone-with-authentication-library?view=aspnetcore-6.0&tabs=visual-studio
https://docs.microsoft.com/zh-cn/aspnet/core/blazor/security/webassembly/hosted-with-identity-server?view=aspnetcore-6.0&tabs=visual-studio
新建Blazor WebAssembly项目WebAsmOidc,身份验证类型=个人账户,无托管主机。框架自动引用认证相关的NuGet类库,自动生成认证相关的文件,改一下就能用。
appsettings.Development.json改为访问已有的Identity Server 4服务器
"Local": {
"Authority": "https://localhost:5001/",
"ClientId": "WebAssemblyOidc",
"DefaultScopes": [
"scope1"
],
"PostLogoutRedirectUri": "/",
"ResponseType": "code"
}
launchSettings.json改一下项目的端口
"applicationUrl": "https://localhost:5801;http://localhost:5800",
AspNetId4Web项目增加Blazor WebAssembly项目的客户端配置,因为WebAssembly代码在浏览器里边可以看到,没有必要用秘钥了
// Blazor WebAssembly客户端
new Client
{
ClientId = "WebAssemblyOidc",
ClientName = "WebAssemblyOidc",
RequireClientSecret = false,
AllowedGrantTypes = GrantTypes.Code,
AllowedScopes ={ "openid", "profile", "scope1", },
//网页客户端运行时的URL
AllowedCorsOrigins = {
"https://localhost:5801",
},
//登录成功之后将要跳转的网页客户端的URL
RedirectUris = {
"https://localhost:5801/authentication/login-callback",
},
//退出登录之后将要跳转的网页客户端的URL
PostLogoutRedirectUris = {
"https://localhost:5801",
},
},
同时运行AspNetId4Web项目、WebAsmOidc项目,在WebAsmOidc项目登录,可以跳转到Identity Server 4登录页面,并成功返回。
参考微软官网的例子,把角色数组拆分为单个角色。
/// Program.cs注册工厂,注意角色的名称也要转换 builder.Services.AddOidcAuthentication(optiOns=> 给FetchData.razor页面增加认证要求 @using Microsoft.AspNetCore.Authorization 再次运行2个项目,测试alice有Admin权限,可以访问FetchData.razor页面,bob不行。 参考微软官网定义,在Program.cs访问资源服务器的HttpClient参数,框架会自动获取Access Token到HttpClient的Header。 https://docs.microsoft.com/zh-cn/aspnet/core/blazor/security/webassembly/additional-scenarios?view=aspnetcore-6.0#configure-the-httpclient-handler AuthorizationMessageHandler 是一个 DelegatingHandler,用于将访问令牌附加到传出 HttpResponseMessage 实例。 令牌是使用由框架注册的 IAccessTokenProvider 服务获取的。 可以使用 ConfigureHandler 方法将 AuthorizationMessageHandler 配置为授权的 URL、作用域和返回 URL。 ConfigureHandler 配置此处理程序,以使用访问令牌授权出站 HTTP 请求。 仅当至少有一个授权 URL 是请求 URI (HttpRequestMessage.RequestUri) 的基 URI 时,才附加访问令牌。 builder.Services.AddHttpClient("MyWebApi", FetchData.razor页面改为访问MyWebApi项目获取数据 protected override async Task OnInitializedAsync() 同时运行AspNetId4Web项目、MyWebAPi项目、WebAsmOidc项目,用管理员alice登录,访问FetchData.razor页面,提示跨域访问错误。 blazor.webassembly.js:1 info: System.Net.Http.HttpClient.MyWebApi.ClientHandler[100] Sending HTTP request GET https://localhost:5601/WeatherForecast fetchdata:1 Access to fetch at 'https://localhost:5601/WeatherForecast' from origin 'https://localhost:5801' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled. :5601/WeatherForecast:1 参考微软官网给MyWebApi项目增加跨域共享配置 https://docs.microsoft.com/zh-cn/aspnet/core/blazor/call-web-api?view=aspnetcore-6.0&pivots=webassembly#call-web-api-example app.UseCors(policy => Fiddler抓包看一下,WebAsmOidc项目访问了2次MyWebAPi项目。 第一次是OPTIONS方法,获取MyWebAPi项目支持的功能。 OPTIONS https://localhost:5601/WeatherForecast HTTP/1.1 第二次才是查询数据。 GET https://localhost:5601/WeatherForecast HTTP/1.1 Blazor WebAssembly项目访问跨域的资源Web Api配置比较麻烦,这是由浏览器安全机制规定的,简单的Blazor WebAssembly项目最好还是配合托管主机一起使用,网页客户端只访问配套的托管主机服务端,对于第三方资源Web Api也通过托管主机中转,托管主机起到类似网关的作用。托管主机是后台服务器,不受浏览器跨域访问的约束。这样网页客户端的HttpClient配置比较简单,资源Web Api也不用配置跨域共享,当然这个会牺牲性能,有利有弊。 访问托管主机的简单配置: builder.Services.AddHttpClient("MyWebApi", DEMO代码地址:https://gitee.com/woodsun/blzid4
/// 自定义用户工厂
/// 在 Client 应用中,创建自定义用户工厂。 Identity 服务器在一个 role 声明中发送多个角色作为 JSON 数组。 单个角色在该声明中作为单个字符串值进行发送。
/// 工厂为每个用户的角色创建单个 role 声明。
/// https://docs.microsoft.com/zh-cn/aspnet/core/blazor/security/webassembly/hosted-with-identity-server?view=aspnetcore-6.0&tabs=visual-studio#name-and-role-claim-with-api-authorization
///
public class CustomUserFactory : AccountClaimsPrincipalFactory
{
public CustomUserFactory(IAccessTokenProviderAccessor accessor)
: base(accessor)
{
}
public override async ValueTask
RemoteUserAccount account,
RemoteAuthenticationUserOptions options)
{
var user = await base.CreateUserAsync(account, options);
if (user.Identity.IsAuthenticated)
{
var identity = (ClaimsIdentity)user.Identity;
var roleClaims = identity.FindAll(identity.RoleClaimType).ToArray();
if (roleClaims.Any())
{
foreach (var existingClaim in roleClaims)
{
identity.RemoveClaim(existingClaim);
}
var rolesElem = account.AdditionalProperties[identity.RoleClaimType];
if (rolesElem is JsonElement roles)
{
if (roles.ValueKind == JsonValueKind.Array)
{
foreach (var role in roles.EnumerateArray())
{
identity.AddClaim(new Claim(options.RoleClaim, role.GetString()));
}
}
else
{
identity.AddClaim(new Claim(options.RoleClaim, roles.GetString()));
}
}
}
}
return user;
}
{
// Configure your authentication provider options here.
// For more information, see https://aka.ms/blazor-standalone-auth
builder.Configuration.Bind("Local", options.ProviderOptions);
//这里是个ClaimType的转换,Identity Server的ClaimType和Blazor中间件使用的名称有区别,需要统一。
options.UserOptions.NameClaim = "name";
options.UserOptions.RoleClaim = "role";
})
.AddAccountClaimsPrincipalFactory
@attribute [Authorize(Roles = "Admin")]获取Access Token访问资源Web Api
client => client.BaseAddress = new Uri("https://localhost:5601"))
.AddHttpMessageHandler(sp => sp.GetRequiredService
.ConfigureHandler(
authorizedUrls: new[] { "https://localhost:5601" },
scopes: new[] { "scope1" }));
builder.Services.AddScoped(sp => sp.GetRequiredService
.CreateClient("MyWebApi"));
{
//forecasts = await Http.GetFromJsonAsync
try
{
forecasts = await Http.GetFromJsonAsync
}
catch (AccessTokenNotAvailableException exception)
{
exception.Redirect();
}
}资源Web Api配置跨域共享
policy.WithOrigins("https://localhost:5801")
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials());
Host: localhost:5601
Connection: keep-alive
Accept: */*
Access-Control-Request-Method: GET
Access-Control-Request-Headers: authorization
Origin: https://localhost:5801
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.51 Safari/537.36 Edg/99.0.1150.39
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-site
Sec-Fetch-Dest: empty
Referer: https://localhost:5801/
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
HTTP/1.1 204 No Content
Date: Wed, 16 Mar 2022 12:13:16 GMT
Server: Kestrel
Access-Control-Allow-Credentials: true
Access-Control-Allow-Headers: authorization
Access-Control-Allow-Methods: GET
Access-Control-Allow-Origin: https://localhost:5801
Host: localhost:5601
Connection: keep-alive
sec-ch-ua: " Not A;Brand";v="99", "Chromium";v="99", "Microsoft Edge";v="99"
authorization: Bearer eyJ……ihg
sec-ch-ua-mobile: ?0
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.51 Safari/537.36 Edg/99.0.1150.39
sec-ch-ua-platform: "Windows"
Accept: */*
Origin: https://localhost:5801
Sec-Fetch-Site: same-site
Sec-Fetch-Mode: cors
Sec-Fetch-Dest: empty
Referer: https://localhost:5801/
Accept-Encoding: gzip, deflate, br
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Wed, 16 Mar 2022 12:13:17 GMT
Server: Kestrel
Access-Control-Allow-Credentials: true
Access-Control-Allow-Origin: https://localhost:5801
Transfer-Encoding: chunked
1ee
[{"date":"2022-03-17T20:13:18.0963201+08:00","temperatureC":13,"temperatureF":55,"summary":"Cool"},{"date":"2022-03-18T20:13:18.0966368+08:00","temperatureC":24,"temperatureF":75,"summary":"Balmy"},{"date":"2022-03-19T20:13:18.0966403+08:00","temperatureC":-17,"temperatureF":2,"summary":"Mild"},{"date":"2022-03-20T20:13:18.0966405+08:00","temperatureC":15,"temperatureF":58,"summary":"Chilly"},{"date":"2022-03-21T20:13:18.0966406+08:00","temperatureC":10,"temperatureF":49,"summary":"Mild"}]
0问题
client => client.BaseAddress = new Uri(builder.HostEnvironment.BaseAddress))
.AddHttpMessageHandler
builder.Services.AddScoped(sp => sp.GetRequiredService
.CreateClient("MyWebApi"));