📖 文章概述
安全是所有应用的基础。本文讲解如何防护常见漏洞、实现认证授权、加密敏感数据,以及构建安全的应用架构。
🎯 安全威胁模型
OWASP Top 10
1. 注入漏洞 (Injection)
- SQL 注入、命令注入、LDAP 注入
2. 失效的身份验证 (Broken Authentication)
- 弱密码、密钥泄露、会话管理不当
3. 敏感数据暴露 (Sensitive Data Exposure)
- 未加密传输、弱加密、敏感信息日志记录
4. 外部实体注入 (XML External Entities, XXE)
- XML 外部实体引用、XML 炸弹
5. 失效的访问控制 (Broken Access Control)
- 权限提升、水平越权、垂直越权
6. 安全配置缺陷 (Security Misconfiguration)
- 默认密码、不必要的服务、过度权限
7. 跨站脚本 (XSS)
- 反射型、存储型、DOM 型 XSS
8. 不安全的反序列化 (Insecure Deserialization)
- 恶意对象注入、远程代码执行
9. 使用含有已知漏洞的组件 (Using Components with Known Vulnerabilities)
- 过期依赖、未打补丁库
10. 日志记录和监控不足 (Insufficient Logging & Monitoring)
- 无法检测攻击、事件记录不完整
🛡️ 防护常见漏洞
1. XSS(跨站脚本)防护
// ❌ 不安全 - 直接输出用户输入
app.get('/vulnerable', (req, res) => {
const search = req.query.q
res.send(`<p>搜索结果: ${search}</p>`) // XSS 风险
})
// ✅ 安全 - HTML 转义
const escapeHtml = (unsafe) => {
return unsafe
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''')
}
app.get('/safe', (req, res) => {
const search = req.query.q
const escaped = escapeHtml(search)
res.send(`<p>搜索结果: ${escaped}</p>`)
})
// 使用专门库
import DOMPurify from 'isomorphic-dompurify'
const cleanHtml = DOMPurify.sanitize(userInput, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'a'],
ALLOWED_ATTR: ['href']
})
// Vue.js 自动转义
// <template>
// <!-- 自动转义 -->
// <p>{{ userInput }}</p>
//
// <!-- 需要 HTML 时使用 v-html(谨慎)-->
// <p v-html="sanitizedHTML"></p>
// </template>
// React 自动转义
// <p>{userInput}</p> // 安全
// <p dangerouslySetInnerHTML={{ __html: userInput }} /> // 需谨慎
2. CSRF(跨站请求伪造)防护
import csrf from 'csurf'
import cookieParser from 'cookie-parser'
import session from 'express-session'
// 配置 CSRF 防护
app.use(cookieParser())
app.use(session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
secure: true, // 仅 HTTPS
httpOnly: true, // 无法通过 JS 访问
sameSite: 'strict' // 仅同站请求
}
}))
// CSRF 令牌中间件
const csrfProtection = csrf({ cookie: false })
// 为表单添加 CSRF 令牌
app.get('/form', csrfProtection, (req, res) => {
res.render('form', { csrfToken: req.csrfToken() })
})
// 验证 CSRF 令牌
app.post('/submit', csrfProtection, (req, res) => {
// CSRF 令牌已验证
res.json({ message: '表单提交成功' })
})
// 前端提交表单
// <form method="POST" action="/submit">
// <input type="hidden" name="_csrf" value="<%= csrfToken %>">
// <input type="text" name="username">
// <button type="submit">提交</button>
// </form>
3. SQL 注入防护
import pg from 'pg'
const pool = new pg.Pool()
// ❌ 不安全 - 字符串拼接
// const query = `SELECT * FROM users WHERE id = ${userId}`
// 攻击: userId = "1 OR 1=1"
// ✅ 安全 - 参数化查询
app.get('/users/:id', async (req, res) => {
const { id } = req.params
try {
// 使用参数化查询
const result = await pool.query(
'SELECT * FROM users WHERE id = $1',
[id]
)
res.json(result.rows)
} catch (error) {
res.status(500).json({ error: error.message })
}
})
// ORM 防护(Prisma)
import { PrismaClient } from '@prisma/client'
const prisma = new PrismaClient()
app.get('/users/:id', async (req, res) => {
const user = await prisma.user.findUnique({
where: { id: parseInt(req.params.id) }
})
res.json(user)
})
// 输入验证
const validateUserId = (id) => {
if (!Number.isInteger(parseInt(id))) {
throw new Error('无效的用户 ID')
}
if (parseInt(id) < 0 || parseInt(id) > 999999) {
throw new Error('用户 ID 超出范围')
}
return parseInt(id)
}
app.get('/users/:id', async (req, res) => {
try {
const id = validateUserId(req.params.id)
const user = await prisma.user.findUnique({
where: { id }
})
res.json(user)
} catch (error) {
res.status(400).json({ error: error.message })
}
})
4. 命令注入防护
import { execFile } from 'child_process'
// ❌ 不安全 - 使用 exec(不建议)
// exec(`ls ${userDir}`) // 攻击: userDir = "; rm -rf /"
// ✅ 安全 - 使用 execFile(参数分离)
const safeExecute = (command, args) => {
return new Promise((resolve, reject) => {
execFile(command, args, (error, stdout, stderr) => {
if (error) reject(error)
resolve(stdout)
})
})
}
// 使用
try {
const result = await safeExecute('ls', [userDir])
res.json({ files: result })
} catch (error) {
res.status(500).json({ error: error.message })
}
// 避免执行 shell 命令
// 改用专门的库或 API
import fs from 'fs'
// 不执行命令,直接使用 Node.js API
const files = fs.readdirSync(userDir)
🔐 认证和授权
5. JWT 认证实现
import jwt from 'jsonwebtoken'
const JWT_SECRET = process.env.JWT_SECRET
const JWT_EXPIRY = '24h'
// 生成 token
function generateToken(userId, email) {
return jwt.sign(
{
userId,
email,
iat: Math.floor(Date.now() / 1000)
},
JWT_SECRET,
{ expiresIn: JWT_EXPIRY }
)
}
// 验证 token
function verifyToken(token) {
try {
return jwt.verify(token, JWT_SECRET)
} catch (error) {
if (error.name === 'TokenExpiredError') {
throw new Error('Token 已过期')
}
throw new Error('无效的 Token')
}
}
// 刷新 token(使用 Refresh Token)
function generateRefreshToken(userId) {
return jwt.sign(
{ userId, type: 'refresh' },
process.env.REFRESH_TOKEN_SECRET,
{ expiresIn: '7d' }
)
}
// 认证中间件
const authMiddleware = (req, res, next) => {
const authHeader = req.headers.authorization
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: '缺少认证令牌' })
}
const token = authHeader.slice(7)
try {
req.user = verifyToken(token)
next()
} catch (error) {
res.status(401).json({ error: error.message })
}
}
// 使用
app.post('/login', async (req, res) => {
const { email, password } = req.body
// 验证密码
const user = await User.findOne({ email })
if (!user || !await user.comparePassword(password)) {
return res.status(401).json({ error: '用户名或密码错误' })
}
const token = generateToken(user.id, user.email)
const refreshToken = generateRefreshToken(user.id)
res.json({ token, refreshToken })
})
app.get('/profile', authMiddleware, (req, res) => {
res.json({ userId: req.user.userId })
})
6. 基于角色的访问控制(RBAC)
// 定义角色和权限
const permissions = {
'admin': ['read', 'write', 'delete', 'manage_users'],
'editor': ['read', 'write'],
'viewer': ['read']
}
// 权限检查中间件
const requirePermission = (requiredPermission) => {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({ error: '未认证' })
}
const userRole = req.user.role
const userPermissions = permissions[userRole]
if (!userPermissions || !userPermissions.includes(requiredPermission)) {
return res.status(403).json({ error: '权限不足' })
}
next()
}
}
// 使用
app.delete('/users/:id',
authMiddleware,
requirePermission('delete'),
async (req, res) => {
await User.findByIdAndDelete(req.params.id)
res.json({ message: '用户已删除' })
}
)
// 基于属性的访问控制(ABAC)
const checkAccess = (resource, action, user) => {
// 所有者总是可以编辑自己的资源
if (resource.owner === user.id && ['read', 'write'].includes(action)) {
return true
}
// 管理员可以执行任何操作
if (user.role === 'admin') {
return true
}
// 其他情况拒绝
return false
}
app.put('/posts/:id', authMiddleware, async (req, res) => {
const post = await Post.findById(req.params.id)
if (!checkAccess(post, 'write', req.user)) {
return res.status(403).json({ error: '权限不足' })
}
await post.updateOne(req.body)
res.json(post)
})
🔑 加密和密钥管理
7. 数据加密
import crypto from 'crypto'
// AES 加密(对称加密)
const ENCRYPTION_KEY = Buffer.from(process.env.ENCRYPTION_KEY, 'hex')
const IV_LENGTH = 16
function encrypt(text) {
const iv = crypto.randomBytes(IV_LENGTH)
const cipher = crypto.createCipheriv(
'aes-256-cbc',
ENCRYPTION_KEY,
iv
)
let encrypted = cipher.update(text, 'utf-8', 'hex')
encrypted += cipher.final('hex')
// 返回 IV + 密文
return iv.toString('hex') + ':' + encrypted
}
function decrypt(text) {
const parts = text.split(':')
const iv = Buffer.from(parts[0], 'hex')
const decipher = crypto.createDecipheriv(
'aes-256-cbc',
ENCRYPTION_KEY,
iv
)
let decrypted = decipher.update(parts[1], 'hex', 'utf-8')
decrypted += decipher.final('utf-8')
return decrypted
}
// 密码哈希(单向)
import bcrypt from 'bcrypt'
async function hashPassword(password) {
const salt = await bcrypt.genSalt(10) // 成本因子
return bcrypt.hash(password, salt)
}
async function comparePassword(password, hash) {
return bcrypt.compare(password, hash)
}
// 使用
const hashedPassword = await hashPassword(userPassword)
await User.create({ email, password: hashedPassword })
// 验证
const isValid = await comparePassword(inputPassword, user.password)
8. 安全的密钥管理
// ✅ 推荐 - 使用环境变量和密钥管理服务
class KeyManager {
constructor() {
// 从环境变量加载主密钥
this.masterKey = process.env.MASTER_KEY
// 使用 AWS Secrets Manager / HashiCorp Vault
this.secretsClient = initializeSecretsManager()
}
async getKey(keyName) {
// 缓存在内存中(不推荐敏感密钥)
if (this.keyCache && this.keyCache[keyName]) {
return this.keyCache[keyName]
}
// 从密钥管理服务获取
const key = await this.secretsClient.getSecret(keyName)
if (!this.keyCache) this.keyCache = {}
this.keyCache[keyName] = key
return key
}
async rotateKey(keyName) {
// 生成新密钥
const newKey = crypto.randomBytes(32).toString('hex')
// 保存新密钥
await this.secretsClient.putSecret(keyName, newKey)
// 更新缓存
if (this.keyCache) delete this.keyCache[keyName]
console.log(`密钥 ${keyName} 已轮换`)
}
}
// ❌ 不安全 - 密钥在代码中硬编码
// const SECRET_KEY = 'my-secret-key'
// ✅ 安全 - 使用环境变量
const SECRET_KEY = process.env.SECRET_KEY
if (!SECRET_KEY) {
throw new Error('SECRET_KEY 环境变量未设置')
}
🔒 安全的传输和存储
9. HTTPS 和安全头
import helmet from 'helmet'
import https from 'https'
import fs from 'fs'
// 使用 Helmet 设置安全头
app.use(helmet())
// 详细配置
app.use(helmet.contentSecurityPolicy({
directives: {
defaultSrc: ["'self'"],
scriptSrc: ["'self'", "'unsafe-inline'"], // 仅在开发环境
styleSrc: ["'self'", "'unsafe-inline'"],
imgSrc: ["'self'", 'data:', 'https:'],
upgradeInsecureRequests: []
}
}))
// 其他安全头
app.use(helmet.hsts({
maxAge: 31536000, // 1 年
includeSubDomains: true,
preload: true
}))
app.use(helmet.noSniff()) // 禁止 MIME 嗅探
app.use(helmet.xssFilter()) // 启用 XSS 过滤
app.use(helmet.referrerPolicy({ // 控制 Referer 头
policy: 'strict-origin-when-cross-origin'
}))
// HTTPS 配置
const options = {
key: fs.readFileSync('server.key'),
cert: fs.readFileSync('server.cert')
}
https.createServer(options, app).listen(443)
// 强制 HTTP 重定向到 HTTPS
app.use((req, res, next) => {
if (req.header('x-forwarded-proto') !== 'https') {
res.redirect(301, `https://${req.header('host')}${req.url}`)
} else {
next()
}
})
10. 安全的 Cookie 配置
import cookieParser from 'cookie-parser'
app.use(cookieParser())
// 设置安全 Cookie
app.use((req, res, next) => {
res.cookie('sessionId', generateSessionId(), {
secure: true, // 仅 HTTPS 传输
httpOnly: true, // 禁止 JavaScript 访问
sameSite: 'strict', // 防止 CSRF
maxAge: 3600000, // 1 小时
path: '/', // 仅在根路径
domain: 'example.com' // 指定域名
})
next()
})
// ❌ 不安全
// res.cookie('sessionId', sessionId)
// ✅ 安全
res.cookie('sessionId', sessionId, {
secure: true,
httpOnly: true,
sameSite: 'strict'
})
🔍 安全审计和监控
11. 安全日志记录
class SecurityAuditLog {
async logSecurityEvent(event) {
const auditEntry = {
timestamp: new Date(),
eventType: event.type, // login, logout, access_denied, etc.
userId: event.userId,
ipAddress: event.ipAddress,
userAgent: event.userAgent,
resource: event.resource,
action: event.action,
result: event.result, // success, failure
details: event.details
}
// 保存到数据库
await AuditLog.create(auditEntry)
// 在受监控的 SIEM 系统中记录敏感事件
if (['failed_login', 'privilege_escalation', 'access_denied'].includes(event.type)) {
await this.alertSecurityTeam(auditEntry)
}
}
async alertSecurityTeam(event) {
// 发送告警
await sendAlert({
channel: 'security',
severity: 'high',
message: `安全事件: ${event.eventType} - ${event.userId}`
})
}
}
// 使用
const auditLog = new SecurityAuditLog()
app.post('/login', async (req, res) => {
const { email, password } = req.body
try {
const user = await User.findOne({ email })
const isValid = user && await user.comparePassword(password)
if (isValid) {
await auditLog.logSecurityEvent({
type: 'login',
userId: user.id,
ipAddress: req.ip,
userAgent: req.get('user-agent'),
result: 'success'
})
res.json({ token: generateToken(user.id) })
} else {
await auditLog.logSecurityEvent({
type: 'failed_login',
ipAddress: req.ip,
userAgent: req.get('user-agent'),
result: 'failure',
details: { email }
})
res.status(401).json({ error: '用户名或密码错误' })
}
} catch (error) {
res.status(500).json({ error: '内部错误' })
}
})
12. 依赖漏洞扫描
# npm audit - 检查漏洞
npm audit
# 修复漏洞
npm audit fix
# 自动修复不兼容的版本
npm audit fix --force
# 生成审计报告
npm audit --json > audit-report.json
// 在 CI/CD 流水线中集成
// .github/workflows/security.yml
name: Security Audit
on: [push, pull_request]
jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '18'
- run: npm install
- run: npm audit --audit-level=moderate
🎓 安全最佳实践清单
DO ✅
□ 使用参数化查询防止 SQL 注入
□ 对所有用户输入进行验证和转义
□ 使用 HTTPS 加密所有通信
□ 设置安全的 Cookie 标志(HttpOnly, Secure, SameSite)
□ 实现强认证(JWT, OAuth 2.0)
□ 使用 RBAC 进行访问控制
□ 记录安全事件进行审计
□ 定期更新依赖和补丁
□ 使用密钥管理服务存储敏感数据
□ 实现速率限制防止暴力破解
□ 定期进行安全审计和渗透测试
DON'T ❌
□ 不要在日志中记录敏感信息(密码、token)
□ 不要在代码中硬编码密钥
□ 不要使用过时的加密算法
□ 不要信任客户端验证
□ 不要暴露详细的错误信息
□ 不要使用弱密码策略
□ 不要忽视依赖漏洞
□ 不要禁用 HTTPS
□ 不要实现自己的加密算法
📚 总结
应用安全的核心:
- 防护漏洞: XSS、CSRF、SQL 注入、命令注入
- 强认证: JWT、OAuth 2.0、多因子认证
- 访问控制: RBAC、ABAC、细粒度权限
- 数据保护: 加密传输、加密存储、密钥管理
- 监控审计: 安全日志、告警、定期扫描
安全是一个持续的过程,不是一次性的工作!