前端本地开发注入测试环境登录态

1,727 阅读3分钟

一、开发场景

在中后台前端开发的过程中,有个这样的场景:本地开发环境通过代理的方式联调测试环境的接口;为了能拿到测试环境接口的数据,需要把测试环境登录态(Cookie)拷贝过来种到本地的开发环境中。

二、现有方案

  1. 手动种植Cookie,document.cookie='uploadticket=NhXfYIYXjmdcZNqped2Pfzc4ay8kvGWLFCoeWigy5Qs=;expires=Tue, 10 Mar 2022 10:03:55 GMT'
  2. 浏览器插件种植Cookie,ModHeader等插件。

三、不足之处

现有的两个方案都是可以达到相应的目的,不足之处是需要人为的去操作登录然后拷贝相应的Cookie,同时还有记忆成本,整个过程下来还是比较无趣而烦恼的。

四、根本方式

  1. 实现原理:在webpack-dev-server的proxy配置中增加对应配置,在转发请求的时候增加测试环境的登录态。
  2. 代理配置示例如下:onProxyReq配置项未在文档中提及,通过proxy的类型提示发现,在http-proxy-middleware 模块中有相应的使用方式介绍。
import onProxyReq from '../scripts/on-proxy-req';

export default {
  '/app/api': {
    target: 'http://test.xxx.com/',
    changeOrigin: true,
    pathRewrite: {},
    onProxyReq: onProxyReq
  }
};

五、整体方案

  1. 请求测试环境登录接口,获取登录态(Cookie),把登录态信息写入到本地文件。
const request = require('umi-request').default;
const cookieFilePath = require('./cookie-file-path');
const utils = require('./utils');
const fs = require('fs/promises');

try {
  const json = require(cookieFilePath);
  if (json.expires > Date.now()) {
    // console.log('Cookie信息未失效');
    process.exit();
  }
} catch (err) { }

request.post('http://test.xxx.com/signinSpa', {
  data: {
    username: 'admin',
    password: 'admin'
  },
  requestType: 'form',
  getResponse: true,
  timeout: 3e3,
}).then(({ response }) => {
  // 文件内容
  const data = {
    cookies: {},
    expires: Date.now() + 3 * 24 * 60 * 60 * 1000
  };

  response.headers.forEach((value, key) => {
    if (typeof key == 'string' && key.toLowerCase() == 'set-cookie') {
      const cookie = utils.parseSetCookie(value);
      if (Object.keys(cookie).length > 0) {
        data.cookies[cookie.name] = cookie.value;
      }
    }
  });

  fs.writeFile(
    cookieFilePath,
    JSON.stringify(data, null, '\t')
  ).then(() => {
    console.log('文件写入成功');
  }).catch(() => {
    console.error('文件写入失败');
  });
}).catch(err => {
  console.error(err);
});
  1. 从文件中读取测试环境登录态信息,然后把登录态信息添加到代理请求的头里。
const cookieFilePath = require('./cookie-file-path');
const utils = require('./utils');
let json = {};

try {
  json = require(cookieFilePath);
} catch (err) { }

// 看这里
function onProxyReq(proxyReq) {
  if (Object.keys(json).length > 0) {
    let cookies = proxyReq.getHeader('Cookie');
    cookies = typeof cookies == 'string' ? utils.parseCookie(cookies) : {};
    cookies = Object.assign({}, cookies, json.cookies);
    cookies = utils.stringifyCookieObj(cookies);
    proxyReq.setHeader('Cookie', cookies);
  }
}

module.exports = onProxyReq;
  1. 实现自动化处理:

在npm scripts中增加前置命令,然后再开启本地开发。

{
  "scripts": {
    "start": "node ./scripts/build-cookie-file.js && cross-env BABEL_CACHE=none UMI_ENV=local umi dev",
  }
}

代理配置文件中导入对应的方法。

import onProxyReq from '../scripts/on-proxy-req';

export default {
  '/app/api': {
    target: 'http://test.xxx.com/',
    changeOrigin: true,
    pathRewrite: {},
    onProxyReq: onProxyReq
  }
};
  1. 开发的时候直接运行npm run start就可以了,再也不用关心测试环境登录态了。

六、后续方案(不再使用文件方式存放cookie)

// user-login-status.js
const axios = require('axios').default;
const utils = require('./utils');

const _url = '';
const _username = '';
const _password = '';
const _config = { headers: { 'Content-Type': 'multipart/form-data' } };

function fetchUserLoginStatus(url, data, config) {
  url = url || _url;
  data = Object.assign({ username: _username, password: _password, }, data);
  config = Object.assign({}, _config, config);

  return axios.post(url, data, config).then((res) => {
    const data = res.data || {};
    const headers = res.headers;
    if (data.code == 200 && Array.isArray(headers['set-cookie'])) {
      const arr = headers['set-cookie'];
      for (let i = 0; i < arr.length; i++) {
        const cookie = utils.parseSetCookie(arr[i]);
        if (cookie.name === 'appliedwebsid') {
          const obj = {};
          obj[cookie.name] = cookie.value;
          return obj;
        };
      }
    }
    return null;
  }).catch((err) => {
    console.error(err);
  });
}

let loginStatus;

function getLoginStatus() {
  return loginStatus;
}

function setLoginStatus(url, data, config) {
  return fetchUserLoginStatus(url, data, config).then(cookie => {
    loginStatus = cookie;
  });
}

setLoginStatus();

module.exports = {
  getLoginStatus,
  setLoginStatus,
};

// proxy-event-handler.js
const utils = require('./utils.js');
const ULS = require('./user-login-status.js');
const zlib = require('zlib');
const concat = require('concat-stream');
const debounce = require('lodash/debounce.js')

function onProxyReq(proxyReq) {
  if (ULS.getLoginStatus()) {
    let cookies = proxyReq.getHeader('Cookie');
    cookies = typeof cookies == 'string' ? utils.parseCookie(cookies) : {};
    cookies = Object.assign({}, cookies, ULS.getLoginStatus());
    cookies = utils.stringifyCookieObj(cookies);
    proxyReq.setHeader('Cookie', cookies);
  }
}

const refreshLoginStatus = debounce(
  ULS.setLoginStatus,
  1e4,
  { trailing: false, leading: true }
);

// TODO 如何优雅的防频呢
function checkLoginStatus(data) {
  try {
    data = JSON.parse(data.toString());
    if (data && data.code == '41002') {
      refreshLoginStatus();
    }
  } catch (err) {
    console.log('检查系统登录态时异常', err);
  }
}

function onProxyRes(proxyRes) {
  const contentType = proxyRes.headers['content-type'];
  const contentEncoding = proxyRes.headers['content-encoding'];
  if (typeof contentType === 'string' && contentType.indexOf('application/json') > -1) {
    if (contentEncoding === 'gzip'
      || contentEncoding === 'compress'
      || contentEncoding === 'deflate'
    ) {
      proxyRes.pipe(zlib.createUnzip()).pipe(concat(checkLoginStatus));
    } else {
      proxyRes.pipe(concat(checkLoginStatus));
    }
  }
}

module.exports = {
  onProxyReq: onProxyReq,
  onProxyRes: onProxyRes,
};