记一次无数据库下动态更新文案的解决历程

背景

一个简单官网 www.xxx.cn,使用 vue + nuxt 作为技术栈,做 ssr;

文案一开始是写死,后面产品提需求了,说他们想要可以随时修改这些文案

好吧,那只能改成动态加载文案了...

解决

提取加载

因为该网站比较轻量,目前还不需要配备一个数据库,

那么在无数据库的情况下,怎么办呢,一般最先想到的就是把文案提取出来,

作为一个静态资源 json 存起来,在 ssr 运行时,提前加载

// 提前加载数据
async asyncData({ $axios }) {
  // $axios 需要自己去安装
  // 这里把 host 做一次提取,直接 axios.get('/secret/download.json') 会有问题
  // ssr 单页应用中
  // 1.切换到当前页面 和 2.在当前页面刷新 
  // 这两种情况中 "/secret/download.json" 这个请求中浏览器自动匹配的 host 不一样的情况
  let host = '' 
  if (typeof window !== 'undefined') {
    host = window.location.origin
  } else {
    host = ''
  }
  let {
    data: { dynamicText }
  } = await $axios.get(`${host}/secret/download.json`)

  return {
    dynamicText // 返回 dynamicText 作为 vue 组件实例的 data 的属性
  }
},
// 提前加载数据
async asyncData({ $axios }) {
  // $axios 需要自己去安装
  // 这里把 host 做一次提取,直接 axios.get('/secret/download.json') 会有问题
  // ssr 单页应用中
  // 1.切换到当前页面 和 2.在当前页面刷新 
  // 这两种情况中 "/secret/download.json" 这个请求中浏览器自动匹配的 host 不一样的情况
  let host = '' 
  if (typeof window !== 'undefined') {
    host = window.location.origin
  } else {
    host = ''
  }
  let {
    data: { dynamicText }
  } = await $axios.get(`${host}/secret/download.json`)

  return {
    dynamicText // 返回 dynamicText 作为 vue 组件实例的 data 的属性
  }
},

加载完成后,页面中拿到动态文案数据 dynamicText,此时页面正常渲染出动态文案

修改

在官网上添加一个用于修改该 json 文件的页面

该页面也需要加载 json 文件,用于展示在页面表单中,让产品修改

加载 json 文件的代码和上面一样,重点是如何提交修改

methods: {
  // 提交修改
  async submit() {
    let formData = []
    // 收集表单中的所有数据(不管有没有修改)
    this.dynamicText.forEach((column, index) => {
      formData.push(formSerialize('form' + index))
    })
    // 提交数据,进行数据重写
    let { res } = axios.post('/updateDynamicText', { data: formData }, { withCredentials: true })
    // 请求失败等情况忽略
    setTimeout(() => {
      if (res.data.code === 0) {
        // 成功刷新页面看结果
        window.location.reload()
      }
    }, 2000)
  }
}
methods: {
  // 提交修改
  async submit() {
    let formData = []
    // 收集表单中的所有数据(不管有没有修改)
    this.dynamicText.forEach((column, index) => {
      formData.push(formSerialize('form' + index))
    })
    // 提交数据,进行数据重写
    let { res } = axios.post('/updateDynamicText', { data: formData }, { withCredentials: true })
    // 请求失败等情况忽略
    setTimeout(() => {
      if (res.data.code === 0) {
        // 成功刷新页面看结果
        window.location.reload()
      }
    }, 2000)
  }
}

后端接口

修改提交的数据自然是要通过后端的,这里我们是 ssr,后端是 node,接口当然得自己写了

在 nuxt 目录下的 server 目录中的 index.js 中

引入 koa-router,koa-bodyparse,router.js,router.js 写一个接口用于修改json文件

// router.js
// 修改文案内容
router.post('/updateDynamicText', async (ctx, next) => {
  // 修改文案
  let formData = ctx.request.body
  await new Promise(resolve => {
    fs.writeFile(
      path.resolve(__dirname, '../static/secret/download.json'),
      JSON.stringify({ dynamicText: formData.data }),
      err => {
        if (err) return err
        ctx.body = {
          code: 200,
          msg: '成功'
        }
        resolve()
      }
    )
  })
})

