(一個真正的之後端形式來集成認證中心的方案)
javascript
首先特別感謝張善友老師提供技術指導,源於上週我發了一篇文章css
《[Mvp.Blazor] 集成Ids4,實現統一受權認證》,html
我原本是想經過像vue框架那樣,經過引oidc-client.js的方式,來實現Ids4的集成問題,我當時覺得已經很好的,後來看了張隊發的文章之後,發現好像我寫的那種方式並不優雅。前端
因此我又從新改了一次,(可是代碼保留了,新建了對應的分支),以適應在Blazor服務端集成ids4的完美體驗,若是你是wasm的項目,也不須要引用,張隊已經寫好了組件,你們看看引用下便可:vue
https://github.com/BlazorHub/AntDesignTemplatejava
那今天我就快速的給你們說一下,如何在Blazor服務端來設計和集成認證中心,固然裏邊會涉及一些基礎知識點,我就不展開了,因此你本身須要先掌握如下知識儲備:git
Ids4配置受權碼模式客戶端github
Razor page的On{handler}{Async}()語法web
HttpContext.User基本使用typescript
在上一篇文章中,咱們主要是經過oidc-client.js的形式進行ids4的鏈接的。
可是咱們的項目畢竟是服務端,Blazor服務端使用ids4,感受和MVC仍是有些類似的,都是基於Cookie的oidc認證模式。
認證中心配置下客戶
你能夠看到,基本就是和MVC配置是同樣的,不只認證中心的客戶端配置很像,就連項目中,認證服務的註冊的方式也是幾乎同樣:
引用nuget包
Microsoft.AspNetCore.Authentication.OpenIdConnect
startup中,註冊認證服務
// 第一步:配置認證方案 services.AddAuthentication(options => { options.DefaultScheme = "Cookies"; options.DefaultChallengeScheme = "oidc"; }) .AddCookie("Cookies") .AddOpenIdConnect("oidc", options => { options.Authority = "https://ids.neters.club/"; options.ClientId = "blazorserver"; // 75 seconds options.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 login OnAccessDenied = context => { context.HandleResponse(); context.Response.Redirect("/"); return Task.CompletedTask; } }; });
相應的註釋,我簡單的寫了寫,固然文章的開篇我也說了,這一塊屬於ids4的基礎部分,之前的文章和視頻說了不少了,之後我就不打算講解了。
重點是要配置那幾個Scope做用域,而後能夠看到有ids4的受權頁面,固然,這個頁面也能夠屏蔽掉不顯示。
註冊好了服務,那確定是要開啓中間件了:
開啓中間件
app.UseAuthentication();
這裏咱們使用到了Razor的Page功能,添加登陸和登出功能,具體的使用方法能夠在微軟官網查看,相應的代碼很簡單:
登陸、登出
// 這裏用到了緩存來管理咱們的用戶登陸信息,下文會講到 // 第二部分: 配置razor page,定義登陸,登出等邏輯 public class _HostAuthModel : PageModel { public readonly AuthStateCache Cache;
public _HostAuthModel(AuthStateCache cache) { Cache = cache; }
// 每次刷新頁面異步加載 public async Task<IActionResult> OnGet() { System.Diagnostics.Debug.WriteLine($"\n_Host OnGet IsAuth? {User.Identity.IsAuthenticated}");
// 判斷Httpcontext是否登陸狀態 if (User.Identity.IsAuthenticated) { var sid = User.Claims .Where(c => c.Type.Equals("sid")) .Select(c => c.Value) .FirstOrDefault();
System.Diagnostics.Debug.WriteLine($"sid: {sid}");
// 若是緩存中不存在 if (sid != null && !Cache.HasSubjectId(sid)) { var authResult = await HttpContext.AuthenticateAsync("oidc"); DateTimeOffset expiration = authResult.Properties.ExpiresUtc.Value; string accessToken = await HttpContext.GetTokenAsync("access_token"); string refreshToken = await HttpContext.GetTokenAsync("refresh_token"); Cache.Add(sid, expiration, accessToken, refreshToken); } }
return Page(); }
// 登陸 public IActionResult OnGetLogin() { System.Diagnostics.Debug.WriteLine("\n_Host OnGetLogin"); var authProps = new AuthenticationProperties { IsPersistent = true, // 設置token的過時時間,至關於前端的localstorage ExpiresUtc = DateTimeOffset.UtcNow.AddHours(1), RedirectUri = Url.Content("~/") };
// 認證中心登陸 return Challenge(authProps, "oidc"); }
// 登出 public async Task OnGetLogout() { System.Diagnostics.Debug.WriteLine("\n_Host OnGetLogout"); var authProps = new AuthenticationProperties { RedirectUri = Url.Content("~/") }; await HttpContext.SignOutAsync("Cookies"); await HttpContext.SignOutAsync("oidc", authProps); } }
代碼中,我已經增長了相應的註釋信息,你應該能看的明白。
只不過具體的寫法有些小夥伴可能沒用過RazorPage,這裏簡單的說一下:
由於咱們的Index頁面沒有綁定任何數據,因此這裏基本上只繼承了PageModel,OnGet方法是個約定,查看mvc的源碼你會發現它會獲取On{handler}{Async}()。好比OnGet,它會在Get Index的時候被執行,咱們能夠經過這個約定進行數據綁定,這裏知道下在Razor Page下HttpMethod也是一個handler,因此Razor Page的處理方式是經過handler進行的。
爲了實現這個效果,咱們還須要配置主頁面_Host.cshtml的路由:
"/{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頁面加權
只須要在須要的頁面內增長特性便可:
@attribute [Authorize]
展現用戶狀態
剛剛上邊咱們已經配置好了用戶登陸和登出接口,也對頁面進行了加權,用來引導用戶去認證中心登陸,或者單點登陸,拉取用戶信息,那如何展現呢?
很簡單,在主頁面_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 config
protected 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>();
這一塊和以前的邏輯是同樣的,經過HttpClient來實現對第三方資源服務器的api訪問,那確定須要獲取token,這個就從上邊的cache中獲取:
public async Task<string> GetAccessToken() { // 注意這獲取聲明數據有問題,參考個人代碼。獲取當前用戶的sid惟一標誌 var sid = _accessor.HttpContext.User.Claims .Where(c => c.Type.Equals("sid")) .Select(c => c.Value) .FirstOrDefault();
// 正常,則返回結果 if (sid != null && _cache.HasSubjectId(sid)) { return _cache.Get(sid).AccessToken; }
// 不然,跳轉登陸頁,去認證中心拉取 _navigationManager.NavigateTo("/Login", true);
return await Task.FromResult(string.Empty); }
到了這裏,咱們的Blazor.Server服務端集成Ids4已經完成了,是否是徹底沒用到任何的js,來查看下效果吧:
能夠看到完成了這樣的流程:
首頁不須要權限;
博客操做頁須要登陸,併成功跳轉認證中心;
登陸後,成功回調到首頁,並獲取用戶信息;
實現單點登陸;
編輯的時候,test用戶返回Forbidden,代表已經登陸,並實現了權限控制;
好啦,本身動手試試吧。
參考文章:
一、https://mcguirev10.com/2019/12/15/blazor-authentication-with-openid-connect.html
二、https://github.com/BlazorHub/AntDesignTemplate
本文分享自微信公衆號 - dotNET跨平臺(opendotnet)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。