认证与授权
概述
认证(Authentication)和授权(Authorization)是Web应用安全的基础。认证解决"你是谁"的问题,授权解决"你能做什么"的问题。
核心概念
认证 vs 授权
特性 | 认证 (Authentication) | 授权 (Authorization) |
---|---|---|
目的 | 验证用户身份 | 控制用户权限 |
时机 | 登录时进行 | 访问资源时进行 |
结果 | 确认用户是谁 | 确认用户能做什么 |
示例 | 用户名密码登录 | 角色权限检查 |
安全原则
- 最小权限原则: 用户只获得完成任务所需的最小权限
- 职责分离: 不同角色承担不同职责
- 深度防御: 多层安全防护
- 安全默认: 默认拒绝,明确允许
认证方式
1. 用户名密码认证
javascript
// 用户登录
const login = async (req, res) => {
try {
const { username, password } = req.body;
// 查找用户
const user = await User.findOne({ username });
if (!user) {
return res.status(401).json({
success: false,
error: { message: '用户名或密码错误' }
});
}
// 验证密码
const isValidPassword = await bcrypt.compare(password, user.password);
if (!isValidPassword) {
return res.status(401).json({
success: false,
error: { message: '用户名或密码错误' }
});
}
// 生成JWT令牌
const token = jwt.sign(
{ userId: user._id, username: user.username },
process.env.JWT_SECRET,
{ expiresIn: '24h' }
);
res.json({
success: true,
data: {
token,
user: {
id: user._id,
username: user.username,
email: user.email
}
}
});
} catch (error) {
res.status(500).json({
success: false,
error: { message: '登录失败' }
});
}
};
2. JWT (JSON Web Token)
JWT结构
javascript
// JWT由三部分组成:Header.Payload.Signature
const jwt = require('jsonwebtoken');
// 创建JWT
const createToken = (payload) => {
return jwt.sign(payload, process.env.JWT_SECRET, {
expiresIn: '24h',
issuer: 'myapp',
audience: 'myapp-users'
});
};
// 验证JWT
const verifyToken = (token) => {
try {
return jwt.verify(token, process.env.JWT_SECRET);
} catch (error) {
throw new Error('无效的令牌');
}
};
// 刷新令牌
const refreshToken = (token) => {
const decoded = jwt.decode(token);
delete decoded.iat;
delete decoded.exp;
return createToken(decoded);
};
JWT中间件
javascript
// JWT认证中间件
const authenticateJWT = (req, res, next) => {
const authHeader = req.headers.authorization;
if (!authHeader) {
return res.status(401).json({
success: false,
error: { message: '访问令牌缺失' }
});
}
const token = authHeader.split(' ')[1]; // Bearer <token>
try {
const decoded = verifyToken(token);
req.user = decoded;
next();
} catch (error) {
return res.status(403).json({
success: false,
error: { message: '访问令牌无效' }
});
}
};
// 可选认证中间件
const optionalAuth = (req, res, next) => {
const authHeader = req.headers.authorization;
if (authHeader) {
try {
const token = authHeader.split(' ')[1];
const decoded = verifyToken(token);
req.user = decoded;
} catch (error) {
// 忽略无效令牌
}
}
next();
};
3. OAuth 2.0
OAuth流程
javascript
// 授权码模式
const oauth2 = require('oauth2-server');
const oauth = new oauth2({
model: {
// 获取客户端
getClient: async (clientId, clientSecret) => {
const client = await Client.findOne({ clientId });
if (client && client.clientSecret === clientSecret) {
return client;
}
return false;
},
// 获取用户
getUser: async (username, password) => {
const user = await User.findOne({ username });
if (user && await bcrypt.compare(password, user.password)) {
return user;
}
return false;
},
// 保存授权码
saveAuthorizationCode: async (code, client, user) => {
const authCode = new AuthorizationCode({
authorizationCode: code.authorizationCode,
expiresAt: code.expiresAt,
scope: code.scope,
client: client.id,
user: user.id
});
await authCode.save();
return authCode;
},
// 获取授权码
getAuthorizationCode: async (authorizationCode) => {
return await AuthorizationCode.findOne({ authorizationCode });
},
// 保存访问令牌
saveToken: async (token, client, user) => {
const accessToken = new AccessToken({
accessToken: token.accessToken,
accessTokenExpiresAt: token.accessTokenExpiresAt,
scope: token.scope,
client: client.id,
user: user.id
});
await accessToken.save();
return accessToken;
},
// 获取访问令牌
getAccessToken: async (accessToken) => {
return await AccessToken.findOne({ accessToken });
}
}
});
// 授权端点
app.get('/oauth/authorize', async (req, res) => {
const request = new oauth.Request(req);
const response = new oauth.Response(res);
try {
const token = await oauth.authorize(request, response);
res.redirect(`${token.redirectUri}?code=${token.authorizationCode}`);
} catch (error) {
res.status(400).json({ error: error.message });
}
});
// 令牌端点
app.post('/oauth/token', async (req, res) => {
const request = new oauth.Request(req);
const response = new oauth.Response(res);
try {
const token = await oauth.token(request, response);
res.json(token);
} catch (error) {
res.status(400).json({ error: error.message });
}
});
4. 第三方登录
Google OAuth
javascript
const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth20').Strategy;
passport.use(new GoogleStrategy({
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: "/auth/google/callback"
}, async (accessToken, refreshToken, profile, done) => {
try {
// 查找或创建用户
let user = await User.findOne({ googleId: profile.id });
if (!user) {
user = new User({
googleId: profile.id,
username: profile.displayName,
email: profile.emails[0].value,
avatar: profile.photos[0].value
});
await user.save();
}
return done(null, user);
} catch (error) {
return done(error, null);
}
}));
// 登录路由
app.get('/auth/google',
passport.authenticate('google', { scope: ['profile', 'email'] })
);
app.get('/auth/google/callback',
passport.authenticate('google', { failureRedirect: '/login' }),
(req, res) => {
// 生成JWT令牌
const token = jwt.sign(
{ userId: req.user._id },
process.env.JWT_SECRET,
{ expiresIn: '24h' }
);
res.redirect(`/dashboard?token=${token}`);
}
);
授权模型
1. RBAC (基于角色的访问控制)
javascript
// 角色定义
const roles = {
ADMIN: 'admin',
USER: 'user',
MODERATOR: 'moderator',
GUEST: 'guest'
};
// 权限定义
const permissions = {
READ_USERS: 'read:users',
WRITE_USERS: 'write:users',
DELETE_USERS: 'delete:users',
READ_POSTS: 'read:posts',
WRITE_POSTS: 'write:posts',
DELETE_POSTS: 'delete:posts'
};
// 角色权限映射
const rolePermissions = {
[roles.ADMIN]: [
permissions.READ_USERS,
permissions.WRITE_USERS,
permissions.DELETE_USERS,
permissions.READ_POSTS,
permissions.WRITE_POSTS,
permissions.DELETE_POSTS
],
[roles.MODERATOR]: [
permissions.READ_USERS,
permissions.READ_POSTS,
permissions.WRITE_POSTS,
permissions.DELETE_POSTS
],
[roles.USER]: [
permissions.READ_POSTS,
permissions.WRITE_POSTS
],
[roles.GUEST]: [
permissions.READ_POSTS
]
};
// 权限检查中间件
const checkPermission = (permission) => {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({
success: false,
error: { message: '未授权访问' }
});
}
const userRole = req.user.role || roles.GUEST;
const userPermissions = rolePermissions[userRole] || [];
if (!userPermissions.includes(permission)) {
return res.status(403).json({
success: false,
error: { message: '权限不足' }
});
}
next();
};
};
// 使用权限检查
app.get('/users',
authenticateJWT,
checkPermission(permissions.READ_USERS),
async (req, res) => {
// 获取用户列表
}
);
app.post('/users',
authenticateJWT,
checkPermission(permissions.WRITE_USERS),
async (req, res) => {
// 创建用户
}
);
2. ABAC (基于属性的访问控制)
javascript
// 属性检查中间件
const checkAttribute = (attribute, value) => {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({
success: false,
error: { message: '未授权访问' }
});
}
// 检查用户属性
if (req.user[attribute] !== value) {
return res.status(403).json({
success: false,
error: { message: '权限不足' }
});
}
next();
};
};
// 资源所有者检查
const checkResourceOwner = (resourceModel) => {
return async (req, res, next) => {
try {
const resource = await resourceModel.findById(req.params.id);
if (!resource) {
return res.status(404).json({
success: false,
error: { message: '资源不存在' }
});
}
// 检查是否为资源所有者或管理员
if (resource.userId.toString() !== req.user.userId && req.user.role !== 'admin') {
return res.status(403).json({
success: false,
error: { message: '权限不足' }
});
}
req.resource = resource;
next();
} catch (error) {
res.status(500).json({
success: false,
error: { message: '服务器错误' }
});
}
};
};
// 使用属性检查
app.put('/posts/:id',
authenticateJWT,
checkResourceOwner(Post),
async (req, res) => {
// 更新文章
}
);
3. 动态权限
javascript
// 动态权限检查
const checkDynamicPermission = (permission, resourceType) => {
return async (req, res, next) => {
try {
const user = await User.findById(req.user.userId).populate('permissions');
// 检查用户是否有特定权限
const hasPermission = user.permissions.some(p =>
p.name === permission &&
(p.resourceType === resourceType || p.resourceType === '*')
);
if (!hasPermission) {
return res.status(403).json({
success: false,
error: { message: '权限不足' }
});
}
next();
} catch (error) {
res.status(500).json({
success: false,
error: { message: '权限检查失败' }
});
}
};
};
// 权限模型
const PermissionSchema = new mongoose.Schema({
name: { type: String, required: true },
resourceType: { type: String, required: true }, // 'user', 'post', '*'
resourceId: { type: mongoose.Schema.Types.ObjectId }, // 特定资源ID
granted: { type: Boolean, default: true }
});
const UserSchema = new mongoose.Schema({
username: { type: String, required: true },
email: { type: String, required: true },
permissions: [{ type: mongoose.Schema.Types.ObjectId, ref: 'Permission' }]
});
安全最佳实践
1. 密码安全
javascript
// 密码加密
const bcrypt = require('bcrypt');
const hashPassword = async (password) => {
const saltRounds = 12;
return await bcrypt.hash(password, saltRounds);
};
// 密码验证
const validatePassword = (password) => {
const minLength = 8;
const hasUpperCase = /[A-Z]/.test(password);
const hasLowerCase = /[a-z]/.test(password);
const hasNumbers = /\d/.test(password);
const hasSpecialChar = /[!@#$%^&*(),.?":{}|<>]/.test(password);
if (password.length < minLength) {
throw new Error('密码至少8位');
}
if (!hasUpperCase || !hasLowerCase) {
throw new Error('密码必须包含大小写字母');
}
if (!hasNumbers) {
throw new Error('密码必须包含数字');
}
if (!hasSpecialChar) {
throw new Error('密码必须包含特殊字符');
}
return true;
};
// 用户注册
const register = async (req, res) => {
try {
const { username, email, password } = req.body;
// 验证密码强度
validatePassword(password);
// 检查用户是否已存在
const existingUser = await User.findOne({
$or: [{ username }, { email }]
});
if (existingUser) {
return res.status(409).json({
success: false,
error: { message: '用户名或邮箱已存在' }
});
}
// 加密密码
const hashedPassword = await hashPassword(password);
// 创建用户
const user = new User({
username,
email,
password: hashedPassword
});
await user.save();
res.status(201).json({
success: true,
message: '用户注册成功'
});
} catch (error) {
res.status(400).json({
success: false,
error: { message: error.message }
});
}
};
2. 会话管理
javascript
// 会话存储
const session = require('express-session');
const RedisStore = require('connect-redis')(session);
const redis = require('redis');
const redisClient = redis.createClient();
app.use(session({
store: new RedisStore({ client: redisClient }),
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
cookie: {
secure: process.env.NODE_ENV === 'production',
httpOnly: true,
maxAge: 24 * 60 * 60 * 1000 // 24小时
}
}));
// 会话清理
const cleanupSessions = async (userId) => {
const sessions = await redisClient.keys(`sess:*`);
for (const sessionKey of sessions) {
const sessionData = await redisClient.get(sessionKey);
const session = JSON.parse(sessionData);
if (session.userId === userId) {
await redisClient.del(sessionKey);
}
}
};
3. 速率限制
javascript
const rateLimit = require('express-rate-limit');
const RedisStore = require('rate-limit-redis');
const redis = require('redis');
const redisClient = redis.createClient();
// 登录限制
const loginLimiter = rateLimit({
store: new RedisStore({
client: redisClient,
prefix: 'login_limit:'
}),
windowMs: 15 * 60 * 1000, // 15分钟
max: 5, // 最多5次尝试
message: {
success: false,
error: { message: '登录尝试过于频繁,请稍后再试' }
},
standardHeaders: true,
legacyHeaders: false
});
// API限制
const apiLimiter = rateLimit({
store: new RedisStore({
client: redisClient,
prefix: 'api_limit:'
}),
windowMs: 60 * 1000, // 1分钟
max: 100, // 最多100次请求
message: {
success: false,
error: { message: '请求过于频繁' }
}
});
app.use('/auth/login', loginLimiter);
app.use('/api', apiLimiter);
4. CSRF保护
javascript
const csrf = require('csurf');
// CSRF保护
app.use(csrf({ cookie: true }));
// CSRF令牌中间件
app.use((req, res, next) => {
res.cookie('XSRF-TOKEN', req.csrfToken());
next();
});
// 错误处理
app.use((err, req, res, next) => {
if (err.code === 'EBADCSRFTOKEN') {
return res.status(403).json({
success: false,
error: { message: 'CSRF令牌无效' }
});
}
next(err);
});
监控和审计
1. 登录日志
javascript
// 登录日志模型
const LoginLogSchema = new mongoose.Schema({
userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
username: { type: String, required: true },
ipAddress: { type: String, required: true },
userAgent: { type: String },
success: { type: Boolean, required: true },
timestamp: { type: Date, default: Date.now }
});
// 记录登录尝试
const logLoginAttempt = async (userId, username, ipAddress, userAgent, success) => {
const log = new LoginLog({
userId,
username,
ipAddress,
userAgent,
success
});
await log.save();
};
// 检测异常登录
const detectSuspiciousLogin = async (userId, ipAddress) => {
const recentLogs = await LoginLog.find({
userId,
timestamp: { $gte: new Date(Date.now() - 24 * 60 * 60 * 1000) }
});
const uniqueIPs = [...new Set(recentLogs.map(log => log.ipAddress))];
if (uniqueIPs.length > 5) {
// 发送安全警告
await sendSecurityAlert(userId, '检测到异常登录活动');
}
};
2. 权限审计
javascript
// 权限审计模型
const PermissionAuditSchema = new mongoose.Schema({
userId: { type: mongoose.Schema.Types.ObjectId, ref: 'User' },
action: { type: String, required: true },
resource: { type: String, required: true },
resourceId: { type: mongoose.Schema.Types.ObjectId },
ipAddress: { type: String },
timestamp: { type: Date, default: Date.now }
});
// 审计中间件
const auditPermission = (action, resource) => {
return (req, res, next) => {
const originalSend = res.send;
res.send = function(data) {
// 记录权限使用
const audit = new PermissionAudit({
userId: req.user?.userId,
action,
resource,
resourceId: req.params.id,
ipAddress: req.ip
});
audit.save();
originalSend.call(this, data);
};
next();
};
};
总结
认证与授权是Web应用安全的核心,需要综合考虑:
- 认证方式: 选择合适的认证方式(用户名密码、JWT、OAuth等)
- 授权模型: 实现合适的授权模型(RBAC、ABAC等)
- 安全措施: 实施密码安全、会话管理、速率限制等
- 监控审计: 记录和分析安全事件
- 持续改进: 根据安全威胁不断改进安全措施
良好的认证授权系统能够有效保护应用和用户数据的安全。