wushf.top下挂载的服务会越来越多,大多数服务是拉取自第三方仓库。
导致每个服务都有自己的鉴权方式,需要维护多套密码。
原作者未必为自己的项目提供单点登录的接口,需要自己考虑解决方案,暂行的方案是:
常见的解决方案是提供一个正方形的标准二维码,扫码登陆。
但是这个要多方备案,还要认证成个体工商户或者企业,需要交钱,比较麻烦。
暂行的方案是:
网页端请求生成新的小程序二维码
根据官方文档,可以携带一个scene。
生成一个sessionId作为会话的唯一标识,将会话作为scene加入到微信小程序二维码中。
sessionId,就知道了“我要授权哪个会话”。二维码有扫描状态,被一个用户扫描之后应不能再被其他用户扫描。
网页轮询当前会话的状态,如已被扫描,那么登录成功,跳转到下一级页面。
这只是大概思路,授权涉及系统的安全性,目前开放的code服务会允许用户借助code-server服务修改开发机文件,因此必须要做好鉴权。
生成sessionId,有一个经典的解决方案,就是生成随机数,检查是否已经存在,若已存在,那么重新生成。
这种方法大概率是没问题的。
但是还是实现了一个无碰撞的生成方案。
观察到一个凭证只在短时间内有效,目前设置的是时长2min的滑动窗口。
那么2min之前生成的凭证,已经在Redis中过期了,不需要再判断有没有碰撞。
可以取当下的时间戳,用2min取模,模数恰好是6位。

代码中目前用的是3min,做一下滑动窗口长度的冗余。
sessionId是一个六位的数字,是不是可以暴力枚举,查询服务器所有会话的状态。
这样显然是不安全的。
在向浏览器传递sessionId时,携带Jwt作为auth凭证。
只有Jwt的负载部分与sessionId匹配时,才允许访问。

期望是有权限的用户才有权限授权。
利用SpringSecurity,在对访问鉴权之后,会在上下文存储权限列表。
可以在下文直接取出。
如果权限列表没有我需要的权限,那么阻止授权。
考虑到未来会扩展更多的服务,需要配置不同的权限,所以授权页面也做了对未来的支持。

在路径中传入申请的权限,在登录时阻止无权用户。
考虑到有权用户未来可能无权,也需要阻止。
权限维护在数据库中,请求服务的时候还是再次验证。
Redis中也塞入了sessionId和token的临时绑定关系,做了二次验证。


用户的凭证和浏览器的凭证格式是不一样的。
当用户查询状态时,如果是第一个用户,那么说明该用户手机扫码了,才能知道这个sessionId并授权。
浏览器查询时,通过凭证验证身份,必须和当前会话一致才能查询。

这个比较简单粗暴。
能通过SpringSecurity,走到这一步,一定是凭证合法的。
验证当前用户的权限列表。有权则允许访问。
简单解耦:权限变更不需要重新编译代码。

原计划是搞个导航栏,搞个弹窗的,搞几个用户管理的页面的。
然后发现现在的服务接口有点少,很少有权限操作,上号用数据库也够用。
所以撤回了导航栏和弹窗,只保留了登录界面。

个人觉得外观还是挺现代化的,登录即是授权:

未来的增长点之一。

创建一个全局的弹窗容器,在需要时填入组件。
容器是否显示,内部填充哪个组件,等等状态,通过pinia维护并在组件间通信。
亲测如果没有动画,会等待两秒办然后出现,比较丑陋。
让GPT生成了一个加载动画,一个转圈的圆形:

为图片加了个透明度过渡动画,图片准备完成后渐显。

初始方案是TS基础模板。
发现兼容性太差了,微信开发者工具提供的初始模板,里面好多语法已经被微信官方弃用了,顶部导航栏在不同设备上高度还不一样。
而且类型检查很差,我也不知道在微信小程序中应该定义成什么类型,是否已被弃用。
最终用的是JS+Sass。
小程序功能比较简单。
加了个渐变的圆反馈状态。
想动态修改圆的颜色和速度,发现纯css有点困难,小程序的语法兼容性问题有点难搞。
最后改成了canvas画圆。
但是canvas会强制处于最高层,无论其他组件设置多少z-index。所以画圆的时候还要画个字母。


前文提到,首个用户扫码会自动修改状态为已扫描。
如何判断是不是当前用户,就看Jwt负责部分是不是自己。
因为Jwt凭证的前两个部分header都是明文转Base64。
从Base64转回JSON字符串就可以。
比较坑的是aotb在微信开发者工具中能用,手机上不能用。
export const atobPolyfill = (base64) => {
var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
var output = '';
var buffer = 0;
var bitCount = 0;
for (var i = 0; i < base64.length; i++) {
var char = base64.charAt(i);
if (char === '=') break;
buffer = (buffer << 6) | chars.indexOf(char);
bitCount += 6;
if (bitCount >= 8) {
bitCount -= 8;
output += String.fromCharCode((buffer >> bitCount) & 0xFF);
buffer &= (1 << bitCount) - 1;
}
}
return output;
}