简言之,单点登录就是只需要登录一次,多个平台共享登录状态的操作
此前,单点登录平台接入指南 中已详细介绍了在我们的 web 端平台如何接入单点登录,但这里我们是共用同一个登录页面。因此,现在有了一个新的问题:类似像 IDP 监控大屏这样的项目,有自己的个性化登录页面,若也需要实现单点登录,则需要新的方案
这里主要解决两种情况,相比之前重点是增加了主动登录:
- 被动登录:在其他地方登录完成后,再打开大屏页面,不需要登录
- 主动登录:在大屏(或其他个性化登录页)登录完成后,再打开其他系统,不需要登录
一、被动登录
被动登录:在其他地方登录完成后,再打开大屏页面,不需要登录
这里的关键点即是在用户鉴权相关逻辑的改动:
- 本地调试鉴权(旧方式):请求头增加 loginToken 进行鉴权
- 线上环境鉴权(新方式):在 Cookie 中增加票据信息 _tk 进行鉴权(生产环境为 _tik)
1、步骤一:统一请求逻辑改动(axios.ts)
1.1 请求头变动
请求参数增加
withCredentials: true(请求允许携带 cookie)const fetch = options => {......let isNeedWithCredentials = true // 是否允许携带cookie , 图片上传的时候不需要if (url.indexOf('oss-cn-shenzhen.aliyuncs.com') > -1) {isNeedWithCredentials = false}// 在每个方法中增加 withCredentials// 以 GET 请求为例return Axios.get(`${url}${options.data ? `?${qs.stringify(options.data, { indices: false })}`: ''}`,{headers: header,responseType: options.responseType,withCredentials: isNeedWithCredentials,},)}
请求头 loginToken 处理,仅在本地调试增加
const fetch = options => {......if (BUILD_TYPE === 'loc') {header.loginToken = window.sessionStorage.idp_screen_loginToken || ''};}
请求头增加 bizPageUrl ```javascript // 过滤地址 // const filterHref = () => { // let url = window.location.href // return url.indexOf(‘/user/login’) > -1 ? url.split(“#”)[0] : url // }
const fetch = options => { …… const header = { bizPageUrl: window.location.href, // 单点登录功能添加 …headers, };
// 在我们的后台管理系统中,bizPageUrl 用如下过滤地址 // const header = { // bizPageUrl: filterHref() // …headers, // }; }
<a name="mBtTQ"></a>#### 1.2 返回 code - 302 处理- 注意这里的 code 是后端返回的 code,不是 http 状态码- 拦截 302 后手动进行登录页返回,并刷新页面- 若可能会多次触发 302 ,可自行增加防抖处理```javascript// 响应拦截器Axios.interceptors.response.use(response => {const { status, statusText, data, url } = response;if (BUILD_TYPE !== 'loc' && data.code === '302') {history.push('/login'); // 返回登录页window.location.reload(); // 刷新页面return}// 后台管理系统处理方式// if (BUILD_TYPE !== 'loc' && data.code === '302') {// if (data.data && data.data.type === 'POST') {// let rootHtml = document.getElementsByTagName('html')// rootHtml[0].innerHTML = data.data.content// window.document.postRedirectSubmitForm.submit();// postRedirectSubmitForm 为返回内容里的form 提交事件// } else if (data.data && data.data.type === 'GET') {// window.location.href = data.data.content// }// return// }},);
2、步骤二:获取用户信息
当用户鉴权会话信息通过时,我们需要在主组件(如:basicLayout.js)首先获取用户信息,再执行后面的操作
接口地址:
GET - http://yingzi-common-app.dev.yingzi.com/api/common/app/v1/account/current/detail
二、主动登录
主动登录:在大屏(或其他个性化登录页)登录完成后,再打开其他系统,不需要登录
原来统一登录页的逻辑和部署都是由后端进行控制的,但现在我们前端的个性化登录页面,这里的逻辑需要我们自己处理
1、步骤一:获取公钥、散列码、唯一码
接口地址:GET - http://passport.dev.yingzi.com/security/keyPair/public
PS:注意这里调用的是单点登录认证中心的接口,不是公共网关
{"code": "000000","msg": "Success","traceId": "2c2f8b6c70aefbfd","data": {"publicKey": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA5Cy8xGNQ348A4FNoymcTz00qQdQBGMRxAsBqKVCY7PZiepIimGv3sLo301fLZ5xwtFornDg5W9qTrKyDY8Atrj0XC6u6HNnxqjFe0LsDcfLzuJD5HqcDZdkqaoWIebckPmMvSM76y6wPmjRkr/NupiNotp0pnNJWSINn0jmZqV2U2glUlRuEV/hxCdzpPgqs91GnmtNfwX6nQY/2Ldb/CzvgtaFlsT9Q95lmVid3hi7usG3Bm5SfdEEjYG11DzOdicSjB1FbB9w9mHFehOfAKZiOPGDs/bUkeyC5fjQrTdrXVCwtOqbe2kXpitLJ92aR536COAOkNYBUjJRiIqU8HQIDAQAB","pbkSha": "5a6c16875f4605ec8406eff584d4d02ac988adea","timestamp": "1614580493284","uid": "9a85ec4a-c473-472f-b151-69487162c2b0"}}
上面是返回的结果,其中:
- publicKey - 公钥
- pbkSha - 散列码
- timestamp - 时间戳
- uid - 唯一码
2、步骤二:加密登录
接口地址:POST - http://passport.dev.yingzi.com/auth/web/login
请求体参数:
- pbkSha - 散列码
- loginData
- loginName - 用户名
- password - 密码
- timestamp - 时间戳
- randomCode - 唯一码
接着,在调用接口时,需要
- 对 loginData 进行非对称加密
- 请求头使用
'Content-Type': 'application/x-www-form-urlencoded'
结合步骤一和步骤二,参考示例如下:
import createApiFunction from '@/services';import JSEncrypt from 'jsencrypt'const { localLogin, getKey, onlineLogin, logout } = createApiFunction('login')const Model: ModelType = {// ...effects: {// 登录请求*reqLogin({ payload, callback }, { call, put }) {try {if (BUILD_TYPE === 'loc') { // 本地调试!(yield put({ type: 'localLogin', payload })).then((isNeedCallBack: boolean) => {isNeedCallBack && callback && callback()})} else { // 线上登录const keyObj: KeyObj = yield put.resolve({ type: 'getKey' }) // 步骤一:获取公钥、散列码、唯一码// 步骤二:加密登录!(yield put({ type: 'onlineLogin', payload: {...payload, ...keyObj} })).then((isNeedCallBack: boolean) => {isNeedCallBack && callback && callback()})}} catch (e) {console.log(e);}},// 步骤一:获取公钥、散列码、唯一码*getKey({ payload, callback }, { put, call }) {try {const res = yield call(getKey)if (res.code === '000000') {return res.data} else {messageInform(res.msg || '未知的查询消息异常', 'error')}} catch (e) {console.log(e)}},// 步骤二:加密登录*onlineLogin({ payload, callback }, { call, put }) {try {const { publicKey, pbkSha, uid, username, password } = payloadconst encrypt = new JSEncrypt()encrypt.setPublicKey(publicKey)const loginData = {loginName: username,password,timestamp: +new Date(),randomCode: uid,}const params = {pbkSha,loginData: encrypt.encrypt(JSON.stringify(loginData)),}const res = yield call(onlineLogin, params, { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } });if (res.code === '000000') {return true} else {messageInform(res.msg || '未知的查询消息异常', 'error');}} catch (e) {console.log(e);}},}}
3、步骤三:统一请求逻辑改动(axios.ts)
这里需要对请求头为 'Content-Type': 'application/x-www-form-urlencoded'的情况进行处理,以加密登录的 POST 请求为例:
const fetch = options => {......// 以 POST 请求为例switch (method.toLowerCase()) {case 'post':let newData = data;if (String(header['Content-Type']) === 'application/x-www-form-urlencoded') {// 序列化请求数据newData = qs.stringify(data);}return Axios.post(url, newData, {headers: header,responseType: options.responseType,withCredentials: isNeedWithCredentials,});}}
4、步骤四:登出逻辑
接口地址:POST - http://yingzi-common-app.dev.yingzi.com/api/common/app/v1/auth/web/logout
const { logout } = createApiFunction('login')const Model: ModelType = {// ...effects: {// 登出逻辑*reqLogout({ payload, callback }, { call, put }) {try {const res = yield call(logout);if (res.code === '000000') {// 清除缓存逻辑(视各自项目情况)// 无需做任何处理,后端返回302 直接在 axios.ts 里执行重定向操作} else {messageInform(res.msg || '未知的查询消息异常', 'error');}} catch (e) {console.log(e);}},}}
三、其他注意点
- 接入单点登录功能的应用,需要在认证中心注册该应用域名,找陈明处理
