记一次无数据库下动态更新文案的解决历程
背景
一个简单官网 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 还是暴露在代码里面的,也会被开发人员直观看到
~~~最终还是避免不了泄漏的风险,话说回来,在程序世界有绝对的安全吗