Skip to content

進階 API

檔案上傳、登入驗證

登入

使用 JSON Web Token (JWT) 製作登入功能

JWT 介紹

近年瀏覽器對第三方 Cookie 驗證規定日趨嚴格,所以符合 RESTful 架構的 JWT 漸漸興起
JWT 是一組 Base64 字串,透過 . 分成三個部分

bash
# header,JWT 的加密演算法
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
# payload,JWT 包含的資料與簽發時間、到期時間
eyJfaWQiOiI2MDI3ZTE4NjEzODk2ZjIxYzhlYjU2NzYiLCJpYXQiOjE2MTMyMzAxODcsImV4cCI6MTYxMzgzNDk4N30
# signature/encryption data,用於驗證資料
rTnKU0l0w-P3xshZ43ZpWfzTSUuEXQoUQ7O3BYSYOsQ

注意

千萬不要將隱私資料存在 JWT,例如使用者的密碼,因為 JWT 的 payload 可以還原原始資料
由於 JWT 簽發後就無法撤銷,所以必須將簽發出去的 JWT 存入資料庫

安裝套件

npm install jsonwebtoken
npm install passport
npm install passport-jwt
npm install passport-local

設定 passport.js

建立一個 passport.js,使用驗證策略編寫自己的驗證方式

js
import passport from 'passport'
import passportJWT from 'passport-jwt'
import bcrypt from 'bcrypt'
import { Strategy as LocalStrategy } from 'passport-local'
import users from './models/users'

const JWTStrategy = passportJWT.Strategy
const ExtractJwt = passportJWT.ExtractJwt

// 新增一個名為 jwt 的驗證方式,使用 JWT 策略
passport.use('jwt', new JWTStrategy({
  // 從 headers 提取 Bearer Token
  jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
  // JWT 驗證 secret
  secretOrKey: process.env.JWT_SECRET
}, (jwtPayload, done) => {
  // jwtPayload 為 jwt 解碼後的資料
  // done 標記驗證完成,done(錯誤, user的資料, authenticate的info)
  users.findOne({ _id: jwtPayload._id }, (err, user) => {
    if (err) {
      return done(err, false)
    }
    if (user) {
      return done(null, user)
    } else {
      return done(null, false)
    }
  })
}))

// LocalStrategy 也可以自訂欄位名稱
// new LocalStrategy({
//   usernameField: 帳號資料欄位名稱,
//   passwordField: 密碼資料欄位名稱,
// }, (username, password, done) => {})
passport.use('login', new LocalStrategy(async (username, password, done) => {
  const user = await users.findOne({ username })
  if (!user) {
    return done(null, false, { message: 'User not found' })
  }
  if (!bcrypt.compareSync(password, user.password)) {
    return done(null, false, { message: 'Wrong password' })
  }
  return done(null, user, { message: 'Logged in successfully' })
}))

index.js 初始化 passport.js

js
import passport from 'passport'
import './passport.js'

app.use(passport.initialize())

就能在路由使用驗證方式

js
import passport from 'passport'
router.post('/login', passport.authenticate('login', { session: false }), login)

編寫 middleware

express 的 middleware 可以當作處理資料的層層關卡

由於 passport 自訂驗證錯誤訊息需要寫成 callback
為避免程式碼混亂所以編寫 middleware/auth.js 統一呼叫

js
import passport from 'passport'

export default (strategy) => {
  return (req, res, next) => {
    // 這裡的 user 和 info 是從 done 傳入的資料
    passport.authenticate(strategy, { session: false }, (err, user, info) => {
      if (err || !user) {
        return res.status(401).send({ success: false, message: info.message })
      }
      req.user = user
      next()
    })(req, res, next)
  }
}

最後改寫路由引用

js
// 引用 middleware
import auth from '../middleware/auth.js'

// 請求先進去登入驗證的 middleware 再繼續處理資料
router.post('/login', auth('login'), login)

登入

編寫使用者的 controller

js
export const login = async (req, res) => {
  if (!req.headers['content-type'] || !req.headers['content-type'].includes('application/json')) {
    res.status(400).send({ success: false, message: '資料格式不符' })
    return
  }

  const token = jwt.sign({ _id: req.user._id.toString() }, process.env.JWT_SECRET, { expiresIn: '7 days' })
  req.user.tokens.push(token)
  await req.user.save()
  res.status(200).send({ success: true, message: '', token })
}

登出

js
export const logout = async (req, res) => {
  try {
    // 將這次請求的 JWT 從使用者資料移除
    req.user.tokens = req.user.tokens.filter(token => token !== req.token)
    await req.user.save()
    res.status(200).send({ success: true, message: '' })
  } catch (error) {
    res.status(500).send({ success: false, message: '伺服器錯誤' })
  }
}

檔案上傳

將檔案上傳至免費空間 Cloudinary

安裝套件

js
npm install multer
npm install cloudinary
npm install multer-storage-cloudinary

編寫 middleware

將上傳檔案寫成 middleware
和登入一樣在需要上傳檔案的路由引用,就能接收檔案

js
import multer from 'multer'
import path from 'path'
import 'dotenv/config'
import { v2 as cloudinary} from 'cloudinary'
import { CloudinaryStorage } from 'multer-storage-cloudinary'

// cloudinary 設定
cloudinary.config({
  cloud_name: process.env.CLOUDINARY_NAME,
  api_key: process.env.CLOUDINARY_KEY,
  api_secret: process.env.CLOUDINARY_SECRET
})

// multer 套件設定
const upload = multer({
  // 指定上傳的檔案儲存到 cloudinary
  storage: new CloudinaryStorage({
    cloudinary
  }),
  // 過濾檔案類型
  fileFilter (req, file, callback) {
    if (!file.mimetype.includes('image')) {
      callback(new multer.MulterError('LIMIT_FORMAT'), false)
    } else {
      callback(null, true)
    }
  },
  // 限制檔案大小為 1MB
  limits: {
    fileSize: 1024 * 1024
  }
})

export default async (req, res, next) => {
  upload.single('image')(req, res, async error => {
    if (error instanceof multer.MulterError) {
      // 如果是 multer 的上傳錯誤
      let message = ''
      if (error.code === 'LIMIT_FILE_SIZE') {
        message = '檔案太大'
      } else if (error.code === 'LIMIT_FORMAT') {
        message = '格式不符'
      } else {
        message = '上傳錯誤'
      }
      res.status(400).send({ success: false, message })
    } else if (error) {
      // 其他錯誤
      res.status(500).send({ success: false, message: '伺服器錯誤' })
    } else {
      next()
    }
  })
}

購物網

以 Vue、Node.js 與 MongoDB 實作一個線上購物網站

  • 能註冊帳號、登入、登出
  • 有購物車功能
  • 有管理後台能上下架商品