6 React项目架构 - web server网络请求转发
Mon, Sep 24, 2018
开发环境工程架构示意图

accessToken
问题:
- 部分接口需要带accessToken才有权限请求
- accesstoken是用户登录后,cnodejs服务器返回的
- accesstoken不能存在浏览器里,有安全风险
解决方案:
- 获取accessToken后,存在web服务器里,通过session机制,在web服务端检测有木有token,没就给用户弹登录,有就转发请求
实现
安装工具
$ npm i body-parser express-session query-string -S |
说明:
- body-parser是用来转化request的body的,转化成json格式的数据
- espress-sisson是express插件,用来存放服务端session
- query-string是用来把url「?」后面的内容转化为一个json的
配置web服务器
server.js
...
const bodyParser = require('body-parser')
app.use(bodyParser.json()) // #1-1
app.use(bodyParser.urlencoded({ extended: false })) // #1-2
const session = require('express-session')
app.use(session({ // #2
maxAge: 10 * 60 * 1000, // #2-1
name: 'sid', // #2-2
resave: false, // #2-3
saveUninitialized: false,
secret: '(‘◇’)?' // #2-4
}))
... |
说明:
- body-parser用于解析request的body中的参数
- 把application json的数据,转化成req.body的数据
- 对应formdate的数据 - 把这些数据也转换到req.body中,之后通过req.body就能拿到请求里的参数了
- 配置服务器session相关功能的
- 设置session的过期时长
- 10分钟
- 做测试用的,所以不需要太长
- 线上的session是要存在数据库中的,如redis
- 现在是存在内存中,服务关掉了,相关数据就没了,所有人都需要重新登录一遍
- 指定sessionID对应的cookieID,浏览器端可以通过cookieID拿到sessionID的值
- 是否每次请求都申请一个cookieID
- secret是一个密钥串,用于对session加密,保证cookie在浏览器端无法解密
- 设置session的过期时长
验证accessToken
接口:
post /accesstoken 验证 accessToken 的正确性
接收 post 参数
- accesstoken
String用户的 accessToken如果成功匹配上用户,返回成功信息。否则 403。
返回值示例
{success: true, loginname: req.user.loginname, id: req.user.id, avatar_url: req.user.avatar_url}
逻辑:
- 客户端调用web服务器的/login接口,发post请求,传accessToken到web服务里来
- web服务器把请求转发给cnode服务器
- 如果验证通过(accessToken有效):
- 构建一个user对象作为value,赋给sessionID为key的map里面 - 信息存在web服务器的内存中(业务逻辑中不用处理,express的session模块已经做好相关逻辑了)
- 把服务端消息返回给客户端
server/util/handler_login.js
const router = require('express').Router()
const axios = require('axios')
const baseUrl = 'https://cnodejs.org/api/v1'
router.post('/login', function(req, res, next) {
axios.post(`${baseUrl}/accesstoken`, {
accesstoken: req.body.accessToken
})
.then((resp) => {
if (resp.status === 200 && resp.data.success) {
req.session.user = {
accessToken: req.body.accessToken,
loginName: resp.data.loginname,
id: resp.data.id,
avatarUrl: resp.data.avatar_url,
}
res.json({
success: true,
data: resp.data
})
}
})
.catch((err) => {
if (err.response) { // #1
res.json({
success: false,
data: err.response.data // #3
})
} else { // #2
next(err)
}
})
})
module.exports = router |
说明:
- 如果cnode服务端返回了报错信息,表明是业务逻辑报错,直接返回err.response给用户
- 如果cnode没报错(发生了非业务逻辑错误),则把错误交给「全局错误处理器」处理
- 如果直接写err.response的话,res.json会把err.response这串东东整个转成字符串,但是response对象太大了,无法转成字符串,会导致响应一致无法返回客户端
- 报错信息:(node:9519) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 2): TypeError: Converting circular structure to JSON
其他接口的代理转发
server/util/proxy.js
const axios = require('axios')
const querystring = require('query-string') // #7
const baseUrl = 'https://cnodejs.org/api/v1'
module.exports = function(req, res, next) {
const path = req.path
const user = req.session.user || { } // #2 防止undfine产生报错
const needAccessToken = req.query.needAccessToken // #1
if (needAccessToken && !user.accessToken) {
res.status(401).send({
success: false,
msg: 'need login'
})
}
const query = Object.assign({}, req.query, {
accesstoken: (needAccessToken && req.method === 'GET') ? user.accessToken : '' // #8
})
if (query.needAccessToken) delete query.needAccessToken // #3
axios(`${baseUrl}${path}`, {
method: req.method,
params: query,
data: querystring.stringify(Object.assign({}, req.body, {
(needAccessToken && req.method === 'POST') ? user.accessToken : '' // #4 所有POST请求都加accessToken
})),
headers: {
'Content-Type': 'application/x-www-form-urlencoded' // #5 指定使用form-data格式发送请求-由于服务端设定导致的
}
}).then((resp) => {
if (resp.status === 200) {
res.send(resp.data)
} else {
res.status(resp.status).send(resp.data) // #6 链式调用,给client返回错误信息
}
}).catch((err) => {
if (err.response) {
res.status(500).send(err.response.data)
} else {
res.status(500).send({
success: false,
msg: '未知错误'
})
}
})
} |
说明:
通过客户端传过来的url中,是否有needAccessToken=true判断是否需要给服务端传accessToken
下面需要通过user.xxx调用相关方法,需要防止user为undefine时导致报错,故在此处做初始化
干掉web服务器和client之间的私自约定的query参数
- 所有发向cnode的POST请求,在body里拼上accessToken,即使cnode接口不需要,加上也OK
- 虽然表面上看上去,是从session.user中拿accessToken的,但是其实,user信息是存在web服务器中的,session.user这步操作,会通过sessionID,在web服务器中找到对应的user,然后拿accessToken
- cnode的api有些无法接收json,所以需要指定下Content-Type,用form-data的格式去发请求
链式调用,给client返回错误信息和状态码
如果不用querystring.stringify转码,会报500错误 - accesstoken不正确
插件:`npm i query-string -S`
如果不使用格式转换工具的话,格式会类似:{"accesstoken": "xxx"}
使用后,格式类似我们在浏览器端使用「表单」请求数据的格式一样
- GET方法,把accessToken拼在query后面...Object.assign语法蛮好玩的...
server.js调用相关转发方法
...
app.use('/api/user', require('./util/handler-login'))
app.use('/api', require('./util/proxy'))
if (!isDev) {
... |
说明:
- 转发相关方法,需要在服务起来之前调用
测试
普通接口:http://localhost:2333/api/topics
accessToken接口:
实现测试页面 client/views/test/api.test.js
import React from 'react'
import axios from 'axios'
/* eslint-disable */ // #1
export default class TestApi extends React.Component {
getTopics() {
axios.get('/api/topics')
.then((resp) => {
console.log(resp)
}).catch((err) => {
console.log(err)
})
}
login() {
axios.post('/api/user/login', {
accessToken: 'xxxxxx-xxx-xxxx-xx'
}).then(resp => {
console.log(resp)
})
}
markAll() {
axios.post('/api/message/mark_all?needAccessToken=true')
.then(resp => {
console.log(resp)
})
}
render() {
return (
<div>
<button onClick={this.getTopics}>topics</button>
<button onClick={this.login}>login</button>
<button onClick={this.markAll}>markAll</button>
</div>
)
}
}
/* eslint-enable */
|
说明:
- 禁止eslint检测区块
router.jsx添加测试页面入口
...
import TestApi from '../views/test/api.test'
export default () => [
...
<Route path='/test' component={TestApi} key="test" />,
] |
注意:
- dev:web:server还不支持router和mobx,通过dev:web:server访问index.html的话,访问不到东西...
- 依然需要通过dev:client访问我们的测试页面 http://localhost:8787/test
- 又因为,默认情况下,通过dev:client走的请求,默认都是走8787端口的,点击「login」按钮,会发的请求是类似:「https://localhost:8787/api/user/login」
- 这样的请求dev:client下的server无法处理
- 需要让我们的web:server处理才行,即:「https://localhost:2333/api/user/login」
- 需要配置webpack的dev服务器的时候,指定下/api相关的请求,转发给2333端口
webpack.config.client.js
if (isDev) {
...
config.devServer = {
host: '0.0.0.0',
port: '8787',
...
proxy: {
'/api': 'http://localhost:2333'
}
};
...
} |
测试方式:
- 起服务:dev:client dev:web:start
- 访问http://localhost:8787/test
- 点击login
- 观察Network