简言之,单点登录就是只需要登录一次,多个平台共享登录状态的操作

此前,单点登录平台接入指南 中已详细介绍了在我们的 web 端平台如何接入单点登录,但这里我们是共用同一个登录页面。因此,现在有了一个新的问题:类似像 IDP 监控大屏这样的项目,有自己的个性化登录页面,若也需要实现单点登录,则需要新的方案

这里主要解决两种情况,相比之前重点是增加了主动登录:

  • 被动登录:在其他地方登录完成后,再打开大屏页面,不需要登录
  • 主动登录:在大屏(或其他个性化登录页)登录完成后,再打开其他系统,不需要登录

一、被动登录

被动登录:在其他地方登录完成后,再打开大屏页面,不需要登录

这里的关键点即是在用户鉴权相关逻辑的改动:

  • 本地调试鉴权(旧方式):请求头增加 loginToken 进行鉴权
  • 线上环境鉴权(新方式):在 Cookie 中增加票据信息 _tk 进行鉴权(生产环境为 _tik)

tk.png

1、步骤一:统一请求逻辑改动(axios.ts)

1.1 请求头变动

  • 请求参数增加 withCredentials: true(请求允许携带 cookie)

    1. const fetch = options => {
    2. ......
    3. let isNeedWithCredentials = true // 是否允许携带cookie , 图片上传的时候不需要
    4. if (url.indexOf('oss-cn-shenzhen.aliyuncs.com') > -1) {
    5. isNeedWithCredentials = false
    6. }
    7. // 在每个方法中增加 withCredentials
    8. // 以 GET 请求为例
    9. return Axios.get(
    10. `${url}${options.data ? `?${qs.stringify(options.data, { indices: false })}`: ''}`,
    11. {
    12. headers: header,
    13. responseType: options.responseType,
    14. withCredentials: isNeedWithCredentials,
    15. },
    16. )
    17. }
  • 请求头 loginToken 处理,仅在本地调试增加

    1. const fetch = options => {
    2. ......
    3. if (BUILD_TYPE === 'loc') {
    4. header.loginToken = window.sessionStorage.idp_screen_loginToken || ''
    5. };
    6. }
  • 请求头增加 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, // }; }

  1. <a name="mBtTQ"></a>
  2. #### 1.2 返回 code - 302 处理
  3. - 注意这里的 code 是后端返回的 code,不是 http 状态码
  4. - 拦截 302 后手动进行登录页返回,并刷新页面
  5. - 若可能会多次触发 302 ,可自行增加防抖处理
  6. ```javascript
  7. // 响应拦截器
  8. Axios.interceptors.response.use(
  9. response => {
  10. const { status, statusText, data, url } = response;
  11. if (BUILD_TYPE !== 'loc' && data.code === '302') {
  12. history.push('/login'); // 返回登录页
  13. window.location.reload(); // 刷新页面
  14. return
  15. }
  16. // 后台管理系统处理方式
  17. // if (BUILD_TYPE !== 'loc' && data.code === '302') {
  18. // if (data.data && data.data.type === 'POST') {
  19. // let rootHtml = document.getElementsByTagName('html')
  20. // rootHtml[0].innerHTML = data.data.content
  21. // window.document.postRedirectSubmitForm.submit();// postRedirectSubmitForm 为返回内容里的form 提交事件
  22. // } else if (data.data && data.data.type === 'GET') {
  23. // window.location.href = data.data.content
  24. // }
  25. // return
  26. // }
  27. },
  28. );

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:注意这里调用的是单点登录认证中心的接口,不是公共网关

  1. {
  2. "code": "000000",
  3. "msg": "Success",
  4. "traceId": "2c2f8b6c70aefbfd",
  5. "data": {
  6. "publicKey": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA5Cy8xGNQ348A4FNoymcTz00qQdQBGMRxAsBqKVCY7PZiepIimGv3sLo301fLZ5xwtFornDg5W9qTrKyDY8Atrj0XC6u6HNnxqjFe0LsDcfLzuJD5HqcDZdkqaoWIebckPmMvSM76y6wPmjRkr/NupiNotp0pnNJWSINn0jmZqV2U2glUlRuEV/hxCdzpPgqs91GnmtNfwX6nQY/2Ldb/CzvgtaFlsT9Q95lmVid3hi7usG3Bm5SfdEEjYG11DzOdicSjB1FbB9w9mHFehOfAKZiOPGDs/bUkeyC5fjQrTdrXVCwtOqbe2kXpitLJ92aR536COAOkNYBUjJRiIqU8HQIDAQAB",
  7. "pbkSha": "5a6c16875f4605ec8406eff584d4d02ac988adea",
  8. "timestamp": "1614580493284",
  9. "uid": "9a85ec4a-c473-472f-b151-69487162c2b0"
  10. }
  11. }

上面是返回的结果,其中:

  • 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'

结合步骤一和步骤二,参考示例如下:

  1. import createApiFunction from '@/services';
  2. import JSEncrypt from 'jsencrypt'
  3. const { localLogin, getKey, onlineLogin, logout } = createApiFunction('login')
  4. const Model: ModelType = {
  5. // ...
  6. effects: {
  7. // 登录请求
  8. *reqLogin({ payload, callback }, { call, put }) {
  9. try {
  10. if (BUILD_TYPE === 'loc') { // 本地调试
  11. !(yield put({ type: 'localLogin', payload })).then((isNeedCallBack: boolean) => {
  12. isNeedCallBack && callback && callback()
  13. })
  14. } else { // 线上登录
  15. const keyObj: KeyObj = yield put.resolve({ type: 'getKey' }) // 步骤一:获取公钥、散列码、唯一码
  16. // 步骤二:加密登录
  17. !(yield put({ type: 'onlineLogin', payload: {...payload, ...keyObj} })).then((isNeedCallBack: boolean) => {
  18. isNeedCallBack && callback && callback()
  19. })
  20. }
  21. } catch (e) {
  22. console.log(e);
  23. }
  24. },
  25. // 步骤一:获取公钥、散列码、唯一码
  26. *getKey({ payload, callback }, { put, call }) {
  27. try {
  28. const res = yield call(getKey)
  29. if (res.code === '000000') {
  30. return res.data
  31. } else {
  32. messageInform(res.msg || '未知的查询消息异常', 'error')
  33. }
  34. } catch (e) {
  35. console.log(e)
  36. }
  37. },
  38. // 步骤二:加密登录
  39. *onlineLogin({ payload, callback }, { call, put }) {
  40. try {
  41. const { publicKey, pbkSha, uid, username, password } = payload
  42. const encrypt = new JSEncrypt()
  43. encrypt.setPublicKey(publicKey)
  44. const loginData = {
  45. loginName: username,
  46. password,
  47. timestamp: +new Date(),
  48. randomCode: uid,
  49. }
  50. const params = {
  51. pbkSha,
  52. loginData: encrypt.encrypt(JSON.stringify(loginData)),
  53. }
  54. const res = yield call(onlineLogin, params, { headers: { 'Content-Type': 'application/x-www-form-urlencoded' } });
  55. if (res.code === '000000') {
  56. return true
  57. } else {
  58. messageInform(res.msg || '未知的查询消息异常', 'error');
  59. }
  60. } catch (e) {
  61. console.log(e);
  62. }
  63. },
  64. }
  65. }

3、步骤三:统一请求逻辑改动(axios.ts)

这里需要对请求头为 'Content-Type': 'application/x-www-form-urlencoded'的情况进行处理,以加密登录的 POST 请求为例:

  1. const fetch = options => {
  2. ......
  3. // 以 POST 请求为例
  4. switch (method.toLowerCase()) {
  5. case 'post':
  6. let newData = data;
  7. if (
  8. String(header['Content-Type']) === 'application/x-www-form-urlencoded'
  9. ) {
  10. // 序列化请求数据
  11. newData = qs.stringify(data);
  12. }
  13. return Axios.post(url, newData, {
  14. headers: header,
  15. responseType: options.responseType,
  16. withCredentials: isNeedWithCredentials,
  17. });
  18. }
  19. }

4、步骤四:登出逻辑

接口地址:POST - http://yingzi-common-app.dev.yingzi.com/api/common/app/v1/auth/web/logout

  1. const { logout } = createApiFunction('login')
  2. const Model: ModelType = {
  3. // ...
  4. effects: {
  5. // 登出逻辑
  6. *reqLogout({ payload, callback }, { call, put }) {
  7. try {
  8. const res = yield call(logout);
  9. if (res.code === '000000') {
  10. // 清除缓存逻辑(视各自项目情况)
  11. // 无需做任何处理,后端返回302 直接在 axios.ts 里执行重定向操作
  12. } else {
  13. messageInform(res.msg || '未知的查询消息异常', 'error');
  14. }
  15. } catch (e) {
  16. console.log(e);
  17. }
  18. },
  19. }
  20. }

三、其他注意点

  • 接入单点登录功能的应用,需要在认证中心注册该应用域名,找陈明处理