(又一个客户端集成了IdentityServer4)
还是很开心的,目前已经有六个开源项目都集成到了Ids4认证中心了。
1、Blazor系列文章回顾
书接上文, 关于Blazor学习 呢, 我也发了几篇文章了,我一般写东西都喜欢偏实战,当然也有系列教程的情节,还记得当时在群里,我说简单看看,浅尝辄止吧,没想到 慢慢的发现了解的就越来 越深入 了 , 这里我 我们再来一个 前情回顾 :
《 我的『MVP.Blazor』快速创建与部署 》
在这篇文章中,我们简单的了解了下,什么的Blazor,他能做些什么,以及如何快速的入门和部署,属于一个认知的阶段,熟话说万事开头难,只要有兴趣了,剩下的就是勤为径;
《 最终选型 Blazor.Server:又快又稳! 》
从这篇文章开始,慢慢的开始实战了,因为刚开始选型的是blazor.wasm,后来发现速度上比较慢,特别是刷新上,所以就最终选型了Blazor.Server了,速度当然没得说,和我们平时的ASP.NETCore是一样的,不过很多人说对硬件要求高,我感觉没什么感觉,2C4G的Linux服务器,几千人在线应该没问题。然后就正式开始了设计我的MVP项目;
《 [号外] Blazor wasm 其实也挺快! 》
选型了server版本以后,总感觉wasm版本不可能那么慢,然后就好好的深入研究了下,通过了PWA、GZIP压缩、CDN等技术,基本能保证WASM框架首屏首次刷新在3~5s之内,之后静态加载毫秒级别,动态刷新是2s以内(可以查看我文章,有具体的数据佐证);
《 [Mvp.Blazor] 动态路由与钩子函数 》
之前三篇文章,我们学会了组件通信、数据请求、数据绑定和继承等知识点,那这篇文章我简单的对路由和钩子函数做了说明和讲解,已经算是比较完善的项目了;
《 如何给Blazor.Server加个API鉴权? 》
我经常在群里说的一句话就是:没有日志的项目是没有灵魂的,没有权限的项目是裸奔的。就是这样的,所以我基本任何项目都会有权限,包括我们功能内部的一些小Portal,我都会在重要页面或数据上增加一定的权限。这篇文章我用了很简单,可以说很low的方法,对资源api实现了鉴权,当然,我在文章中也说了,这种方案肯定不靠谱。
所以,在这个端午节三天期间内,趁着没地方去,我又各种的翻看资料,这里说下,国外的资料还是比较丰富的,有条件的话,还是要科学上网更好些。
最终呢,不负众望,实现了将Blazor.Server集成到了Ids4的统一认证平台上,如果你用的是Blazor.wasm,基本差不多,甚至更简单,等你有实战项目了就知道了。
这里先说明一下,因为毕竟是集成Ids4,涉及的知识会比较多,比如如何 使用oidc-client、如何c#调用js事件、如何封装service模块 ,不过 本文就不过多的对这几个知识点讲解原理了,先列出来操作步骤和代码 ,毕竟篇幅有限,之后我会针对我认为比较重要的知识点稍微讲解讲解。
2、Ids4模块配置
如果你之前开发过Ids4呢,接下来已经能看懂,如果完全不会,建议还是先把Ids4学一遍吧,除非就完全copy我的代码,尽管会遇到这样那样的Bug。
涉及到的页面和模块
(蓝色背景的三个文件)
1、先在认证中心配置Client
我们既然要集成认证平台,那肯定要去认证中心,配置一个客户端,因为我们的Blazor是一个前端的框架,所以我们使用implicit简化模式,和Blog.Admin很相似,只不过一个组件安装一个是直接使用js静态文件,原理都一样。
(Blazor客户端的基本配置)
详细应该能看的懂,注意一点就是需要配置
AllowAccessTokensViaBrowser = true
这样才能有资格接收认证平台返回过来的access_token。
2、客户端配置config.js
首先需要下载或者从admin项目中拷贝出来oidc-client.js文件:
然后就是设计配置文件,我取名为app.js,主要还是连接ids4的相关内容:
var url = window.location.origin;
var settings = {
authority: "https://ids.neters.club",
client_id: "blazorjs",
redirect_uri: url + '/callback',
post_logout_redirect_uri: url,
response_type: 'id_token token',
scope: 'openid profile roles blog.core.api',
popup_redirect_uri: url + '/callback',
popup_post_logout_redirect_uri: url,
silent_redirect_uri: url + '/silent',
automaticSilentRenew: false,
filterProtocolClaims: true,
loadUserInfo: true,
revokeAccessTokenOnSignout: true
};
var mgr = new Oidc.UserManager(settings);
///////////////////////////////
// events
///////////////////////////////
mgr.events.addAccessTokenExpiring(function () {
console.log("token expiring");
// maybe do this code manually if automaticSilentRenew doesn't work for you
mgr.signinSilent().then(function (user) {
console.log("silent renew success", user);
}).catch(function (e) {
console.error("silent renew error", e.message);
})
});
mgr.events.addAccessTokenExpired(function () {
console.log("token expired");
});
mgr.events.addSilentRenewError(function (e) {
console.log("silent renew error", e.message);
});
mgr.events.addUserLoaded(function (user) {
console.log("user loaded", user);
mgr.getUser().then(function () {
console.log("getUser loaded user after userLoaded event fired");
});
});
mgr.events.addUserUnloaded(function (e) {
console.log("user unloaded");
});
这里先看看热闹即可,具体的代码我建议还是直接从我的项目中获取,具体内容不做赘述;
3、blazor项目引用
我们都知道Blazor.Server更像是一个netcore项目,那如何引用js文件呢,很简单,之前的文章中我也讲过,有一个统一的主页面,用来承载整个app,那就是_Host.cshtml,我们就这几在这里引用即可,如果你是用WASM的话,直接有一个index.html,和这个是同一个道理:
(在Blazor.Server中引用js文件)
那现在我们都配置好了客户端和连接,也引用到了Blazor项目里,那如何去调用具体的js方法呢,请往下继续看。
3、C#调用js方法模块
是不是如果你看到这个逻辑都很怪异,我们都知道c#和js完全就不是一个逻辑,那是如何相互调用的呢,不仅c#可以使用js方法,我们也同样能在js里去调用c#代码,当然这是在Blazor框架里,你用mvc还是比较复杂的,平时我们也是习惯用signalR来实现的双工通信。
那我以登录为例子,讲解如何C#调用js吧:
1、注入JS运行时
我们如果想调用js,肯定需要一个运行时环境,这里已经给我们提供给了一个封装好的接口,直接注入即可:
@inject IJSRuntime JS
然后在@code代码块中,我们使用JS,可以看到有两个异步方法:
2、封装扩展方法
这个就是用来帮助我们去Invoke脚本方法的,原理不解释,直接封装扩展:
/// <summary>
/// JSRuntime扩展类
/// 用来调取app.js文件
/// </summary>
public static class JSRuntimeExtensions
{
public async static Task SignInAsync(this IJSRuntime jsRuntime)
{
await jsRuntime.InvokeAsync<object>
("users.startSigninMainWindow");
}
}
括号中的参数呢,是调用的js方法名称, user.xxxx,注意这个格式,下文会将如何写这个js方法 ,而且,也可以传递参数,像这样:
public async static Task SetUserInfoToStorage(
this IJSRuntime jsRuntime, UserInfoModel userInfoModel)
{
await jsRuntime.InvokeAsync<UserInfoModel>
("users.setUserInfoToStorage", userInfoModel);
}
当然也可以用返回值,不过这里有一个小坑,js时间转成c#时间的时候,会少八个小时,自己注意一下就行:
public async static Task<UserInfoModel> GetUserInfoFromStorage(
this IJSRuntime jsRuntime)
{
return await jsRuntime.InvokeAsync<UserInfoModel>
("users.getUserInfoFromStorage");
}
具体的还是看我的源码吧,否则文章会比较长。
3、然后,C#调用扩展
其实也不一定需要封装扩展,直接用原生的invoke也是一样的,不过现在我通过开源了Blog.Core项目以后,越来越多封装情有独钟了。
@code {
protected override async Task OnAfterRenderAsync(
bool firstRender)
{
await JS.SignInAsync();
}
}
是不是很简单,这样就直接可以在c#中,调用js脚本方法了,但是这个js方法任意写function就行了么,并不是。
4、最后,封装js方法
还是用上边的例子: users.startSigninMainWindow 这个方法,对应的js是这样的:
window.users = {
startSigninMainWindow: function () {
// ...
},
}
里边的内容很简单,就是调用上一节的oidc-client的方法,主要是外边的结构,自己把握一下就明白了,对应在浏览器中是这样的,相当于给window窗体增加一个属性:
这个我用着还挺好上手的,如果很多小伙伴不懂的话,以后在单独写文章吧。
到了这里,我们已经配置了ids4模块、c#调用模块,那就剩下最后一个模块: 调用资源服务器的service服务模块了 。
4、调用service模块
不知道大家还记得不记得,在之前的简单的鉴权中,我是通过一个input输入框,手动输入token的方案,还是很low的:
那现在我们就不需要手动配置了,用了ids4后,一切都是自动的,所以还需要继续做封装。
这一部分涉及的代码:
1、获取访问状态——token
在上一节中,我们说到了用c#来调用js,在用户登录成功后,获取用户信息,然后保存到了localstorage里,现在我们如果要发送http请求,就肯定每次获取access_token然后添加到htpp报头里。
public async Task<string> GetAccessToken()
{
var userInfo = await _jS.GetUserInfoFromStorage();
if (!IsLogin(userInfo))
{
await _jS.SignInAsync();
}
return userInfo.AccessToken;
}
public bool IsLogin(UserInfoModel UserInfo) =>
UserInfo != null && UserInfo.AccessToken.IsNotEmptyOrNull() && !UserInfo.IsExpired();
我们这里做了封装,等token失效的时候,会重新去ids4认证中心拉取新的令牌。
2、封装Http操作
上边我们已经获取到了token,接下来就需要发送了,使用的是HttpClient,那每次都设置肯定比较麻烦,感觉再来个封装:
public abstract class BaseService
{
protected BaseService(IServiceProvider serviceProvider)
{
ServiceProvider = serviceProvider;
}
protected IServiceProvider ServiceProvider { get; }
protected HttpClient HttpClient => ServiceProvider.GetService<HttpClient>();
protected IJSRuntime JS => ServiceProvider.GetService<IJSRuntime>();
protected AccessState AccessState => ServiceProvider.GetService<AccessState>();
protected async Task<HttpClient> SecurityHttpClientAsync()
{
var httpClient = ServiceProvider.GetService<HttpClient>();
httpClient.DefaultRequestHeaders.Remove("Authorization");
var token = await AccessState.GetAccessToken();
httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {token}");
httpClient.BaseAddress = new Uri("http://apk.neters.club");
return httpClient;
}
}
这是一个抽象基类,然后每个服务继承了就行了。
PS:这里的资源服务用的Blog.Core的接口,你可以用自己的各种服务,无论是webservice,restful还是grpc。
3、定义Blog具体服务
有了服务基类以后,我们在定义每一个基础服务的时候,就简单了不少,只关注业务逻辑即可,不用关心令牌权限了:
/// <summary>
/// 服务基类
/// 主要用来对Http请求的基础封装
/// </summary>
public class BlogService : BaseService
{
/// <summary>
/// 构造函数
/// </summary>
/// <param name="serviceProvider"></param>
public BlogService(IServiceProvider serviceProvider) :
base(serviceProvider)
{
}
/// <summary>
/// 获取全部博文
/// </summary>
/// <param name="types"></param>
/// <param name="page"></param>
/// <returns></returns>
public async Task<MessageModel<List<BlogArticle>>> GetBlogs(string types, int page = 1)
{
var httpClient = await SecurityHttpClientAsync();
return await httpClient.GetFromJsonAsync<MessageModel<List<BlogArticle>>>
($"/api/Blog/GetBlogsByTypesForMVP?types={types}&page={page}");
}
}
是不是就是很普通的调用接口了!
4、前端调用
前端就很简单了,注入我们的blogservice,然后发送请求即可:
@inject BlogService BlogService
@using Blog.MVP.Blazor.SSR.Pages.Post.component
<h1>编辑</h1>
<Editor BlogArticle="BlogArticle" OnSaveCallback="OnSaveAsync"></Editor>
<div class="text-danger">
@_errmsg
</div>
@code {
[Parameter]
public int Id { get; set; }
private BlogArticle BlogArticle { get; set; }
private string _errmsg = "";
protected override async Task OnInitializedAsync()
{
BlogArticle = (await BlogService.GetBlogByIdForMVP(Id)).response;
}
private async Task OnSaveAsync(BlogArticle blogArticle)
{
BlogArticle = blogArticle;
var result = await BlogService.UpdateBlog(BlogArticle);
if (result.IsSuccessStatusCode)
{
NavManager.NavigateTo("/blog/list");
}
else
{
_errmsg = "保存失败! 错误原因:" + result.ReasonPhrase + "。请重试或登录";
}
}
}
最后别忘了startup注册服务
// services and state
services.AddScoped<BlogService>();
services.AddScoped<AccessState>();
5、总结
经过上边几步的操作,我们已经可以发送请求了,来先看看效果:
(这里有一个小瑕疵,登录后右上角个人信息需要刷新,以后再优化)
已经实现了单点登录、注销,授权验证等等功能,如果没有权限,就提示无权限:
重要说明
虽然我们已经写完了,也很流畅,但是这里有一个问题:
如果想要在页面进入的时候初始化就调用js事件。
比如:如果你想在进入一个页面的时候,就需要权限需要去登录,就比如我的blog/list页面,我在获取service的时候,会先判断access_token,如果不存在就去登录,那这个时候肯定需要调用js事件。
你可能会这么写:
protected override async Task OnInitializedAsync()
{
BlogArticle = (await BlogService.GetBlogByIdForMVP(Id))
.response;
}
但是只要去调用或者去刷新,可能会遇到这个一个问题:
它的意思是,我们不能在初始化的时候对页面进行js操作,必须要页面渲染完成才可以,
那这个时候就要 考虑那三个阶段六个钩子 了,官方已经提醒我们使用OnAfterRenderAsync了,但是又有一个问题是,如果你这么写,页面的data就无法渲染,已经我们这是在页面加载完成了才会获取的service。
经过我搜索stack overflow,发现已经有人趟过了:
https://stackoverflow.com/questions/61438796/javascript-interop-error-when-calling-javascript-from-oninitializedasync-blazor
可见生命周期还是要好好学学的。
好啦,假期也结束了,该收收心了,记得我的DDD领域驱动设计概论视频也发布了,记得去看看,有问题尽量视频下边留言,群里讨论太乱了。
拜拜。