Skip to content

Classic Case

Authentication

Cookie 是在客户端(通常是浏览器)上存储的小型文本文件,主要用于在客户端和服务器之间存储和传递数据。Cookies 存储在客户端的文件系统中,通常存储在浏览器的 Cookie 存储中。

js
const express = require('express')
const cookieParser = require('cookie-parser')

const app = express()
const port = 3000

app.use(cookieParser())

// 设置Cookie
app.get('/set-cookie', (req, res) => {
  res.cookie('user', 'John Doe', { maxAge: 900000, httpOnly: true })
  res.send('Cookie已设置')
})

// 获取Cookie(注意:需要使用cookie-parser中间件来解析Cookie)
app.get('/get-cookie', (req, res) => {
  const user = req.cookies.user
  if (user) {
    res.send(`欢迎回来,${user}!`)
  } else {
    res.send('没有找到Cookie')
  }
})

// 删除Cookie
app.get('/delete-cookie', (req, res) => {
  res.clearCookie('user')
  res.send('Cookie已删除')
})

app.listen(port, () => {
  console.log(`应用程序正在监听端口 ${port}`)
})

Session

Session 主要用于跟踪用户的状态和上下文信息。Session 数据通常存储在服务器上,可以使用不同的存储后端,如内存、数据库或分布式缓存。

javascript
const express = require('express')
const mongoose = require('mongoose')
const session = require('express-session')
const MongoStore = require('connect-mongo')
const crypto = require('crypto')

const app = express()
const port = 3000

// 使用 express-session 中间件
app.use(
  session({
    secret: 'mysecretkey',
    resave: false,
    saveUninitialized: true,
    store: MongoStore.create({
      mongoUrl: 'mongodb://127.0.0.1:27017/session',
      collectionName: 'sessions',
      ttl: 1000 * 60,
      autoRemove: 'native', // session 有效期,过期会自动删除
      secret: 'your-secret-key'
    }),
    cookie: {
      maxAge: 1000 * 60,
      httpOnly: true,
      sameSite: true,
      secure: false
    }
  })
)

// 连接 MongoDB 数据库
mongoose.connect('mongodb://127.0.0.1:27017/session', {
  useNewUrlParser: true,
  useUnifiedTopology: true
})

// 创建数据库模式
const userSchema = new mongoose.Schema(
  {
    username: String,
    password: String
  },
  {
    versionKey: false,
    timestamps: true
  }
)

// 创建用户数据模型
const userModel = mongoose.model('user', userSchema)

// 注册路由
app.post('/register', express.json(), (req, res) => {
  const { username, password } = req.body

  userModel
    .findOne({ username })
    .then((data) => {
      if (data) {
        res.send('用户名已存在')
      } else {
        const salt = crypto.randomBytes(16).toString('hex')
        const hash = crypto
          .createHmac('sha256', salt)
          .update(password)
          .digest('hex')

        userModel
          .create({ username, password: `${hash}:${salt}` })
          .then(() => res.send('注册成功'))
          .catch(() => res.send('注册失败'))
      }
    })
    .catch(() => res.send('发生错误'))
})

// 登录路由
app.post('/login', express.json(), (req, res) => {
  const { username, password } = req.body

  // 检查用户是否存在
  userModel
    .findOne({ username })
    .then((data) => {
      // 分离密码哈希值和盐
      const [hash, salt] = data.password.split(':')

      // 验证密码
      const inputHash = crypto
        .createHmac('sha256', salt)
        .update(password)
        .digest('hex')

      // 检查密码是否匹配
      if (hash === inputHash) {
        req.session.username = data.username
        res.send('登录成功')
      } else {
        res.send('密码不正确')
      }
    })
    .catch(() => res.send('用户不存在'))
})

// 注销路由
app.post('/logout', (req, res) => {
  req.session.destroy((err) => {
    if (err) {
      res.send('退出失败')
    } else {
      res.clearCookie('connect.sid') // Session 默认 Cookie 名称
      res.send('退出成功')
    }
  })
})

// 删除用户
app.delete('/delete', (req, res) => {
  const { username } = req.query

  userModel
    .deleteOne({ username })
    .then(() => res.send('删除用户成功'))
    .catch(() => res.send('删除用户失败'))
})

// 验证 session 中间件
const isAuthMiddleware = (req, res, next) => {
  if (req.session.username) {
    res.send(`欢迎访问 ${req.session.username} 资料页面`)
    next()
  } else {
    res.send('请先登录')
  }
}

// 受保护的路由 - 仅在登录后才能访问
app.get('/profile', isAuthMiddleware)

