Skip to content

认证与授权

概述

认证(Authentication)和授权(Authorization)是Web应用安全的基础。认证解决"你是谁"的问题,授权解决"你能做什么"的问题。

核心概念

认证 vs 授权

特性认证 (Authentication)授权 (Authorization)
目的验证用户身份控制用户权限
时机登录时进行访问资源时进行
结果确认用户是谁确认用户能做什么
示例用户名密码登录角色权限检查

安全原则

  1. 最小权限原则: 用户只获得完成任务所需的最小权限
  2. 职责分离: 不同角色承担不同职责
  3. 深度防御: 多层安全防护
  4. 安全默认: 默认拒绝,明确允许

认证方式

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应用安全的核心,需要综合考虑:

  1. 认证方式: 选择合适的认证方式(用户名密码、JWT、OAuth等)
  2. 授权模型: 实现合适的授权模型(RBAC、ABAC等)
  3. 安全措施: 实施密码安全、会话管理、速率限制等
  4. 监控审计: 记录和分析安全事件
  5. 持续改进: 根据安全威胁不断改进安全措施

良好的认证授权系统能够有效保护应用和用户数据的安全。