// router.js
// 修改文案内容
router.post('/updateDynamicText', async (ctx, next) => {
  // 修改文案
  let formData = ctx.request.body
  await new Promise(resolve => {
    fs.writeFile(
      path.resolve(__dirname, '../static/secret/download.json'),
      JSON.stringify({ dynamicText: formData.data }),
      err => {
        if (err) return err
        ctx.body = {
          code: 200,
          msg: '成功'
        }
        resolve()
      }
    )
  })
})

这就大功告成了,YES~~~ 。。。。

这么搞的话,就是作死了
该修改页面是暴露到外网的,没有权限控制,如果被别人知道这个页面,想咋改就咋改,想怎么玩就怎么玩

怎么办呢。。。

刚好公司内部有一个统一的登录账号和鉴权接口

https://i.oa.xxx.im/verify_cookies
验证 cookie 为
xxx_name: xxx_name
xxx_sign: xxx_sign
https://i.oa.xxx.im/verify_cookies
验证 cookie 为
xxx_name: xxx_name
xxx_sign: xxx_sign

我搭建了一个 oauth,外网要修改的时候要经过当次鉴权

问题又出来了:

1. 当次修改请求需要携带 cookie 用来鉴权,但是官网现在并没有登录注册功能
2. 就算有登录注册功能,该 cookie 也不能用,因为官网 www.xxx.cn 和 i.oa.xxx.im 不能共享 cookie
3. 该鉴权接口只能使用内网机器访问调用
1. 当次修改请求需要携带 cookie 用来鉴权,但是官网现在并没有登录注册功能
2. 就算有登录注册功能,该 cookie 也不能用,因为官网 www.xxx.cn 和 i.oa.xxx.im 不能共享 cookie
3. 该鉴权接口只能使用内网机器访问调用

有啥问题,就解决啥问题

我们曲线救国,新创建一个项目,专门用来做鉴权服务,该服务运行在内网上,当然还是 node 服务了

这次我把修改 json 文件的页面搬到内部一个中控资源修改平台,域名比如:source.xxx.im

1. 先到 i.oa.xxx.im 登录页面进行登录,确认为内部员工 -> 
2. 进入 source.xxx.im/updateDynamicText 页面,即修改 json 文件的页面
可以展示动态文案,因为文案是静态资源,可以 get 获取,
修改文案后,对新起的鉴权服务器发起修改请求 -> 
3. 新起的服务请求地址我设置为 write.xxx.im,因此可以拿到 cookie 信息
1. 先到 i.oa.xxx.im 登录页面进行登录,确认为内部员工 -> 
2. 进入 source.xxx.im/updateDynamicText 页面,即修改 json 文件的页面
可以展示动态文案,因为文案是静态资源,可以 get 获取,
修改文案后,对新起的鉴权服务器发起修改请求 -> 
3. 新起的服务请求地址我设置为 write.xxx.im,因此可以拿到 cookie 信息

下面进入鉴权操作

let userName = ctx.cookies.get('xxx_name')
let userSign = ctx.cookies.get('xxx_sign')
let reqData = ctx.request.body // 下面鉴权成功后要使用到,里面是当次修改过的动态文案数据
// 用户鉴权
let auth = await new Promise(resolve => {
  https.get(
    {
      hostname: 'i.oa.xxx.im',
      // 这里 path 特别注意要加 /,不然请求无效
      path: '/verify_cookies',
      headers: {
        cookie: `xxx_name=${userName};xxx_sign=${userSign}`
      }
    },
    res => {
      let data = []
      res.on('data', chunk => {
        data.push(chunk)
      })
      res.on('end', () => {
        let sData = data.toString()
        if (sData !== 'True') {
          ctx.body = {
            code: 404,
            data: '用户鉴权失败'
          }
          // 用户鉴权失败
          resolve(0)
        } else {
          // 用户鉴权通过
          resolve(1)
        }
      })
    }
  )
})
let userName = ctx.cookies.get('xxx_name')
let userSign = ctx.cookies.get('xxx_sign')
let reqData = ctx.request.body // 下面鉴权成功后要使用到,里面是当次修改过的动态文案数据
// 用户鉴权
let auth = await new Promise(resolve => {
  https.get(
    {
      hostname: 'i.oa.xxx.im',
      // 这里 path 特别注意要加 /,不然请求无效
      path: '/verify_cookies',
      headers: {
        cookie: `xxx_name=${userName};xxx_sign=${userSign}`
      }
    },
    res => {
      let data = []
      res.on('data', chunk => {
        data.push(chunk)
      })
      res.on('end', () => {
        let sData = data.toString()
        if (sData !== 'True') {
          ctx.body = {
            code: 404,
            data: '用户鉴权失败'
          }
          // 用户鉴权失败
          resolve(0)
        } else {
          // 用户鉴权通过
          resolve(1)
        }
      })
    }
  )
})

