Blazor.Server以正确的方式集成Ids4
(一個真正的以后端形式來集成認證中心的方案)
?
本文導讀
首先特別感謝張善友老師提供技術指導,源于上周我發了一篇文章
《[Mvp.Blazor] 集成Ids4,實現統一授權認證》,
我本來是想通過像vue框架那樣,通過引oidc-client.js的方式,來實現Ids4的集成問題,我當時以為已經很好的,后來看了張隊發的文章以后,發現好像我寫的那種方式并不優雅。
所以我又重新改了一次,(但是代碼保留了,新建了對應的分支),以適應在Blazor服務端集成ids4的完美體驗,如果你是wasm的項目,也不需要引用,張隊已經寫好了組件,大家看看引用下即可:
https://github.com/BlazorHub/AntDesignTemplate
那今天我就快速的給大家說一下,如何在Blazor服務端來設計和集成認證中心,當然里邊會涉及一些基礎知識點,我就不展開了,所以你自己需要先掌握以下知識儲備:
Ids4配置授權碼模式客戶端
Razor page的On{handler}{Async}()語法
HttpContext.User基本使用
第一部分:配置認證方案
在上一篇文章中,我們主要是通過oidc-client.js的形式進行ids4的連接的。
但是我們的項目畢竟是服務端,Blazor服務端使用ids4,感覺和MVC還是有些相似的,都是基于Cookie的oidc認證模式。
認證中心配置下客戶
你可以看到,基本就是和MVC配置是一樣的,不僅認證中心的客戶端配置很像,就連項目中,認證服務的注冊的方式也是幾乎一樣:
引用nuget包
Microsoft.AspNetCore.Authentication.OpenIdConnectstartup中,注冊認證服務
// 第一步:配置認證方案services.AddAuthentication(options =>{options.DefaultScheme = "Cookies";options.DefaultChallengeScheme = "oidc";}).AddCookie("Cookies").AddOpenIdConnect("oidc", options =>{options.Authority = "https://ids.neters.club/";options.ClientId = "blazorserver"; // 75 secondsoptions.ClientSecret = "secret";options.ResponseType = "code";options.SaveTokens = true;// 為api在使用refresh_token的時候,配置offline_access作用域options.GetClaimsFromUserInfoEndpoint = true;// 作用域獲取options.Scope.Clear();options.Scope.Add("roles");//"roles"options.Scope.Add("rolename");//"rolename"options.Scope.Add("blog.core.api");options.Scope.Add("profile");options.Scope.Add("openid");options.Events = new OpenIdConnectEvents{// called if user clicks Cancel during loginOnAccessDenied = context =>{context.HandleResponse();context.Response.Redirect("/");return Task.CompletedTask;}};});相應的注釋,我簡單的寫了寫,當然文章的開篇我也說了,這一塊屬于ids4的基礎部分,以前的文章和視頻說了很多了,以后我就不打算講解了。
重點是要配置那幾個Scope作用域,然后可以看到有ids4的授權頁面,當然,這個頁面也可以屏蔽掉不顯示。
注冊好了服務,那肯定是要開啟中間件了:
開啟中間件
app.UseAuthentication();第二部分:登錄、登出的頁面設計
這里我們使用到了Razor的Page功能,添加登錄和登出功能,具體的使用方法可以在微軟官網查看,相應的代碼很簡單:
登錄、登出
代碼中,我已經增加了相應的注釋信息,你應該能看的明白。
只不過具體的寫法有些小伙伴可能沒用過RazorPage,這里簡單的說一下:
因為我們的Index頁面沒有綁定任何數據,所以這里基本上只繼承了PageModel,OnGet方法是個約定,查看mvc的源碼你會發現它會獲取On{handler}{Async}()。比如OnGet,它會在Get Index的時候被執行,我們可以通過這個約定進行數據綁定,這里知道下在Razor Page下HttpMethod也是一個handler,所以Razor Page的處理方式是通過handler進行的。
為了實現這個效果,我們還需要配置主頁面_Host.cshtml的路由:
@page "/{handler?}"你可能會好奇,那既然要使用到認證中心了,為啥還需要登錄登出呢,其實客戶端都是需要的,不信你用mvc項目,也需要配置的。
權限組件
Blazor自帶了相應的授權組件,可以很好的幫助我們來實現對權限的控制,只需要在App.razor中:
@inject NavigationManager NavManager<Router AppAssembly="@typeof(Program).Assembly"><Found Context="routeData"><AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)"><NotAuthorized>@{// 使用權限組件,如果當然組件配置Authorize,并且用戶未登錄,則跳轉登錄頁(這里是ids4)NavManager.NavigateTo("/Login", true);}</NotAuthorized><Authorizing><h1>Authentication in progress</h1><p>Only visible while authentication is in progress.</p></Authorizing></AuthorizeRouteView></Found><NotFound><CascadingAuthenticationState><LayoutView Layout="@typeof(MainLayout)"><h1>Sorry</h1><p>Sorry, there's nothing at this address.</p></LayoutView></CascadingAuthenticationState></NotFound> </Router>大概意思就是,我們可以指定我們的razor頁面是否需要加權,如果不配置,那就是很正常的瀏覽,比如我們的博客index首頁,肯定不能加權,除非是后臺管理系統,那就需要每個頁面都加權了,配置好后,如果用戶未登錄,那就會立刻跳轉到上邊我們配置的登錄地址,跳轉到認證中心。
那如何對特定頁面加權呢,很簡單。
razor頁面加權
只需要在需要的頁面內增加特性即可:
展示用戶狀態
剛剛上邊我們已經配置好了用戶登錄和登出接口,也對頁面進行了加權,用來引導用戶去認證中心登錄,或者單點登錄,拉取用戶信息,那如何展示呢?
很簡單,在主頁面_Host.cshtml中,使用User屬性來實現:
@model _HostAuthModel@if (User.Identity.IsAuthenticated){<div id="logined" style="display: contents;"><div class="menu-item my-2 my-md-0 mr-md-3 dropdown"><button type="button" class="btn btn-primary dropdown-toggle" data-toggle="dropdown">設置 - <span id="username">@(userName) </span></button><div class="dropdown-menu"></div></div><a class="menu-item my-2 btn btn-outline-primary" href="/logout">注銷</a></div>}else{<div id="accessed"><a class="menu-item my-2 btn btn-outline-primary" href="/login">登入</a></div>}具體的代碼看我的項目即可。
那到了這里,我們已經完成了Blazor服務端如何集成ids4的代碼,不過這樣還是有些問題的,比如:
如果獲取access_token來訪問第三方的資源服務器api呢?
第三部分:管理用戶授權狀態
之前我們用js方法的時候,還記得嗎,我們使用的是localstorage的形式,存在了客戶端,包括用戶信息,令牌,過期時間等等,然后通過JSRuntime來實現對js的控制和使用,那今天我們不用js了,如何來管控呢,我這里用的是內存緩存的形式,當然你可以使用Redis來實現分布式,思路都一樣。
用戶數據存儲cache
在上邊的登錄的時候,我們看到了,每次登錄成功回調的時候,都會刷新頁面,也當然會執行OnGet()方法,這樣,就會把當然用戶的信息,通過特定的sid作為緩存key的形式來保存到內存里,這個sid就像是session一樣,每次登錄成功回調后,都會有一個唯一的字符串,作為標識,開發過微信的應該都知道。
那就定義一個cache管理類:
public class AuthStateCache{private ConcurrentDictionary<string, ServerAuthModel> Cache= new ConcurrentDictionary<string, ServerAuthModel>();public bool HasSubjectId(string subjectId)=> Cache.ContainsKey(subjectId);public void Add(string subjectId, DateTimeOffset expiration, string accessToken, string refreshToken){System.Diagnostics.Debug.WriteLine($"Caching sid: {subjectId}");var data = new ServerAuthModel{SubjectId = subjectId,Expiration = expiration,AccessToken = accessToken,RefreshToken = refreshToken};Cache.AddOrUpdate(subjectId, data, (k, v) => data);}public ServerAuthModel Get(string subjectId){Cache.TryGetValue(subjectId, out var data);return data;}public void Remove(string subjectId){System.Diagnostics.Debug.WriteLine($"Removing sid: {subjectId}");Cache.TryRemove(subjectId, out _);}}這個很簡單,就不多說了,就是對用戶數據的增刪改查,標識就是sid。那現在就有了一個問題,我們知道,登錄的時候是存到cache里的,那什么時候刪除呢?
請往下看。
AuthenticationStateProvider 服務
這個服務是今天的重頭戲,你需要好好的了解一下它的作用:
內置的 AuthenticationStateProvider 服務可從 ASP.NET Core 的 HttpContext.User 獲取身份驗證狀態數據。 身份驗證狀態就是這樣與現有 ASP.NET Core 身份驗證機制集成。
AuthenticationStateProvider 服務可以提供當前用戶的 ClaimsPrincipal 數據。
簡單的概況呢,就是開啟這個服務,我們可以獲取當前用戶的claim聲明,并且定期的做一個篩查,就像是一個定時器,每十秒執行一次,判斷當前用戶是否過期,如果正好過期了,就把這個cache記錄給刪掉。
/// <summary>/// 配置狀態服務處理器,定時校驗授權狀態/// RevalidationInterval為刷新時間,類似于滑動時間/// </summary>public class AuthStateHandler : RevalidatingServerAuthenticationStateProvider{private readonly AuthStateCache Cache;public AuthStateHandler(ILoggerFactory loggerFactory,AuthStateCache cache): base(loggerFactory){Cache = cache;}protected override TimeSpan RevalidationInterval=> TimeSpan.FromSeconds(10); // TODO read from configprotected override Task<bool> ValidateAuthenticationStateAsync(AuthenticationState authenticationState, CancellationToken cancellationToken){var sid =authenticationState.User.Claims.Where(c => c.Type.Equals("sid")).Select(c => c.Value).FirstOrDefault();if (sid != null && Cache.HasSubjectId(sid)){var data = Cache.Get(sid);System.Diagnostics.Debug.WriteLine($"NowUtc: {DateTimeOffset.UtcNow.ToString("o")}");System.Diagnostics.Debug.WriteLine($"ExpUtc: {data.Expiration.ToString("o")}");if(DateTimeOffset.UtcNow >= data.Expiration){System.Diagnostics.Debug.WriteLine($"*** EXPIRED ***");Cache.Remove(sid);return Task.FromResult(false);}}else{System.Diagnostics.Debug.WriteLine($"(not in cache)");}return Task.FromResult(true);}}思路就是這樣,自己應該能看明白,就是定時做了一個判斷,然后刪除cache。
服務注冊容器
把上邊的兩個服務注冊下:
?//?第三部分:授權狀態的保護與管理services.AddSingleton<AuthStateCache>();// 開啟AuthenticationStateProvider 服務services.AddScoped<AuthenticationStateProvider,?AuthStateHandler>();第四部分:獲取token,訪問api
這一塊和之前的邏輯是一樣的,通過HttpClient來實現對第三方資源服務器的api訪問,那肯定需要獲取token,這個就從上邊的cache中獲取:
到了這里,我們的Blazor.Server服務端集成Ids4已經完成了,是不是完全沒用到任何的js,來查看下效果吧:
可以看到完成了這樣的流程:
首頁不需要權限;
博客操作頁需要登錄,并成功跳轉認證中心;
登錄后,成功回調到首頁,并獲取用戶信息;
實現單點登錄;
編輯的時候,test用戶返回Forbidden,表明已經登錄,并實現了權限控制;
好啦,自己動手試試吧。
參考文章:
1、https://mcguirev10.com/2019/12/15/blazor-authentication-with-openid-connect.html
2、https://github.com/BlazorHub/AntDesignTemplate
總結
以上是生活随笔為你收集整理的Blazor.Server以正确的方式集成Ids4的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 认证授权方案之授权初识
- 下一篇: 作为一个有理想的程序员,必读的书都有哪些