app.listen(port, () => {
  console.log(`应用程序正在监听端口 ${port}`)
})

注意

由于 Session 数据存在在服务器上的数据库中,因此退出(注销)账户时需要调用 API 接口来删除数据库中的 Session 数据,或删除浏览器上的 cookie 即可。

Local Strategy

"Local Strategy" 是 Passport.js 中的一种身份验证策略,通常用于处理基于用户名和密码的本地身份验证。它是 Passport.js 提供的一种内置策略,用于验证用户的登录凭据。

js
const express = require('express')
const cookieParser = require('cookie-parser')
const passport = require('passport')
const LocalStrategy = require('passport-local').Strategy
const expressSession = require('express-session')

const app = express()

// 模拟数据库中的用户数据(实际中从数据库中获取)
const users = [{ _id: 1, username: 'user', password: 'password' }]

// 配置 Express 中间件
app.use(cookieParser())
app.use(express.urlencoded({ extended: true }))
app.use(
  expressSession({
    secret: 'your-secret-key',
    resave: false,
    saveUninitialized: false
  })
)
app.use(passport.initialize())
app.use(passport.session())

// 配置本地策略(用户名和密码)
passport.use(
  new LocalStrategy((username, password, done) => {
    // 在实际应用中,应该从数据库中查找用户并验证密码
    const user = users.find(
      (u) => u.username === username && u.password === password
    )

    if (user) {
      return done(null, user)
    } else {
      return done(null, false)
    }
  })
)

// 序列化用户
passport.serializeUser((user, done) => {
  done(null, user._id)
})

// 反序列化用户
passport.deserializeUser((id, done) => {
  // 根据用户ID从数据库中检索用户数据
  const user = users.find((u) => u._id === id)
  done(null, user)
})

// 设置路由
app.get('/', (req, res) => {
  res.send('Welcome to the homepage.')
})

// 登录路由,使用本地策略
app.post(
  '/login',
  passport.authenticate('local', {
    successRedirect: '/profile',
    failureRedirect: '/login'
  })
)

// 保护受保护路由,要求用户已登录
app.get('/profile', isAuthenticated, (req, res) => {
  res.send('Welcome to your profile page.')
})

// 登出路由
app.get('/logout', (req, res) => {
  res.clearCookie('connect.sid')
  res.redirect('/')
})

// 自定义身份验证中间件,检查用户是否已经登录
function isAuthenticated(req, res, next) {
  const user = req.cookies['connect.sid']
  if (user) {
    return next()
  }
  res.redirect('/login')
}

// 启动服务器
app.listen(3000, () => {
  console.log('Server is running on http://localhost:3000')
})

JWT

"Token"(令牌)是一个通用术语,用于代表身份、访问权利或授权信息的一种数据元素。令牌通常作为一种安全性措施,用于验证用户、应用程序或设备的身份,以便获得特定资源或执行特定操作。

"JWT"(JSON Web Token)是一种常见的 Token 格式,用于跨网络传输信息,通常包含有关用户身份和其他声明的信息,以及签名以确保其完整性。

javascript
const express = require('express')
const mongoose = require('mongoose')
const jwt = require('jsonwebtoken')
const crypto = require('crypto')

const app = express()

// 连接到MongoDB数据库
mongoose.connect('mongodb://127.0.0.1:27017/jsonwebtoken', {
  useNewUrlParser: true,
  useUnifiedTopology: true
})

// 定义数据库模式
const userSchema = new mongoose.Schema(
  {
    username: String,
    password: String
  },
  {
    versionKey: false,
    timestamps: true
  }
)

// 定义用户模型
const userModel = mongoose.model('user', userSchema)

// 用户注册
app.post('/register', express.json(), (req, res) => {
  const { username, password } = req.body

  userModel
    .findOne({ username })
    .then((data) => {
      if (data) {
        res.send('用户已存在')
      } else {
        const salt = crypto.randomBytes(16).toString('hex')
        const hash = crypto
          .createHmac('sha256', salt)
          .update(password)
          .digest('hex')

        userModel
          .create({ username, password: `${hash}:${salt}` })
          .then(() => res.send('注册成功'))
          .catch(() => res.send('注册失败'))
      }
    })
    .catch(() => res.send('注册发生错误'))
})