鉴权通过后

if (auth) {
  await new Promise(resolve => {
    let req = https.request(
      {
        hostname: 'www.xxx.cn',
        path: '/updateDynamicText',
        method: 'POST',
        headers: {
          'Content-type': 'application/json'
        }
      },
      res => {
        let data = []
        res.on('data', chunk => {
          data.push(chunk)
        })
        res.on('end', () => {
          let sData = JSON.parse(data.toString())
          if (sData.code === 200) {
            ctx.body = {
              code: 200,
              data: sData.msg
            }
          } else {
            ctx.body = {
              code: 404,
              data: sData.msg
            }
          }
          resolve()
        })
      }
    )
    // 此处的 name 和 pass 为 www.xxx.cn/updateDynamicText 接口的要验证的账号和密码
    req.write(JSON.stringify({data: reqData.dynamicText, name: 'name', pass: 'pass'}))
    req.end()
  })
}
if (auth) {
  await new Promise(resolve => {
    let req = https.request(
      {
        hostname: 'www.xxx.cn',
        path: '/updateDynamicText',
        method: 'POST',
        headers: {
          'Content-type': 'application/json'
        }
      },
      res => {
        let data = []
        res.on('data', chunk => {
          data.push(chunk)
        })
        res.on('end', () => {
          let sData = JSON.parse(data.toString())
          if (sData.code === 200) {
            ctx.body = {
              code: 200,
              data: sData.msg
            }
          } else {
            ctx.body = {
              code: 404,
              data: sData.msg
            }
          }
          resolve()
        })
      }
    )
    // 此处的 name 和 pass 为 www.xxx.cn/updateDynamicText 接口的要验证的账号和密码
    req.write(JSON.stringify({data: reqData.dynamicText, name: 'name', pass: 'pass'}))
    req.end()
  })
}

www.xxx.cn/updateDynamicText 接口需要加入账号和密码的验证

// secret.js 存储账号和密码
module.exports = {
  name: 'name',
  pass: 'pass'
}

// router.js
let formData = ctx.request.body
const config = require('./secret')
if (formData.name !== config.name || formData.pass !== config.pass) {
  ctx.body = {
    code: 403,
    msg: '没有权限'
  }
  return
}
// 修改文案内容
router.post('/updateDynamicText', async (ctx, next) => {
  // 修改文案
  let formData = ctx.request.body
  await new Promise(resolve => {
    fs.writeFile(
      path.resolve(__dirname, '../static/secret/download.json'),
      JSON.stringify({ dynamicText: formData.data }),
      err => {
        if (err) return err
        ctx.body = {
          code: 200,
          msg: '成功'
        }
        resolve()
      }
    )
  })
})
// secret.js 存储账号和密码
module.exports = {
  name: 'name',
  pass: 'pass'
}

// router.js
let formData = ctx.request.body
const config = require('./secret')
if (formData.name !== config.name || formData.pass !== config.pass) {
  ctx.body = {
    code: 403,
    msg: '没有权限'
  }
  return
}
// 修改文案内容
router.post('/updateDynamicText', async (ctx, next) => {
  // 修改文案
  let formData = ctx.request.body
  await new Promise(resolve => {
    fs.writeFile(
      path.resolve(__dirname, '../static/secret/download.json'),
      JSON.stringify({ dynamicText: formData.data }),
      err => {
        if (err) return err
        ctx.body = {
          code: 200,
          msg: '成功'
        }
        resolve()
      }
    )
  })
})

整个流程:

