Nodejs后端上传文件到阿里云OSS

背景

最近涉及到上传文件到阿里云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
        }))
      }
    })
  })
}

你可能感兴趣的:(阿里云,node.js,vue.js,javascript,后端)