目录:
- OpenID 与 OAuth2 基础知识
- Blazor wasm Google 登录
- Blazor wasm Gitee 码云登录
- Blazor OIDC 单点登录授权实例1-建立和配置IDS身份验证服务
- Blazor OIDC 单点登录授权实例2-登录信息组件wasm
- Blazor OIDC 单点登录授权实例3-服务端管理组件
- Blazor OIDC 单点登录授权实例4 - 部署服务端/独立WASM端授权
- Blazor OIDC 单点登录授权实例5 - 独立SSR App (net8 webapp)端授权
- Blazor OIDC 单点登录授权实例6 - Winform 端授权
- Blazor OIDC 单点登录授权实例7 - Blazor hybird app 端授权
(目录暂时不更新,跟随合集标题往下走)
源码
BlazorOIDC.WinForms
建立 BlazorOIDC.WinForms 工程
自行安装 Vijay Anand E G 模板,快速建立 Blazor WinForms 工程, 命名为 BlazorOIDC.WinForms
引用以下库
<ItemGroup><PackageReference Include="Microsoft.AspNetCore.Components.Authorization" Version="8.0.4" /><PackageReference Include="Microsoft.AspNetCore.Components.WebView.WindowsForms" Version="8.*" /><PackageReference Include="Microsoft.Extensions.Hosting" Version="8.*" /><FrameworkReference Include="Microsoft.AspNetCore.App"></FrameworkReference><PackageReference Include="IdentityModel.OidcClient" Version="5.2.1" /></ItemGroup>
_Imports.razor 加入引用
@using Microsoft.AspNetCore.Components.Authorization
Main.razor 加入授权
完整代码
<CascadingAuthenticationState><Router AppAssembly="@GetType().Assembly"><Found Context="routeData"><RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" /></Found><NotFound><LayoutView Layout="@typeof(MainLayout)"><p>Sorry, there's nothing at this address.</p></LayoutView></NotFound></Router>
</CascadingAuthenticationState>
添加Oidc授权配置
新建文件 ExternalAuthStateProvider.cs
完整代码
using IdentityModel.OidcClient;
using Microsoft.AspNetCore.Components.Authorization;
using System.Security.Claims;namespace BlazorOIDC.WinForms;public class ExternalAuthStateProvider : AuthenticationStateProvider
{private readonly Task<AuthenticationState> authenticationState;public ExternalAuthStateProvider(AuthenticatedUser user) =>authenticationState = Task.FromResult(new AuthenticationState(user.Principal));private ClaimsPrincipal currentUser = new ClaimsPrincipal(new ClaimsIdentity());public override Task<AuthenticationState> GetAuthenticationStateAsync() =>Task.FromResult(new AuthenticationState(currentUser));public Task<AuthenticationState> LogInAsync(){var loginTask = LogInAsyncCore();NotifyAuthenticationStateChanged(loginTask);return loginTask;async Task<AuthenticationState> LogInAsyncCore(){var user = await LoginWithExternalProviderAsync();currentUser = user;return new AuthenticationState(currentUser);}}private async Task<ClaimsPrincipal> LoginWithExternalProviderAsync(){/*提供 Open ID/MSAL 代码以对用户进行身份验证。查看您的身份提供商的文档以获取详细信息。根据新的声明身份返回新的声明主体。*/string authority = "https://localhost:5001/";//string authority = "https://ids2.app1.es/"; //真实环境string api = $"{authority}WeatherForecast";string clientId = "Blazor5002";OidcClient? _oidcClient;HttpClient _apiClient = new HttpClient { BaseAddress = new Uri(api) };var browser = new SystemBrowser(5002);var redirectUri = string.Format($"http://localhost:{browser.Port}/authentication/login-callback");var redirectLogoutUri = string.Format($"http://localhost:{browser.Port}/authentication/logout-callback");var options = new OidcClientOptions{Authority = authority,ClientId = clientId,RedirectUri = redirectUri,PostLogoutRedirectUri = redirectLogoutUri,Scope = "BlazorWasmIdentity.ServerAPI openid profile",//Scope = "Blazor7.ServerAPI openid profile",Browser = browser,Policy = new Policy { RequireIdentityTokenSignature = false }};_oidcClient = new OidcClient(options);var result = await _oidcClient.LoginAsync(new LoginRequest());ShowResult(result);var authenticatedUser = result.User;return authenticatedUser;}private static void ShowResult(LoginResult result, bool showToken = false){if (result.IsError){Console.WriteLine("\n\nError:\n{0}", result.Error);return;}Console.WriteLine("\n\nClaims:");foreach (var claim in result.User.Claims){Console.WriteLine("{0}: {1}", claim.Type, claim.Value);}if (showToken){Console.WriteLine($"\nidentity token: {result.IdentityToken}");Console.WriteLine($"access token: {result.AccessToken}");Console.WriteLine($"refresh token: {result?.RefreshToken ?? "none"}");}}public Task Logout(){currentUser = new ClaimsPrincipal(new ClaimsIdentity());NotifyAuthenticationStateChanged(Task.FromResult(new AuthenticationState(currentUser)));return Task.CompletedTask;}
}public class AuthenticatedUser
{public ClaimsPrincipal Principal { get; set; } = new();
}
添加Oidc浏览器授权方法
新建文件 SystemBrowser.cs
完整代码
using IdentityModel.OidcClient.Browser;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using System.Diagnostics;
using System.Net;
using System.Net.Sockets;
using System.Runtime.InteropServices;
using System.Text;
#nullable disablenamespace BlazorOIDC.WinForms;public class SystemBrowser : IBrowser
{public int Port { get; }private readonly string _path;public SystemBrowser(int? port = null, string path = null){_path = path;if (!port.HasValue){Port = GetRandomUnusedPort();}else{Port = port.Value;}}private int GetRandomUnusedPort(){var listener = new TcpListener(IPAddress.Loopback, 0);listener.Start();var port = ((IPEndPoint)listener.LocalEndpoint).Port;listener.Stop();return port;}public async Task<BrowserResult> InvokeAsync(BrowserOptions options, CancellationToken cancellationToken = default){using (var listener = new LoopbackHttpListener(Port, _path)){OpenBrowser(options.StartUrl);try{var result = await listener.WaitForCallbackAsync();if (string.IsNullOrWhiteSpace(result)){return new BrowserResult { ResultType = BrowserResultType.UnknownError, Error = "Empty response." };}return new BrowserResult { Response = result, ResultType = BrowserResultType.Success };}catch (TaskCanceledException ex){return new BrowserResult { ResultType = BrowserResultType.Timeout, Error = ex.Message };}catch (Exception ex){return new BrowserResult { ResultType = BrowserResultType.UnknownError, Error = ex.Message };}}}public static void OpenBrowser(string url){try{Process.Start(url);}catch{// hack because of this: https://github.com/dotnet/corefx/issues/10361if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)){url = url.Replace("&", "^&");Process.Start(new ProcessStartInfo("cmd", $"/c start {url}") { CreateNoWindow = true });}else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)){Process.Start("xdg-open", url);}else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)){Process.Start("open", url);}else{throw;}}}
}public class LoopbackHttpListener : IDisposable
{const int DefaultTimeout = 60 * 5; // 5 mins (in seconds)IWebHost _host;TaskCompletionSource<string> _source = new TaskCompletionSource<string>();public string Url { get; }public LoopbackHttpListener(int port, string path = null){path = path ?? string.Empty;if (path.StartsWith("/")) path = path.Substring(1);Url = $"http://localhost:{port}/{path}";_host = new WebHostBuilder().UseKestrel().UseUrls(Url).Configure(Configure).Build();_host.Start();}public void Dispose(){Task.Run(async () =>{await Task.Delay(500);_host.Dispose();});}void Configure(IApplicationBuilder app){app.Run(async ctx =>{if (ctx.Request.Method == "GET"){await SetResultAsync(ctx.Request.QueryString.Value, ctx);}else if (ctx.Request.Method == "POST"){if (!ctx.Request.ContentType.Equals("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase)){ctx.Response.StatusCode = 415;}else{using (var sr = new StreamReader(ctx.Request.Body, Encoding.UTF8)){var body = await sr.ReadToEndAsync();await SetResultAsync(body, ctx);}}}else{ctx.Response.StatusCode = 405;}});}private async Task SetResultAsync(string value, HttpContext ctx){try{ctx.Response.StatusCode = 200;ctx.Response.ContentType = "text/html; charset=utf-8";await ctx.Response.WriteAsync("<h1>您现在可以返回应用程序.</h1>");await ctx.Response.Body.FlushAsync();_source.TrySetResult(value);}catch(Exception ex){Console.WriteLine(ex.ToString());ctx.Response.StatusCode = 400;ctx.Response.ContentType = "text/html; charset=utf-8";await ctx.Response.WriteAsync("<h1>无效的请求.</h1>");await ctx.Response.Body.FlushAsync();}}public Task<string> WaitForCallbackAsync(int timeoutInSeconds = DefaultTimeout){Task.Run(async () =>{await Task.Delay(timeoutInSeconds * 1000);_source.TrySetCanceled();});return _source.Task;}
}
Shared 文件夹新建登录/注销页面组件
LoginComponent.razor
完整代码
@inject AuthenticationStateProvider AuthenticationStateProvider
@page "/Login"
@using System.Security.Claims<button @onclick="Login">Log in</button><p>@Msg</p><AuthorizeView><Authorized>你好, @context.User.Identity?.Name<br /><br /><br /><h5>以下是用户的声明</h5><br />@foreach (var claim in context.User.Claims){<p>@claim.Type: @claim.Value</p>} </Authorized> </AuthorizeView><p>以下是基于角色或基于策略的授权,未登录不显示 </p><AuthorizeView Roles="Admin, Superuser"><p>只有管理员或超级用户才能看到.</p>
</AuthorizeView>@code
{[Inject]private AuthenticatedUser? authenticatedUser { get; set; }/// <summary>/// 级联参数获取身份验证状态数据/// </summary>[CascadingParameter]private Task<AuthenticationState>? authenticationStateTask { get; set; }private string? Msg { get; set; }private ClaimsPrincipal? User { get; set; }public async Task Login(){var authenticationState = await ((ExternalAuthStateProvider)AuthenticationStateProvider).LogInAsync();User = authenticationState?.User;if (User != null){if (User.Identity != null && User.Identity.IsAuthenticated){Msg += "已登录." + Environment.NewLine;}}}
}
LogoutComponent.razor
完整代码
@inject AuthenticationStateProvider AuthenticationStateProvider
@page "/Logout"<button @onclick="Logout">Log out</button>@code
{public async Task Logout(){await ((ExternalAuthStateProvider)AuthenticationStateProvider).Logout();}
}
NavMenu.razor 加入菜单
<div class="nav-item px-3"><NavLink class="nav-link" href="Login"><span class="oi oi-plus" aria-hidden="true"></span> Login</NavLink></div><div class="nav-item px-3"><NavLink class="nav-link" href="Logout"><span class="oi oi-plus" aria-hidden="true"></span> Logout</NavLink></div>
Form1.cs 修改首页
var blazor = new BlazorWebView(){Dock = DockStyle.Fill,HostPage = "wwwroot/index.html",Services = Startup.Services!,StartPath = "/Login"};blazor.RootComponents.Add<Main>("#app");Controls.Add(blazor);
Startup.cs 注册服务
完整代码
using BlazorOIDC.WinForms.Data;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Hosting;namespace BlazorOIDC.WinForms;
public static class Startup
{public static IServiceProvider? Services { get; private set; }public static void Init(){var host = Host.CreateDefaultBuilder().ConfigureServices(WireupServices).Build();Services = host.Services;}private static void WireupServices(IServiceCollection services){services.AddWindowsFormsBlazorWebView();services.AddSingleton<WeatherForecastService>();services.AddAuthorizationCore();services.TryAddScoped<AuthenticationStateProvider, ExternalAuthStateProvider>();services.AddSingleton<AuthenticatedUser>();#if DEBUGservices.AddBlazorWebViewDeveloperTools();
#endif}
}