目录:
- OpenID 与 OAuth2 基础知识
- Blazor wasm Google 登录
- Blazor wasm Gitee 码云登录
- Blazor SSR/WASM IDS/OIDC 单点登录授权实例1-建立和配置IDS身份验证服务
- Blazor SSR/WASM IDS/OIDC 单点登录授权实例2-登录信息组件wasm
- Blazor SSR/WASM IDS/OIDC 单点登录授权实例3-服务端管理组件
- Blazor SSR/WASM IDS/OIDC 单点登录授权实例4 - 部署服务端/独立WASM端授权
- Blazor SSR/WASM IDS/OIDC 单点登录授权实例5 - Blazor hybird app 端授权
- Blazor SSR/WASM IDS/OIDC 单点登录授权实例5 - Winform 端授权
源码
BlazorWasmOIDC
ConsoleOIDC
BlazorOIDC.Server 项目
部署
部署步骤跟平常blazor一样, 这里就不复述了, demo 部署后的域名是 ids2.app.es
新建独立WASM工程
改为以前文章配置的测试点 "applicationUrl": "https://localhost:5002;http://localhost:5003",
Program.cs 文件
using BlazorWasmOIDC;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.AspNetCore.Components.WebAssembly.Hosting;var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app");
builder.RootComponents.Add<HeadOutlet>("head::after");builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });var authority = "https://localhost:5001/";
var clientId = "Blazor5002";
var url2 = builder.HostEnvironment.BaseAddress;//完整的配置
builder.Services.AddOidcAuthentication(options =>
{options.ProviderOptions.Authority = authority;options.ProviderOptions.ClientId = clientId;options.ProviderOptions.ResponseType = "code";options.ProviderOptions.RedirectUri = $"{url2}authentication/login-callback";options.ProviderOptions.PostLogoutRedirectUri = $"{url2}authentication/logout-callback";options.ProviderOptions.DefaultScopes.Add("BlazorWasmIdentity.ServerAPI");options.UserOptions.RoleClaim = "role";
});await builder.Build().RunAsync();
Index.razor 文件
@page "/"
@using System.Diagnostics.CodeAnalysis
@using Microsoft.AspNetCore.Components
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication<PageTitle>Index</PageTitle><AuthorizeView><Authorized>你好, @context.User.Identity?.Name</Authorized><NotAuthorized><span>看起来你还没登录</span></NotAuthorized></AuthorizeView>
测试
-
先独立启动ssr工程 Blazor SSR/WASM IDS/OIDC 单点登录授权实例1-建立和配置IDS身份验证服务, 监听地址为 https://localhost:5001/
-
独立启动 BlazorWasmOIDC 工程, 监听地址为 https://localhost:5002/
-
登录测试
[附带]控制台授权测试
添加工程 ConsoleOIDC
<Project Sdk="Microsoft.NET.Sdk"><PropertyGroup><OutputType>Exe</OutputType><TargetFramework>net8.0</TargetFramework><ImplicitUsings>enable</ImplicitUsings><Nullable>enable</Nullable></PropertyGroup> <ItemGroup><FrameworkReference Include="Microsoft.AspNetCore.App"></FrameworkReference><PackageReference Include="Newtonsoft.Json" Version="13.0.3" /><PackageReference Include="Serilog.Extensions.Logging" Version="8.0.0" /><PackageReference Include="Serilog.Sinks.Console" Version="5.0.1" /><PackageReference Include="IdentityModel.OidcClient" Version="5.2.1" /></ItemGroup></Project>
代码
using IdentityModel.Client;
using IdentityModel.OidcClient;
using Newtonsoft.Json.Linq;
using Serilog;namespace ConsoleOIDC;public class Program
{static string authority = "https://localhost:5001/";//static string authority = "https://ids2.app1.es/"; //真实环境static string api = $"{authority}WeatherForecast";static string clientId = "Blazor5002";static OidcClient? _oidcClient;static HttpClient _apiClient = new HttpClient { BaseAddress = new Uri(api) };public static void Main(string[] args) => MainAsync().GetAwaiter().GetResult();public static async Task MainAsync(){Console.WriteLine("+-----------------------+");Console.WriteLine("| Sign in with OIDC |");Console.WriteLine("+-----------------------+");Console.WriteLine("");await Login();}private static async Task Login(){// 使用环回地址上的可用端口创建重定向 URI。// 要求 OP 允许 127.0.0.1 上的随机端口 - 否则设置静态端口var browser = new SystemBrowser(5002);var redirectUri = $"http://localhost:{browser.Port}/authentication/login-callback";var redirectLogoutUri = $"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 }};var serilog = new LoggerConfiguration().MinimumLevel.Error().Enrich.FromLogContext().WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level}] {SourceContext}{NewLine}{Message}{NewLine}{Exception}{NewLine}").CreateLogger();options.LoggerFactory.AddSerilog(serilog);_oidcClient = new OidcClient(options);var result = await _oidcClient.LoginAsync(new LoginRequest());ShowResult(result);await CallApi(result.AccessToken);await NextSteps(result);}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"}");}}private static async Task NextSteps(LoginResult result){var currentAccessToken = result.AccessToken;var currentRefreshToken = result.RefreshToken;var menu = " x...exit c...call api ";if (currentRefreshToken != null) menu += "r...refresh token ";while (true){Console.WriteLine("\n\n");Console.Write(menu);var key = Console.ReadKey();if (key.Key == ConsoleKey.X) return;if (key.Key == ConsoleKey.C) await CallApi(currentAccessToken);if (key.Key == ConsoleKey.R){var refreshResult = await _oidcClient.RefreshTokenAsync(currentRefreshToken);if (refreshResult.IsError){Console.WriteLine($"Error: {refreshResult.Error}");}else{currentRefreshToken = refreshResult.RefreshToken;currentAccessToken = refreshResult.AccessToken;Console.WriteLine("\n\n");Console.WriteLine($"access token: {refreshResult.AccessToken}");Console.WriteLine($"refresh token: {refreshResult?.RefreshToken ?? "none"}");}}}}private static async Task CallApi(string currentAccessToken){try{_apiClient.SetBearerToken(currentAccessToken);var response = await _apiClient.GetAsync("");if (response.IsSuccessStatusCode){var str = await response.Content.ReadAsStringAsync();Console.WriteLine($"Response: {str}");var json = JArray.Parse(await response.Content.ReadAsStringAsync());Console.WriteLine("\n\n");Console.WriteLine(json);}else{Console.WriteLine($"Error: {response.ReasonPhrase}");}}catch (Exception e){Console.WriteLine($"Error: {e.Message}");}}
}
添加 SystemBrowser.cs
using IdentityModel.OidcClient.Browser;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using System;
using System.Diagnostics;
using System.IO;
using System.Net;
using System.Net.Sockets;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;
using System.Threading.Tasks;namespace ConsoleOIDC;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;}
}