昨天收到 QA 的生产报障,说是测试环境的附件上传功能报了 403 的错误,错误信息:403 Request Entity Too Lager。我尝试复现问题,发现传个几兆的文件都费劲啊,一传一个失败。不用说,项目用到 ng 代理,并且前端工程也没有做相应错误的提示,十有八九是 ng 配置文件的问题了。一看配置文件果然啥都没写,没记错的话,ng 缺省状态下只能请求体只能接受 1M 大小的数据量。
于是找到对应项目的代理配置,在 server 里面加上:
server {
listen my_port;
server_name localhost;
client_max_body_size 5M; # 上传文件大小限制
add_header X-Max-Upload-Size 5M; # 将最大值通过头信息传递
add_header Access-Control-Expose-Headers x-max-upload-size;
location /PartyBuilding_Api/login/
{
proxy_pass my_proxy;
}
# 禁用常用攻击网址,直接返回444
location ~* ^/(cgi|actuator|shell|hudson|druid|php) {
return 444;
}
}
1、寻找解决方案
完善 ng 配置之后,通常有以下几种做法:
方法一、通过后端接口读取配置
可以通过后端接口向前端提供 nginx 的client_max_body_size
配置值,步骤如下:
- 在后端提供一个 API:让后端读取 nginx 的配置并提供一个接口返回配置值。
-
- 可以在后端读取 nginx 配置文件的内容,找到
client_max_body_size
的值,然后返回给前端。 - 或者后端直接硬编码该值为接口输出(如果不会频繁更改)。
- 可以在后端读取 nginx 配置文件的内容,找到
例如,后端返回一个类似这样的 JSON 相应:
{
"maxUploadSize": 3145728 // 3 MB in bytes
}
- 前端获取配置值:在前端应用启动时,向该 API 发起请求,得到
maxUploadSize
的值。
-
- 然后在上传文件前,可以用获取到的
maxUploadSize
来判断文件大小并给出提示。
- 然后在上传文件前,可以用获取到的
let maxUploadSize = null;
// 获取最大上传大小
async function fetchMaxUploadSize() {
const response = await fetch('/api/get-upload-config'); // 调整为实际的 API 路径
const data = await response.json();
maxUploadSize = data.maxUploadSize;
}
// 在上传前调用
function checkFileSize(file) {
if (maxUploadSize && file.size > maxUploadSize) {
alert(`文件大小不能超过 ${(maxUploadSize / (1024 * 1024)).toFixed(2)} MB`);
return false;
}
return true;
}
// 应用初始化时调用
fetchMaxUploadSize();
方法二 、nginx 通过响应头传递配置
另一种方法是让Nginx在响应头中包含client_max_body_size
,这样前端可以直接读取这个值。
- 在Nginx配置中添加响应头:修改Nginx配置文件,设置一个自定义的响应头,将
client_max_body_size
值传递给前端。
server {
...
client_max_body_size 3M; # 最大3MB
add_header X-Max-Upload-Size 3M; # 将最大值通过头信息传递
...
}
- 前端读取响应头:前端在页面加载时或首次上传时,可以通过发起一个请求,读取响应头中的
X-Max-Upload-Size
,然后将其转换为数值进行校验。
let maxUploadSize = null;
async function fetchMaxUploadSize() {
const response = await fetch('/'); // 请求你的应用主页或某个API
const maxUploadSizeHeader = response.headers.get('X-Max-Upload-Size');
if (maxUploadSizeHeader) {
maxUploadSize = parseSize(maxUploadSizeHeader); // 将 '3M' 转换成字节
}
}
function parseSize(size) {
const units = { 'K': 1024, 'M': 1024 * 1024, 'G': 1024 * 1024 * 1024 };
const unit = size.slice(-1);
const number = parseFloat(size);
return number * (units[unit.toUpperCase()] || 1);
}
function checkFileSize(file) {
if (maxUploadSize && file.size > maxUploadSize) {
alert(`文件大小不能超过 ${(maxUploadSize / (1024 * 1024)).toFixed(2)} MB`);
return false;
}
return true;
}
fetchMaxUploadSize();
方法三、在构建时传递环境变量
如果前后端分离,且后端(或Nginx配置)中有client_max_body_size
的配置,可以在构建时将这个值作为环境变量注入前端应用。但这种方法需要在构建时设置,并且如果Nginx配置更改,可能需要重新构建前端应用。
在.env
文件种添加配置:
VUE_APP_MAX_UPLOAD_SIZE=3145728 # 3MB in bytes
然后在前端代码中访问:
const maxUploadSize = process.env.VUE_APP_MAX_UPLOAD_SIZE;
最推荐的方案是方法 1 或者方法 2,这样可以动态读取配置,而不需要在前端硬编码大小限制。
2、确定解决方案
因为在后端直接读取 Nginx 的client_max_body_size
配置值并不那么容易,因为这个配置值属于 Nginx,而不是后端程序的直接配置,不过,仍然可以采用一些变通的方式来实现类似的效果。
- 如果配置值并不会频繁变更,可以硬编码在配置文件中
- 通过环境变量传递配置,即环境变量存储 ng 的配置值,然后通过程序读取。
由于我的开发环境并不能很方便的操作到生产机器,且如果硬编码在配置文件中,每次更新 ng 配置值,都需要重启服务,这样成本太高,所以不采用此方式。
最终决定将 ng 的配置值暴露到响应头中,用户在登录系统可在响应头拿到该值,然后在前端代码全局存储。
以下代码是本系统封装好的请求的部分代码,在此获得配置值,并传递给 data
// 发送 POST 请求
service(requestConfig).then(response => {
// 获取ng自定义header的值
response.data.maxUploadSize = response.headers['x-max-upload-size']
resolve(response.data)
}).catch(error => {
// console.log('jungle:' + error)
// alert('超时了:' + error + typeof error)
reject(error)
})
在获取用户信息的函数中设置该值
import ...
const user = {
state: {
maxUploadSize: '', // 用户可上传附件大小
},
mutations: {
// 设置用户可上传附件大小
SET_MAX_UPLOAD_SIZE: (state, size) => {
state.maxUploadSize = size
},
},
actions: {
// 获取用户信息
GetUserInfo({ commit }) {
getUserInfo().then(res => {
if (res.type === 'success') {
// ...
commit('SET_MAX_UPLOAD_SIZE', res.maxUploadSize) // 设置用户可上传附件大小
} else {
// ...
}
})
},
}
}
export default user
在 getter.js 全局暴露:
const getters = {
maxUploadSize: state => state.user.maxUploadSize
}
export default getters
在 element-ui 组件 el-upload 上传的方法进行拦截判断
/**
* 文件上传
*/
beforeUpload(uploadItem) {
const maxUploadSize = this.$store.getters.maxUploadSize // ng配置可上传文件大小
if (this.maxUploadSize !== undefined && this.maxUploadSize !== null && this.maxUploadSize !== '') {
const maxUploadSize = this.unitToByte(this.maxUploadSize)
if (uploadItem.file.size > maxUploadSize) {
this.$message({ type: 'warning', message: '文件大小超过了' + maxUploadSize + '的限制' })
return
}
}
// ...业务代码
}