背景
有一个Web后台项目,之前的上传架构是用户前端上传->后端接口->后端调用oss.put把文件传到OSS中,非常合理安全的架构。但这样相当于后端服务器需要下载一次+上传一次,虽然OSS上传不收费,但如果用户上传了一个1G的文件,那么我的后端服务器会消耗2G流量,按照阿里云0.8元/G的流量价格,上传一个1G文件就是1.6元,这实在太贵了。
迫于没钱,我决定由前端直接连接OSS上传,不经过后端服务器,这样上传一个1G文件的成本是0
警告: 强烈不推荐非专业安全人员尝试这种操作!
警告: 强烈不推荐非专业安全人员尝试这种操作!
警告: 强烈不推荐非专业安全人员尝试这种操作!
实际方案
最标准的做法是: 后端新增一个需鉴权接口用于前端实时获取STS TOKEN,前端使用STS TOKEN直连OSS进行上传
我精简了一下,直接从后端接口中取出一个低权限的AKSK,再在前端初始化OSSClient。相比STS TOKEN方案,安全性有所下降但还能接受
这个方案是经过仔细考量的:
- 这是一个后台系统,用户不登录成功的话无法使用任何前端页面的操作
- 我的AKSK上传凭据是RAM用户,仅有OSS的管理权限
- 获取AKSK的接口需要鉴权
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| import { put, signatureUrl, getFileNameUUID } from '/@/utils/lib/oss' import { reportUploadOSS } from '/@/api/user/xxxxx';
async function uploadApiByItem(item: FileItem) { try { item.status = UploadResultStatus.UPLOADING; let obj_file_id = getFileNameUUID() let objName = obj_file_id + '_' + item.file.name await put(`${objName}`, item.file); const res2 = await signatureUrl(`${objName}`); await reportUploadOSS({ file_name: item.file.name, file_id: obj_file_id, file_url: res2, file_type: item.file.file_type });
item.status = UploadResultStatus.SUCCESS; item.responseData = res2; return { success: true, error: null }; } catch (e) { console.log(e); item.status = UploadResultStatus.ERROR; return { success: false, error: e, }; } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| import OSS from 'ali-oss' import { GetOSSAKSK } from '/@/api/user/xxxxxx'; let get_aksk_res = await GetOSSAKSK() let client = new OSS({ region: get_aksk_res.region, secure: true, accessKeyId: get_aksk_res.accessKeyId, accessKeySecret: get_aksk_res.accessKeySecret, bucket: get_aksk_res.bucket })
export const put = async (ObjName, fileUrl) => { try { let result = await client.put(`${ObjName}`, fileUrl) return result } catch (e) { console.log(e) } }
export const signatureUrl= async (ObjName) => { try { let result = await client.signatureUrl(`${ObjName}`,{expires: 999999999}) return result } catch (e) { console.log(e) } }
export const getFileNameUUID = () => { function rx() { return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1) } return `${+new Date()}_${rx()}${rx()}` }
|
1 2 3 4 5 6 7 8
| @user_bp.route('/api/user/get_oss_secret', methods=['GET']) @jwt_required() def get_oss_secret(): """获取AKSK信息""" return jsonify({'code': 200, 'msg': '', 'data': {"region": ALI_OSS_REGION, "accessKeyId": ALI_OSS_ACCESS_KEY, "accessKeySecret": ALI_OSS_SECRET_KEY, "bucket": ALI_OSS_BUCKET_NAME}})
|
其它错误方案
- 将AKSK直接写在oss.js中,省去后端新加接口的成本 -> 所有攻击者可以伪造登录请求的返回值,进而加载到oss.js
- 后端新接口返回的AKSK权限过大 -> 通过鉴权的攻击者可以直接接管服务器