前言
前面选用了IdentityServer4做为认证授权的基础框架,感兴趣的可以看上篇<微服务下认证授权框架的探讨>,已经初步完成了authorization-code与implicit的简易demo(html+js 在IIS部署的站点),并实现了SSO,本想着将Demo迁移到vue工程是轻而易举,毕竟也没啥东西,最终拿到access_token,存储到store里,跟传统的jwt基本上一样,不出意外,意外总是会发生,不然也没办法水一篇帖子,玩笑归玩笑,如果有理解错误,或者使用不当的地方,欢迎拍砖,以免误人子弟
环境角色
- vue2.0 cli
- .net core 6.0
这里为什么不用vue3,尝试过,最终放弃了,本来就是半吊子的前端水平,vue3里的vite跟ts,极大的增加了学习成本,包升级后一些莫名其妙的bug,特别是vite,一些bug提示对我来说特别陌生与不友好(项目不报错,js控制台报错,好像是兼容之类的问题),总之还是太菜,解决不了,又菜又爱玩
准备动手
- http://localhost:3000/test 测试页面,用来代替原来的登录页,用于保存access_token
- http://localhost:3000/callback 回调页面,用于将code换取access_token
- http://localhost:3000/logout 退出页面,用于清除localStorage缓存
在IdentityServer4里,对于前端工程提供了oidc-client.js这个包,在github开源,不过在2021年左右就停止维护了,这里我收集了两条跟本次吐槽相关的Issues
https://github.com/IdentityModel/oidc-client-js/issues/1393
https://github.com/IdentityModel/oidc-client-js/issues/1360
OAuth2.0 与 OAuth2.1
吓了一跳,OAuth2.1是个什么玩意?来看看GPT的回答
原来官方还没有这个东西,市面上一些对OAuth2.0拓展或者增强的东西,搞了个OAuth2.1,或许只是一个草案,本次吐槽的坑点就是oidc-client.js里PKCE的使用,PKCE全称是 Proof Key for Code Exchange, 在2015年发布, 它是 OAuth 2.0 核心的一个扩展协议, 所以可以和现有的授权模式结合使用,比如 Authorization Code + PKCE,东西是好东西,但是强制使用,不与之前的OAuth2.0兼容,就要被骂了
啥是PKCE?
大白话,就是防止授权码(code)被坏人拦截,而设计的一个拓展协议,坏人拿到了code也换取不了access_token,它的实现过程也非常简单,三两句就能讲明白,就不画图了,直接上代码截图
三个角色,授权服务,前端工程,后端工程
- 认证中心登录,回调页面上多了两个参数
code_challenge_method 签名方法
code_challenge 签名
- 使用code换取access_token的时候,要多带一个code_verifier(随机生成的一个字符串) 参数,否则兑换失败
code_challenge = code_challenge_method(code_verifier)
- 协议约定,如果在认证中心生成code的时候,携带了code_challenge_method与code_challenge,那么在使用code兑换
access_token的时候,必须携带code_verifier
如果这还听不明白,那我举个古代银票防伪的例子
张三去钱庄存银子的时候,跟钱庄做了约定,留下一句暗号(DNF)跟暗号的生成方式(取每个字的首字符)给钱庄,兑换者在用银票换银子的时候,钱庄会检查这张银票有没有跟张三做约定,如果有约定,则按照约定让兑换者提供明文,钱庄用兑换者提供的明文跟张三提供的暗号生成方式,来生成暗号,如果一致则允许兑换银子
银票 = code
暗号 = 签名(code_challenge )
暗号的生成方式 = 签名方法(code_challenge_method )
明文 = code_verifier(随机字符串)
为啥要喷oidc-client.js里PKCE的实现?
- PKCE关不了
var config = {
authority: "https://localhost:6201",
...
pkce:false
};
前面Issues有提过,提供了这个配置,但是不生效,总之一定会带上
- 回调逻辑存在问题
var userManager = new Oidc.UserManager(config);
userManager.signinRedirectCallback().then(function(user) {
// 登录成功,可以在这里处理登录后的逻辑,例如重定向到首页或显示欢迎信息
console.log('用户已登录:', user);
window.location.href = '/home'; // 重定向到应用的主页
}).catch(function(error) {
// 处理登录失败的情况
console.error('登录失败:', error);
alert('登录过程中发生错误,请重试。');
});
这是oidc-client.js里回调页面的处理,它自动做了处理,但是它绕过后端程序,直接向认证服务发起,这里返回400错误,是因为该客户端必须校验秘钥,我们一般将秘钥存储在后端程序,前端程序请求后端,后端带上客户端秘钥,将请求转发给认证服务,这才是authorization-code标准的认证流程,也相对安全
- 相同的版本,在(html+js)站点下,并没有启用PKCE,而在vue-cli工程,默认启用PKCE
这点也是我最郁闷的地方,最开始我怀疑是版本问题导致的,后面我降级到相同的版本,但是结果却还是一样,折腾了好久,依旧没有解决,*******
解决办法
- 后端强行关闭PKCE认证
既然vue-cli工程里发起的获取code,会携带PKCE认证信息,那我在重定向地址里,把这两个参数移除,那就搞定收工了
- 啃oidc-client.js源码,翻Issues,看看有没有转机
先看Issues,把问题关键字贴进去搜一下,很遗憾没有找到有用的东西,源码简单的翻了下,也没啥帮助
- 逆向oidc-client.js,重写回调页面逻辑
既然我们缺code_verifier,那就去找,打印config.userStore,就找到了,这得感谢作者没有每次都删除该对象,不然可能就真的芭比Q了,知道了位置,我们直接从Localstorage取,然后根据时间戳取最新的那条记录,然后提取出code_verifier
function getCodeVerifier() {
const allKeys = [];
const items =[];
for (let i = 0; i < localStorage.length; i++) {
allKeys.push(localStorage.key(i));
}
const pattern = /^oidc\./;
const oidcKeys = allKeys.filter(key => pattern.test(key));
for(let i =0;i< oidcKeys.length;i++){
const str = localStorage.getItem(oidcKeys[i]);
if(str != null && str != ''){
const model = JSON.parse(str);
if(model.created == '' || model.created == null || model.created == undefined){
continue
}
items.push(model)
localStorage.removeItem(oidcKeys[i])
}
}
const item = items.reduce((nearest, current) => {
return (nearest.created > current.created) ? nearest : current;
});
return item.code_verifier;
}
最终效果