目录:
- 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 工程
自行安装 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("nnError:n{0}", result.Error); return; } Console.WriteLine("nnClaims:"); 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 disable namespace 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/10361 if (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 DEBUG services.AddBlazorWebViewDeveloperTools(); #endif } }
运行
