Blazor.Server以正确的方式集成Ids4
(一個(gè)真正的以后端形式來集成認(rèn)證中心的方案)
?
本文導(dǎo)讀
首先特別感謝張善友老師提供技術(shù)指導(dǎo),源于上周我發(fā)了一篇文章
《[Mvp.Blazor] 集成Ids4,實(shí)現(xiàn)統(tǒng)一授權(quán)認(rèn)證》,
我本來是想通過像vue框架那樣,通過引oidc-client.js的方式,來實(shí)現(xiàn)Ids4的集成問題,我當(dāng)時(shí)以為已經(jīng)很好的,后來看了張隊(duì)發(fā)的文章以后,發(fā)現(xiàn)好像我寫的那種方式并不優(yōu)雅。
所以我又重新改了一次,(但是代碼保留了,新建了對應(yīng)的分支),以適應(yīng)在Blazor服務(wù)端集成ids4的完美體驗(yàn),如果你是wasm的項(xiàng)目,也不需要引用,張隊(duì)已經(jīng)寫好了組件,大家看看引用下即可:
https://github.com/BlazorHub/AntDesignTemplate
那今天我就快速的給大家說一下,如何在Blazor服務(wù)端來設(shè)計(jì)和集成認(rèn)證中心,當(dāng)然里邊會涉及一些基礎(chǔ)知識點(diǎn),我就不展開了,所以你自己需要先掌握以下知識儲備:
Ids4配置授權(quán)碼模式客戶端
Razor page的On{handler}{Async}()語法
HttpContext.User基本使用
第一部分:配置認(rèn)證方案
在上一篇文章中,我們主要是通過oidc-client.js的形式進(jìn)行ids4的連接的。
但是我們的項(xiàng)目畢竟是服務(wù)端,Blazor服務(wù)端使用ids4,感覺和MVC還是有些相似的,都是基于Cookie的oidc認(rèn)證模式。
認(rèn)證中心配置下客戶
你可以看到,基本就是和MVC配置是一樣的,不僅認(rèn)證中心的客戶端配置很像,就連項(xiàng)目中,認(rèn)證服務(wù)的注冊的方式也是幾乎一樣:
引用nuget包
Microsoft.AspNetCore.Authentication.OpenIdConnectstartup中,注冊認(rèn)證服務(wù)
// 第一步:配置認(rèn)證方案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的時(shí)候,配置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;}};});相應(yīng)的注釋,我簡單的寫了寫,當(dāng)然文章的開篇我也說了,這一塊屬于ids4的基礎(chǔ)部分,以前的文章和視頻說了很多了,以后我就不打算講解了。
重點(diǎn)是要配置那幾個(gè)Scope作用域,然后可以看到有ids4的授權(quán)頁面,當(dāng)然,這個(gè)頁面也可以屏蔽掉不顯示。
注冊好了服務(wù),那肯定是要開啟中間件了:
開啟中間件
app.UseAuthentication();第二部分:登錄、登出的頁面設(shè)計(jì)
這里我們使用到了Razor的Page功能,添加登錄和登出功能,具體的使用方法可以在微軟官網(wǎng)查看,相應(yīng)的代碼很簡單:
登錄、登出
代碼中,我已經(jīng)增加了相應(yīng)的注釋信息,你應(yīng)該能看的明白。
只不過具體的寫法有些小伙伴可能沒用過RazorPage,這里簡單的說一下:
因?yàn)槲覀兊腎ndex頁面沒有綁定任何數(shù)據(jù),所以這里基本上只繼承了PageModel,OnGet方法是個(gè)約定,查看mvc的源碼你會發(fā)現(xiàn)它會獲取On{handler}{Async}()。比如OnGet,它會在Get Index的時(shí)候被執(zhí)行,我們可以通過這個(gè)約定進(jìn)行數(shù)據(jù)綁定,這里知道下在Razor Page下HttpMethod也是一個(gè)handler,所以Razor Page的處理方式是通過handler進(jìn)行的。
為了實(shí)現(xiàn)這個(gè)效果,我們還需要配置主頁面_Host.cshtml的路由:
@page "/{handler?}"你可能會好奇,那既然要使用到認(rèn)證中心了,為啥還需要登錄登出呢,其實(shí)客戶端都是需要的,不信你用mvc項(xiàng)目,也需要配置的。
權(quán)限組件
Blazor自帶了相應(yīng)的授權(quán)組件,可以很好的幫助我們來實(shí)現(xiàn)對權(quán)限的控制,只需要在App.razor中:
@inject NavigationManager NavManager<Router AppAssembly="@typeof(Program).Assembly"><Found Context="routeData"><AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)"><NotAuthorized>@{// 使用權(quán)限組件,如果當(dāng)然組件配置Authorize,并且用戶未登錄,則跳轉(zhuǎn)登錄頁(這里是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頁面是否需要加權(quán),如果不配置,那就是很正常的瀏覽,比如我們的博客index首頁,肯定不能加權(quán),除非是后臺管理系統(tǒng),那就需要每個(gè)頁面都加權(quán)了,配置好后,如果用戶未登錄,那就會立刻跳轉(zhuǎn)到上邊我們配置的登錄地址,跳轉(zhuǎn)到認(rèn)證中心。
那如何對特定頁面加權(quán)呢,很簡單。
razor頁面加權(quán)
只需要在需要的頁面內(nèi)增加特性即可:
展示用戶狀態(tài)
剛剛上邊我們已經(jīng)配置好了用戶登錄和登出接口,也對頁面進(jìn)行了加權(quán),用來引導(dǎo)用戶去認(rèn)證中心登錄,或者單點(diǎn)登錄,拉取用戶信息,那如何展示呢?
很簡單,在主頁面_Host.cshtml中,使用User屬性來實(shí)現(xiàn):
@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">設(shè)置 - <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>}具體的代碼看我的項(xiàng)目即可。
那到了這里,我們已經(jīng)完成了Blazor服務(wù)端如何集成ids4的代碼,不過這樣還是有些問題的,比如:
如果獲取access_token來訪問第三方的資源服務(wù)器api呢?
第三部分:管理用戶授權(quán)狀態(tài)
之前我們用js方法的時(shí)候,還記得嗎,我們使用的是localstorage的形式,存在了客戶端,包括用戶信息,令牌,過期時(shí)間等等,然后通過JSRuntime來實(shí)現(xiàn)對js的控制和使用,那今天我們不用js了,如何來管控呢,我這里用的是內(nèi)存緩存的形式,當(dāng)然你可以使用Redis來實(shí)現(xiàn)分布式,思路都一樣。
用戶數(shù)據(jù)存儲cache
在上邊的登錄的時(shí)候,我們看到了,每次登錄成功回調(diào)的時(shí)候,都會刷新頁面,也當(dāng)然會執(zhí)行OnGet()方法,這樣,就會把當(dāng)然用戶的信息,通過特定的sid作為緩存key的形式來保存到內(nèi)存里,這個(gè)sid就像是session一樣,每次登錄成功回調(diào)后,都會有一個(gè)唯一的字符串,作為標(biāo)識,開發(fā)過微信的應(yīng)該都知道。
那就定義一個(gè)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 _);}}這個(gè)很簡單,就不多說了,就是對用戶數(shù)據(jù)的增刪改查,標(biāo)識就是sid。那現(xiàn)在就有了一個(gè)問題,我們知道,登錄的時(shí)候是存到cache里的,那什么時(shí)候刪除呢?
請往下看。
AuthenticationStateProvider 服務(wù)
這個(gè)服務(wù)是今天的重頭戲,你需要好好的了解一下它的作用:
內(nèi)置的 AuthenticationStateProvider 服務(wù)可從 ASP.NET Core 的 HttpContext.User 獲取身份驗(yàn)證狀態(tài)數(shù)據(jù)。 身份驗(yàn)證狀態(tài)就是這樣與現(xiàn)有 ASP.NET Core 身份驗(yàn)證機(jī)制集成。
AuthenticationStateProvider 服務(wù)可以提供當(dāng)前用戶的 ClaimsPrincipal 數(shù)據(jù)。
簡單的概況呢,就是開啟這個(gè)服務(wù),我們可以獲取當(dāng)前用戶的claim聲明,并且定期的做一個(gè)篩查,就像是一個(gè)定時(shí)器,每十秒執(zhí)行一次,判斷當(dāng)前用戶是否過期,如果正好過期了,就把這個(gè)cache記錄給刪掉。
/// <summary>/// 配置狀態(tài)服務(wù)處理器,定時(shí)校驗(yàn)授權(quán)狀態(tài)/// RevalidationInterval為刷新時(shí)間,類似于滑動時(shí)間/// </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);}}思路就是這樣,自己應(yīng)該能看明白,就是定時(shí)做了一個(gè)判斷,然后刪除cache。
服務(wù)注冊容器
把上邊的兩個(gè)服務(wù)注冊下:
?//?第三部分:授權(quán)狀態(tài)的保護(hù)與管理services.AddSingleton<AuthStateCache>();// 開啟AuthenticationStateProvider 服務(wù)services.AddScoped<AuthenticationStateProvider,?AuthStateHandler>();第四部分:獲取token,訪問api
這一塊和之前的邏輯是一樣的,通過HttpClient來實(shí)現(xiàn)對第三方資源服務(wù)器的api訪問,那肯定需要獲取token,這個(gè)就從上邊的cache中獲取:
到了這里,我們的Blazor.Server服務(wù)端集成Ids4已經(jīng)完成了,是不是完全沒用到任何的js,來查看下效果吧:
可以看到完成了這樣的流程:
首頁不需要權(quán)限;
博客操作頁需要登錄,并成功跳轉(zhuǎn)認(rèn)證中心;
登錄后,成功回調(diào)到首頁,并獲取用戶信息;
實(shí)現(xiàn)單點(diǎn)登錄;
編輯的時(shí)候,test用戶返回Forbidden,表明已經(jīng)登錄,并實(shí)現(xiàn)了權(quán)限控制;
好啦,自己動手試試吧。
參考文章:
1、https://mcguirev10.com/2019/12/15/blazor-authentication-with-openid-connect.html
2、https://github.com/BlazorHub/AntDesignTemplate
總結(jié)
以上是生活随笔為你收集整理的Blazor.Server以正确的方式集成Ids4的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 认证授权方案之授权初识
- 下一篇: 作为一个有理想的程序员,必读的书都有哪些