i.oa.xxx.im -> 登录
source.xxx.im -> 请求修改
write.xxx.im -> 鉴权,接过请求修改的数据,发起真正修改数据的请求
www.xxx.cn -> 校验账号密码,成功后修改动态文案数据
i.oa.xxx.im -> 登录
source.xxx.im -> 请求修改
write.xxx.im -> 鉴权,接过请求修改的数据,发起真正修改数据的请求
www.xxx.cn -> 校验账号密码,成功后修改动态文案数据

感觉流程很长,还新起了一个项目服务,而且账号密码是写死的,如果被离职人员恶搞咋办。。。

头疼。。。

突然想到阿里云有个 oss 服务

把流程简化一下

官网这边:

1. 将 www.xxx.cn/updateDynamicText 接口废弃
2. 请求动态数据去阿里云 OSS 请求
1. 将 www.xxx.cn/updateDynamicText 接口废弃
2. 请求动态数据去阿里云 OSS 请求
async asyncData({ $axios }) {
  let {
    data: { dynamicText }
  } = await $axios.get(
    'https://xxx.aliyuncs.com/dynamicText'
  )
  return {
    dynamicText
  }
}
async asyncData({ $axios }) {
  let {
    data: { dynamicText }
  } = await $axios.get(
    'https://xxx.aliyuncs.com/dynamicText'
  )
  return {
    dynamicText
  }
}

发起请求修改数据的这边 source.xxx.im

// 请求数据
async created() {
  let { dynamicText } = await axios.get('http://xxx.aliyuncs.com/dynamicText')
  this.dynamicText = dynamicText

  // 调用阿里 OSS SDK 
  this.aliClient = new OSS({
    region: 'oss-cn-shanghai',
    accessKeyId: 'accessKeyId',
    accessKeySecret: 'accessKeySecret',
    bucket: 'xxx-public'
  })
},
methods: {
  // 更新文案
  async putBucket(data) {
    try {
      let result = await this.aliClient.put('dynamicText', new Blob([JSON.stringify(data)], {
        type: 'application/json;charset=utf-8'
      }))
      this.status = '修改成功'
      setTimeout(() => {
        window.location.reload()
      }, 2000)
    } catch(err) {
      this.status = '修改失败'
    }
  },
  // 发起提交数据请求
  submit() {
    this.status = ''
    let formData = []
    this.columns.forEach((column, index) => {
      formData.push(formSerialize('form' + index))
    })

    this.putBucket({ dynamicText: formData})
  }
}
// 请求数据
async created() {
  let { dynamicText } = await axios.get('http://xxx.aliyuncs.com/dynamicText')
  this.dynamicText = dynamicText

  // 调用阿里 OSS SDK 
  this.aliClient = new OSS({
    region: 'oss-cn-shanghai',
    accessKeyId: 'accessKeyId',
    accessKeySecret: 'accessKeySecret',
    bucket: 'xxx-public'
  })
},
methods: {
  // 更新文案
  async putBucket(data) {
    try {
      let result = await this.aliClient.put('dynamicText', new Blob([JSON.stringify(data)], {
        type: 'application/json;charset=utf-8'
      }))
      this.status = '修改成功'
      setTimeout(() => {
        window.location.reload()
      }, 2000)
    } catch(err) {
      this.status = '修改失败'
    }
  },
  // 发起提交数据请求
  submit() {
    this.status = ''
    let formData = []
    this.columns.forEach((column, index) => {
      formData.push(formSerialize('form' + index))
    })

    this.putBucket({ dynamicText: formData})
  }
}

整个流程变为:

source.xxx.im -> 请求修改阿里 OSS
source.xxx.im -> 请求修改阿里 OSS

这里还有个问题, 调用 OSS SDK 时的 accessKeyId/accessKeySecret 是直接暴露出来的

危险危险 还是会被恶搞,咋办呢~~~

整个流程再变为:

source.xxx.im -> 请求修改阿里 OSS
write.xxx.im -> 鉴权,接过请求修改的数据,发起真正修改阿里 OSS
source.xxx.im -> 请求修改阿里 OSS
write.xxx.im -> 鉴权,接过请求修改的数据,发起真正修改阿里 OSS

然而 accessKeyId/accessKeySecret 还是暴露在代码里面的,也会被开发人员直观看到

~~~最终还是避免不了泄漏的风险,话说回来,在程序世界有绝对的安全吗