// 用户登录
app.post('/login', express.json(), (req, res) => {
  const { username, password } = req.body

  userModel
    .findOne({ username })
    .then((data) => {
      if (!data) {
        res.send('用户不存在')
      } else {
        // 从数据库中获取salt,再使用crypto验证密码
        const [hash, salt] = data.password.split(':')
        const hashVerify = crypto
          .createHmac('sha256', salt)
          .update(password)
          .digest('hex')

        if (hash === hashVerify) {
          // 创建JWT令牌并发送给客户端
          jwt.sign(
            { username: data.username },
            'secretKey',
            {
              expiresIn: 60
            },
            (err, token) => {
              if (err) {
                res.send('token生成出错')
              } else {
                // 将Authorization头部设置为令牌值
                res.header('Authorization', `Bearer ${token}`)

                // 暴露Authorization头部
                res.setHeader('Access-Control-Expose-Headers', 'Authorization')

                res.send('登录成功')
              }
            }
          )
        } else {
          res.send('密码错误')
        }
      }
    })
    .catch(() => res.send('登录发生错误'))
})

// 验证JWT中间件
const verifyTokenMiddleware = (req, res, next) => {
  const token = req.headers.authorization?.split(' ')[1]

  if (token) {
    jwt.verify(token, 'secretKey', (err, decoded) => {
      if (err) {
        res.send('token 过期')
      } else {
        req.user = decoded
        jwt.sign(
          { username: decoded.username },
          'secretKey',
          { expiresIn: 60 },
          (err, newToken) => {
            if (err) {
              res.send('newToken生成出错')
            } else {
              res.header('Authorization', `Bearer ${newToken}`)
              res.setHeader('Access-Control-Allow-Headers', 'Authorization')
              next()
            }
          }
        )
      }
    })
  } else {
    next()
  }
}

// 保护路由
app.get('/protected', verifyTokenMiddleware, (req, res) => {
  req.user ? res.send(`欢迎 ${req.user.username}`) : res.send('请登录')
})

// 删除用户
app.delete('/delete', (req, res) => {
  const { username } = req.query
  userModel
    .deleteOne({ username })
    .then(() => res.send('删除用户成功'))
    .catch(() => res.send('删除用户出错'))
})

// 启动服务器
const port = process.env.PORT || 3000
app.listen(port, () => console.log(`Server is running on port ${port}`))

注意

由于 Token 存储在浏览器 Storage 中,因此退出(注销)账户时直接删除浏览器存储上的 Token 即可,无需调用 API 接口。

JWT Strategy

"JWT Strategy" 是 Passport.js 中的一种身份验证策略,用于处理基于 JSON Web Token (JWT) 的身份验证。JWT 是一种用于安全传输信息的开放标准(RFC 7519),通常用于通过令牌验证用户的身份。

js
const express = require('express')
const passport = require('passport')
const JwtStrategy = require('passport-jwt').Strategy
const ExtractJwt = require('passport-jwt').ExtractJwt
const jwt = require('jsonwebtoken')
const app = express()

// 模拟数据库中的用户数据(实际中从数据库中获取)
const users = [{ id: 1, username: 'user', password: 'password' }]

// 配置 Express 中间件
app.use(express.urlencoded({ extended: true }))

// 配置JWT策略
const jwtOptions = {
  jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
  secretOrKey: 'your-secret-key' // 在实际应用中,应该存储在安全的地方
}

passport.use(
  new JwtStrategy(jwtOptions, (jwt_payload, done) => {
    // 在实际应用中,可以根据 jwt_payload 中的信息来查找用户
    const user = users.find((u) => u.id === jwt_payload.id)

    if (user) {
      return done(null, user)
    } else {
      return done(null, false)
    }
  })
)

// 设置路由
app.get('/', (req, res) => {
  res.send('Welcome to the homepage.')
})

// 登录路由,生成并返回JWT令牌
app.post('/login', (req, res) => {
  // 在实际应用中,应该进行用户名和密码的验证,并生成令牌
  const user = users.find(
    (u) => u.username === req.body.username && u.password === req.body.password
  )
  console.log('user', user)
  if (user) {
    const token = createToken(user)
    res.header('Authorization', `Bearer ${token}`)
    res.setHeader('Access-Control-Allow-Headers', 'Authorization')
    res.json({ token })
  } else {
    res.status(401).json({ message: 'Authentication failed' })
  }
})

// 受保护的路由,要求用户已提供有效的JWT令牌
app.get(
  '/profile',
  passport.authenticate('jwt', { session: false }),
  (req, res) => {
    res.send(`Welcome to your profile page, ${req.user.username}!`)
  }
)

// 创建JWT令牌
function createToken(user) {
  const payload = { id: user.id }
  return jwt.sign(payload, jwtOptions.secretOrKey, { expiresIn: 60 })
}

// 启动服务器
app.listen(3000, () => {
  console.log('Server is running on http://localhost:3000')
})

Authorization

