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.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 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功能,添加登录和登出功能,具体的使用方法可以在微软官网查看,相应的代码很简单:

登录、登出

 // 这里用到了缓存来管理我们的用户登录信息,下文会讲到// 第二部分: 配置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的过期时间,相当于前端的localstorageExpiresUtc = 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的路由:

@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页面加权

只需要在需要的页面内增加特性即可:

@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 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中获取:

 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,表明已经登录,并实现了权限控制;

好啦,自己动手试试吧。

参考文章:

1、https://mcguirev10.com/2019/12/15/blazor-authentication-with-openid-connect.html

2、https://github.com/BlazorHub/AntDesignTemplate

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/308906.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

作为一个有理想的程序员,必读的书都有哪些?

很多程序员朋友问我&#xff1a;“哪本最具影响力的书&#xff0c;是每个程序员都应该读的&#xff1f;” 笔者从事软件开发15年&#xff0c;看过的计算机相关的书籍不下百本了&#xff0c;如果非要推荐的话&#xff0c;给大家精选以下10本&#xff0c;希望对大家有所帮助&…

认证授权方案之JwtBearer认证

1.前言回顾&#xff1a;认证方案之初步认识JWT在现代Web应用程序中&#xff0c;即分为前端与后端两大部分。当前前后端的趋势日益剧增&#xff0c;前端设备&#xff08;手机、平板、电脑、及其他设备&#xff09;层出不穷。因此&#xff0c;为了方便满足前端设备与后端进行通讯…

使用过滤器模式,让客户关怀中的代码更加干净整洁

一&#xff1a;实际场景介绍我们在给用户做订单催付通知的时候&#xff0c;会有这样的一种场景&#xff0c;用户在系统后台设置一组可以催付的规则&#xff0c;比如说订单金额大于xx元&#xff0c;非黑名单用户&#xff0c;来自哪个地区&#xff0c;已购买过某个商品&#xff0…

C++实现各种排序以及复杂度,稳定性分析

代码如下: #include<iostream> using namespace std;void Bubble_Sort(int *a, int n) {bool flag;int tmp 0;for (int i n - 1; i > 0; i--){flag false;for (int j 0; j < i; j){if (a[j] > a[j 1]){swap(a[j], a[j 1]);flag true;}}if (!flag) break…

Webapi管理和性能测试工具WebBenchmark

WebBenchmark是一款基于开源通讯组件Beetlex扩展的Webapi管理和性能测试工具&#xff0c;在传统工具中一般管理工具缺乏性能压测能力或有性能压测的缺少管理功能&#xff1b;WebBenchmark的设计目标是就管理和性能测试能力同时具备。接下来介绍一下工具的功能和使用&#xff1a…

Abstract Factory(抽象工厂)--对象创建模式

Abstract Factory &#xff08;抽象工厂&#xff09;–对象创建模式 一、意图 提供一个创建一系列相关或者相互依赖的接口&#xff0c;而无需指定它们具体的类。 二、动机 1.在软件系统中&#xff0c;经常面临着“一系列相互依赖的对象”的创建工 作;同时&#xff0c;由于需求…

Builder(生成器)--对象创建型模式

Builder&#xff08;生成器&#xff09;–对象创建型模式 一、意图 将一个复杂的对象构建与它的表示分离&#xff0c;使得同样的构建过程可以创建不同的表示。 二、动机 1.在软件系统中&#xff0c;有时候面临着“一个复杂对象”的创建工作&#xff0c;其通常由各个部分的子对…

Gartner:缺乏技术人才将影响企业数字化转型

导语大多数公司在数字化转型的阶段对所需的技能方面都处于“盲目”状态。正文随着COVID-19响应加快了数字化转型的速度和规模&#xff0c;缺乏数字化技能可能会危害人才计划不统一的公司。甚至在冠状病毒大流行之前&#xff0c;董事会就将数字/技术中断列为2020年的头等大事&am…

