Classic Case
Authentication
Cookie
Cookie 是在客户端(通常是浏览器)上存储的小型文本文件,主要用于在客户端和服务器之间存储和传递数据。Cookies 存储在客户端的文件系统中,通常存储在浏览器的 Cookie 存储中。
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 数据通常存储在服务器上,可以使用不同的存储后端,如内存、数据库或分布式缓存。
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 提供的一种内置策略,用于验证用户的登录凭据。
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 格式,用于跨网络传输信息,通常包含有关用户身份和其他声明的信息,以及签名以确保其完整性。
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),通常用于通过令牌验证用户的身份。
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
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 表单
<form action="/profile" method="post" enctype="multipart/form-data">
<input type="file" name="avatar" />
<input type="file" name="gallery" />
</form>
JavaScript
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))
服务端
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
})