OAuth 2.0 是一种用于授权第三方应用程序访问用户数据或资源的标准协议。在 Node.js 应用程序中实现 OAuth 2.0 身份验证策略通常涉及使用 Passport.js 中的 OAuth 2.0 身份验证策略之一,这些策略通常针对特定的身份提供者(如 Google、Facebook、GitHub 等)。

OAuth2.0 Strategy

js
const express = require('express')
const passport = require('passport')
const OAuth2Strategy = require('passport-oauth2').Strategy
const expressSession = require('express-session')
const app = express()

// 配置 Express 中间件
app.use(
  expressSession({
    secret: 'your-secret-key',
    resave: false,
    saveUninitialized: false
  })
)
app.use(passport.initialize())
app.use(passport.session())

// 配置OAuth2策略(GitHub示例)
const GitHubOAuth2Strategy = new OAuth2Strategy(
  {
    authorizationURL: 'https://github.com/login/oauth/authorize',
    tokenURL: 'https://github.com/login/oauth/access_token',
    clientID: 'your-github-client-id',
    clientSecret: 'your-github-client-secret',
    callbackURL: 'http://localhost:3000/auth/github/callback' // 回调URL需要与GitHub OAuth应用程序设置一致
  },
  (accessToken, refreshToken, profile, done) => {
    // 在这里,你可以处理OAuth 2.0成功后的逻辑,例如将用户数据存储在数据库中
    // 通常,profile参数中包含有关用户的信息
    return done(null, profile)
  }
)

// 注册GitHub OAuth2策略
passport.use(GitHubOAuth2Strategy)

// 序列化和反序列化用户
passport.serializeUser((user, done) => {
  // 在这里,你可以定义如何将用户数据序列化为会话数据
  done(null, user)
})

passport.deserializeUser((user, done) => {
  // 在这里,你可以定义如何从会话数据反序列化用户数据
  done(null, user)
})

// 设置路由
app.get('/', (req, res) => {
  res.send('Welcome to the homepage.')
})

// GitHub登录路由
app.get(
  '/auth/github',
  passport.authenticate('oauth2', { scope: ['user:email'] })
)

// GitHub登录回调路由
app.get(
  '/auth/github/callback',
  passport.authenticate('oauth2', { failureRedirect: '/' }),
  (req, res) => {
    // 成功登录后的重定向或其他逻辑
    res.redirect('/profile')
  }
)

// 保护受保护的路由,要求用户已登录
app.get('/profile', isAuthenticated, (req, res) => {
  res.send(`Welcome to your profile page, ${req.user.username}!`)
})

// 登出路由
app.get('/logout', (req, res) => {
  res.clearCookie('connect.sid')
  res.redirect('/')
})

// 自定义身份验证中间件,检查用户是否已经登录
function isAuthenticated(req, res, next) {
  const user = req.cookies['connect.sid']
  if (user) {
    return next()
  }
  res.redirect('/')
}

// 启动服务器
app.listen(3000, () => {
  console.log('Server is running on http://localhost:3000')
})

FormData

客户端

HTML 表单

html
<form action="/profile" method="post" enctype="multipart/form-data">
  <input type="file" name="avatar" />
  <input type="file" name="gallery" />
</form>

JavaScript

js
const form = document.getElementById('form')
const formData = new FormData(form)

fetch('/upload', {
  method: 'POST',
  body: formData,
  headers: {
    // 设置请求头,指定 Content-Type 为 multipart/form-data
    'Content-Type': 'multipart/form-data'
  }
})
  .then((response) => response.json())
  .then((data) => console.log(data))
  .catch((error) => console.error(error))

服务端

js
const express = require('express')
const multer = require('multer')

const app = express()

// 设置上传文件的存储路径,一般位于静态目录下
const upload = multer({ dest: 'uploads/' })

// 上传单个文件的表单
app.post('/profile', upload.single('avatar'), function (req, res, next) {
  // req.file 是 avatar 文件,req.body 将保存文本字段
})

// 上传多个文件的表单
app.post(
  '/photos/upload',
  upload.array('photos', 12),
  function (req, res, next) {
    // req.files 是 photos 文件的数组,req.body 将保存文本字段
  }
)

// 多个字段上传文件的表单
const cpUpload = upload.fields([
  { name: 'avatar', maxCount: 1 },
  { name: 'gallery', maxCount: 8 }
])
app.post('/cool-profile', cpUpload, function (req, res, next) {
  // req.files 是一个对象(字符串 -> 数组),其中 fieldname 是键,文件数组是值,req.body 将保存文本字段
  // req.files['avatar'][0] -> File
  // req.files['gallery'] -> Array
})

读书、摄影、画画、弹琴、编程