最近涉及到上传文件到阿里云OSS上,试过【sts临时授权】前端直传,这样只用后端返回个临时sts账号即可,服务器带宽也不用担心占用太多,虽然有时限,但是这个【sts临时授权】总感觉不是太安全,被人拿到了,期间的上传的相关记录也可以不和后端关联,那我不是冤大头吗?看其他大佬的操作也看得不是怎么看的懂。
因为我当前的需求上传的量并不大,所以还是选择了安全一点的方法,完全后端上传。
1、前端传【formData】类型数据给后端;
2、后端使用【multer】保存临时文件,然后进行各项上传前的验证判断,是否允许上传;
3、上传;
相关代码全在这了,对应代码都有相应注释
1、前端上传代码
upload (file) {
let config = {
headers: {'Content-Type': 'multipart/form-data'}
}
let param = new FormData()// 创建form对象
param.append('file', file)
return axios.post('http://localhost:8888/upload', param, config)
}
2、OSS配置文件【oss.js】
const OSS = require('ali-oss')
/**
* 阿里云OSS配置config
* @type {{accessKeyId: string, bucket: string, accessKeySecret: string, region: string}}
*/
const ossconfig = {
// region填写Bucket所在地域。以华东1(杭州)为例,Region填写为oss-cn-hangzhou。
region: 'oss-cn-hangzhou',
// 阿里云账号RAM用户进行API访问或日常运维。
accessKeyId: '你自己的accessKeyId',
accessKeySecret: '你自己的accessKeySecret',
bucket: 'bucket名'
}
/**
* 初始化配置
* @type {Client}
*/
const client = new OSS(ossconfig)
/**
* 上传文件,这里是上传到OSS的 upload文件夹下
* @param filepath 文件上传到阿里云OSS中的路径,不带bucket
* @param file 文件本地路径
* @returns {Promise<*|string>}
*/
async function putOss (filepath, file) {
try {
// 填写OSS文件完整路径和本地文件的完整路径。OSS文件完整路径中不能包含Bucket名称。
// 如果本地文件的完整路径中未指定本地路径,则默认从示例程序所属项目对应本地路径中上传文件。
const result = await client.put(filepath, file)
if (result.res.statusCode === 200) {
return 'success'
} else {
return 'warning'
}
} catch (e) {
return e
}
}
/**
* 删除文件
* @param url 文件在阿里云OSS中的路径,不带bucket
* @returns {Promise}
*/
async function deleteObject (url) {
try {
await client.delete(url)
return true
} catch (error) {
if (error.code === 'NoSuchKey') {
return false
}
}
}
module.exports = {
ossconfig,
client,
putOss,
deleteObject
}
2、mysql操作相关方法【user.js】
/**
* 上传日志
* @param req 请求头
* @param url 【文件合法】上传到阿里云OSS的路径/【文件非法】本地文件名
* @param status 上传是否成功,【true】为成功,【false】反之
* @param msg 上传失败提示信息
*/
function uploadLog (req, url, status, msg) {
let time = moment(Date.now()).format('YYYY-MM-DD HH:mm:ss')
// 获取id
let token = getCookie('token', req.headers.cookie)
let result = jwt.verifyToken(token)
let id = result.data.user
// 获取文件参数
let type = req.file.mimetype !== undefined ? req.file.mimetype : ''
let size = req.file.size !== undefined ? file_size_deal(req.file.size) : ''
let conn = mysql.createConnection(dbconfig)
conn.query('INSERT INTO upload_log (id, type, size, url, created_time, status, msg, token) VALUES (?,?,?,?,?,?,?,?)', [id, type, size, url, time, status, msg, token], (err, results, fields) => {
if (err) {
console.log('上传日志录入报错:')
console.log(err)
}
})
conn.end()
}
/**
* 文件大小尺寸处理
* @param sizes 文件大小
* @returns {string}
*/
// eslint-disable-next-line camelcase
function file_size_deal (sizes) {
let size = sizes
size = (size / 1024 / 1024).toFixed(2)
if (size < 1) {
size = (size * 1024).toFixed(2) + 'kb'
} else if (size >= 1 & size < 1024) {
size = size + 'MB'
} else if (size >= 1024) {
size = (size / 1024).toFixed(2) + 'GB'
}
return size
}
/**
* 上传阿里云OSS前守卫,防止恶意多次上传
* @param req
* @param callback
*/
function uploadGuard (req, callback) {
// 获取限制时间段,这里是最近3小时
let opentime = moment(Date.now()).subtract(1, 'hours').format('YYYY-MM-DD HH:mm:ss')
// 获取id
let token = getCookie('token', req.headers.cookie)
let result = jwt.verifyToken(token)
let id = result.data.user
let conn = mysql.createConnection(dbconfig)
conn.query('SELECT role FROM tb_login WHERE id = ?;' +
'SELECT * FROM upload_log WHERE id = ? AND created_time > ?', [id, id, opentime], (err, results, fields) => {
if (err) {
console.log(err)
callback(JSON.stringify({
code: 'error',
data: err
}))
} else {
callback(JSON.stringify({
code: 'success',
data: results
}))
}
})
conn.end()
}
3、后端接口【核心】
const fs = require('fs')
const multer = require('multer')
const user = require('./user') // 涉及mysql方法
const uploadLength = 30 // 1小时内上传限制次数
/**
* 后端upload上传文件阿里云OSS
*/
router.post('/upload', uploadFile, (req, res) => {
let file = req.file
// 获取文件后缀名,如'.jpg'
let index = file.originalname.lastIndexOf('.')
let type = file.originalname.substr(index)
// 获取当前年月,作为区分子目录
let month = moment(Date.now()).format('YYYY-MM')
// 随机生成16进制的28位数的文件名,用于上传oss时显示
let ossName = Math.random().toString(16).slice(2) + Math.random().toString(16).slice(2)
ossName = `${ossName}${type}`
// 拼接新文件存储路径(文件目录+随机数+文件后缀名),用于上传oss时显示
let osspath = 'upload/' + month + '/' + ossName
// 临时文件在服务器中的本地路径
let filePath = './' + req.file.path
// 重写文件,生成图片,用于上传
fs.rename(filePath, ossName, (err) => {
if (err) {
user.uploadLog(req, osspath, 'false', '文件重新写入失败')
res.end(JSON.stringify({
code: 301,
msg: '文件重新写入失败',
data: err
}))
} else {
// 重写文件在服务器中的本地路径
let localFile = './' + ossName
// 上传图片
OSS.putOss(osspath, localFile)
.then(data => {
fs.unlinkSync(localFile)
if (data === 'success') {
user.uploadLog(req, osspath, 'true', '')
res.end(JSON.stringify({
code: 200,
msg: '文件上传成功',
data: osspath
}))
} else if (data === 'warning') {
user.uploadLog(req, osspath, 'false', '文件上传失败')
res.end(JSON.stringify({
code: 303,
msg: '文件上传失败',
data: data
}))
} else {
user.uploadLog(req, osspath, 'false', '文件上传意外错误304')
res.end(JSON.stringify({
code: 304,
msg: '文件上传意外错误',
data: data
}))
}
})
.catch(error => {
console.log(error)
fs.unlinkSync(localFile)
user.uploadLog(req, osspath, 'false', '文件上传意外错误305')
res.end(JSON.stringify({
code: 305,
msg: '文件上传意外错误',
data: error
}))
})
}
})
})
/**
* 获取上传文件,并作以判断是否合法
* @param req
* @param res
* @param next
*/
function uploadFile (req, res, next) {
// dest 值为文件存储的路径;single方法,表示上传单个文件,参数为表单数据对应的key
let upload = multer({dest: 'uploads/'}).single('file')
upload(req, res, (err) => {
// 获取文件相应数据
let file = req.file
const isJPG = file.mimetype === 'image/jpeg' | file.mimetype === 'image/png'
const isLt2M = file.size / 1024 / 1024 < 2
const filepath = './uploads/' + file.filename
const fileName = file.originalname !== undefined ? file.originalname : ''
// 上传前守卫
user.uploadGuard(req, function (data) {
data = JSON.parse(data)
// 判断是否获取到上传记录
if (data.code === 'success') {
let role = data.data[0][0].role !== undefined ? data.data[0][0].role : 'user'
let length = data.data[1].length !== undefined ? data.data[1].length : 0
console.log(length)
console.log(uploadLength)
// 判断是否恶意多次上传
if (role !== 'admin' & length >= uploadLength) {
fs.unlinkSync(filepath)
user.uploadLog(req, fileName, 'false', '可疑上传访问,已拦截')
res.end(JSON.stringify({
code: 401,
msg: '暂时无法使用上传功能',
data: '您近期上传文件次数过于频繁,请稍候重试'
}))
} else {
// 判断文件处理是否异常
if (err) {
// 删除临时文件
fs.unlinkSync(filepath)
user.uploadLog(req, fileName, 'false', '文件类型或大小异常错误,请按提示选择文件')
res.end(JSON.stringify({
code: 301,
msg: '文件类型或大小异常错误,请按提示选择文件',
data: err
}))
} else if (!isJPG | !isLt2M) {
// 删除临时文件
fs.unlinkSync(filepath)
user.uploadLog(req, fileName, 'false', '文件类型或大小超过限制,请按提示选择文件')
res.end(JSON.stringify({
code: 302,
msg: '文件类型或大小超过限制,请按提示选择文件',
data: 'isJPG:' + isJPG + ';isLt2M:' + isLt2M
}))
} else {
// 执行上传操作
next()
}
}
} else {
fs.unlinkSync(filepath)
user.uploadLog(req, fileName, 'false', '获取上传历史记录及身份意外错误')
res.end(JSON.stringify({
code: 400,
msg: '获取上传历史记录及身份意外错误',
data: data
}))
}
})
})
}