DEBUG org.springframework.web.servlet.DispatcherServlet - Error rendering view [org.thymeleaf.spring

报错信息如下: 报错原因: thymeleaf有一些限制&#xff0c;使用th语言&#xff0c;内容为空就会报错 改成这样解决问题:

Factory Method(工厂方法)--对象创建型模式

Factory Method&#xff08;工厂方法&#xff09;–对象创建型模式 一、意图 定义一个用于创建对象的接口&#xff0c;让子类决定实例化哪一个类。Factory Method使一个类的实例化延迟到其子类。 二、动机 1.在软件系统中&#xff0c;经常面临着创建对象的工作;由于需求的变化…

全内存的redis用习惯了?那能突破内存限制类redis产品ssdb呢?

首先说一下背景&#xff0c;在双十一的时候&#xff0c;我们系统接受X宝的订单推送&#xff0c;原先的实现方式是使用 redis 的 List 作为推送数据的承载&#xff0c;在非大促的场景下&#xff0c;一切运行正常&#xff0c;内存占用大概3-4G&#xff0c;机器是16G内存。由于提前…

Prototype(原型)--对象创建模式

Prototype&#xff08;原型&#xff09;–对象创建模式 一、意图 用原型实例指定创建对象的种类&#xff0c;并且通过拷贝这些原型创建新的对象。 二、动机 1.在软件系统中&#xff0c;经常面临着“某些结构复杂的对象”的创建工作&#xff1b;由于需求的变化&#xff0c;这些…

认证授权方案之授权揭秘 (上篇)

一、前言回顾&#xff1a;认证授权方案之授权初识从上一节中&#xff0c;我们在对授权系统已经有了初步的认识和使用&#xff0c;可以发现&#xff0c;asp.net core为我们提供的授权策略是一个非常强大丰富且灵活的认证授权方案&#xff0c;能够满足大部分的授权场景。在Config…

Singleton(单件)--对象创建模式

Singleton&#xff08;单件&#xff09;–对象创建模式 一、意图 保证一个类仅有一个实例&#xff0c;并提供一个访问它的全局访问点。 二、动机 1.在软件系统中&#xff0c;经常有这样一些特殊的类&#xff0c;必须保证它们在系统中只存在一个实例&#xff0c;才能确保它们的…

龙芯团队 在移值 MIPS64 下的.NET Core 进度速报

写在开始前我们的主要业务基于 dotnet core 2.x 与 3.1 完成&#xff0c;目前 dotnet core 3.1 支持的 CPU 架构列表中还不包含龙芯&#xff0c;且在 gitlab issue 中表示官方当前没有对 MIPS 的支持计划。更具体操作系统与 CPU 架构列表见 [Download .NET Core 3.1](https://d…

Adapter(适配器)--类对象结构型模式

Adapter&#xff08;适配器&#xff09;–类对象结构型模式 一、意图 将一个类的接口转换成客户希望的另外一个接口。Adapter模式使得原本接口不兼容而不能一起工作的那些类可以一起工作。 二、动机 1.在软件系统中&#xff0c;由于应用环境的变化&#xff0c;常常需要将“一…

如何使用ABP框架(2)三层架构与领域驱动设计的对比

本文来自长沙.NET技术社区&#xff0c;原创&#xff1a;邹溪源。全文共有8500字&#xff0c;读完需耗时10分钟。题图来自pixabay简述上一篇简述了ABP框架中的一些基础理论&#xff0c;包括ABP前后端项目的分层结构&#xff0c;以及后端项目中涉及到的知识点&#xff0c;例如DTO…

Bridge(桥接)--对象结构模式

Bridge&#xff08;桥接&#xff09;–对象结构模式 一、意图 将抽象部分与它的实现部分分离&#xff0c;使它们都可以独立的变化。 二、动机 1.由于某些类型的固有的实现逻辑&#xff0c;使得它们具有两个变化的维度&#xff0c;乃至多个纬度的变化。 2.如何应对这种“多维度…

Composite(组合)--对象结构型模式

Composite&#xff08;组合&#xff09;–对象结构型模式 一、意图 将对象组合成树形结构以表示“部分-整体”的层次结构。Composite使得用户对单个对象和组合对象的使用具有一致性。 二、动机 1.软件在某些情况下&#xff0c;客户代码过多的依赖于对象容器复杂的内部实现结构…