阿里云主机折上折
  • 微信号
您当前的位置:网站首页 > 用户认证与权限管理

用户认证与权限管理

作者:陈川 阅读数:7356人阅读 分类: MongoDB

用户认证与权限管理的基本概念

用户认证与权限管理是任何现代应用的核心功能之一。认证解决的是"你是谁"的问题,而权限管理则决定"你能做什么"。在Mongoose中,我们可以通过Schema设计、中间件和插件等方式实现这些功能。

Mongoose中的用户模型设计

用户模型是认证系统的基础。一个典型的用户Schema可能包含以下字段:

const userSchema = new mongoose.Schema({
  username: {
    type: String,
    required: true,
    unique: true,
    trim: true,
    minlength: 3
  },
  email: {
    type: String,
    required: true,
    unique: true,
    trim: true,
    lowercase: true,
    validate: [validator.isEmail, 'Invalid email']
  },
  password: {
    type: String,
    required: true,
    minlength: 8,
    select: false
  },
  role: {
    type: String,
    enum: ['user', 'admin', 'moderator'],
    default: 'user'
  },
  isActive: {
    type: Boolean,
    default: true
  },
  lastLogin: Date,
  passwordChangedAt: Date,
  passwordResetToken: String,
  passwordResetExpires: Date
}, {
  timestamps: true
});

密码加密与安全存储

明文存储密码是极其危险的。我们应该使用bcrypt等库对密码进行哈希处理:

userSchema.pre('save', async function(next) {
  if (!this.isModified('password')) return next();
  
  this.password = await bcrypt.hash(this.password, 12);
  this.passwordChangedAt = Date.now() - 1000;
  next();
});

认证流程实现

实现基本的登录认证流程:

userSchema.methods.correctPassword = async function(candidatePassword) {
  return await bcrypt.compare(candidatePassword, this.password);
};

userSchema.methods.createAuthToken = function() {
  return jwt.sign(
    { id: this._id, role: this.role },
    process.env.JWT_SECRET,
    { expiresIn: process.env.JWT_EXPIRES_IN }
  );
};

// 登录控制器示例
exports.login = async (req, res) => {
  const { email, password } = req.body;
  
  // 1) 检查邮箱和密码是否存在
  if (!email || !password) {
    return res.status(400).json({
      status: 'fail',
      message: '请提供邮箱和密码'
    });
  }
  
  // 2) 检查用户是否存在且密码正确
  const user = await User.findOne({ email }).select('+password');
  
  if (!user || !(await user.correctPassword(password))) {
    return res.status(401).json({
      status: 'fail',
      message: '邮箱或密码不正确'
    });
  }
  
  // 3) 生成token
  const token = user.createAuthToken();
  
  // 4) 发送响应
  res.status(200).json({
    status: 'success',
    token,
    data: {
      user
    }
  });
};

权限控制与角色管理

基于角色的访问控制(RBAC)是常见的权限管理方式:

// 角色权限映射
const rolePermissions = {
  user: ['read:own_profile', 'update:own_profile'],
  moderator: ['read:any_profile', 'update:any_profile', 'delete:comments'],
  admin: ['read:any_profile', 'update:any_profile', 'delete:any_profile', 'manage:users']
};

// 权限检查中间件
exports.restrictTo = (...roles) => {
  return (req, res, next) => {
    if (!roles.includes(req.user.role)) {
      return res.status(403).json({
        status: 'fail',
        message: '您没有执行此操作的权限'
      });
    }
    next();
  };
};

// 使用示例 - 保护路由
router.get(
  '/admin/users',
  authController.protect,
  authController.restrictTo('admin'),
  userController.getAllUsers
);

高级权限控制

对于更复杂的权限需求,可以使用基于属性的访问控制(ABAC):

// 检查用户是否拥有资源的权限
userSchema.methods.canAccessResource = function(resource) {
  if (this.role === 'admin') return true;
  
  // 用户只能访问自己的资源
  if (resource.user && resource.user.equals(this._id)) return true;
  
  // 特定条件下的访问权限
  if (this.role === 'moderator' && resource.type === 'comment') return true;
  
  return false;
};

// 使用示例
exports.getUser = async (req, res) => {
  const user = await User.findById(req.params.id);
  
  if (!req.user.canAccessResource(user)) {
    return res.status(403).json({
      status: 'fail',
      message: '无权访问此资源'
    });
  }
  
  res.status(200).json({
    status: 'success',
    data: {
      user
    }
  });
};

密码重置与账户安全

实现安全的密码重置流程:

userSchema.methods.createPasswordResetToken = function() {
  const resetToken = crypto.randomBytes(32).toString('hex');
  
  this.passwordResetToken = crypto
    .createHash('sha256')
    .update(resetToken)
    .digest('hex');
    
  this.passwordResetExpires = Date.now() + 10 * 60 * 1000; // 10分钟
  
  return resetToken;
};

// 密码重置控制器
exports.forgotPassword = async (req, res) => {
  // 1) 获取用户
  const user = await User.findOne({ email: req.body.email });
  if (!user) {
    return res.status(404).json({
      status: 'fail',
      message: '该邮箱未注册'
    });
  }
  
  // 2) 生成重置token
  const resetToken = user.createPasswordResetToken();
  await user.save({ validateBeforeSave: false });
  
  // 3) 发送邮件
  const resetURL = `${req.protocol}://${req.get('host')}/api/v1/users/resetPassword/${resetToken}`;
  
  try {
    await sendEmail({
      email: user.email,
      subject: '您的密码重置token(10分钟内有效)',
      message: `请访问以下链接重置密码: ${resetURL}`
    });
    
    res.status(200).json({
      status: 'success',
      message: '重置链接已发送至邮箱'
    });
  } catch (err) {
    user.passwordResetToken = undefined;
    user.passwordResetExpires = undefined;
    await user.save({ validateBeforeSave: false });
    
    return res.status(500).json({
      status: 'error',
      message: '发送邮件时出错,请稍后重试'
    });
  }
};

会话管理与JWT刷新

实现安全的JWT刷新机制:

// 生成刷新token
userSchema.methods.createRefreshToken = function() {
  return jwt.sign(
    { id: this._id },
    process.env.JWT_REFRESH_SECRET,
    { expiresIn: process.env.JWT_REFRESH_EXPIRES_IN }
  );
};

// 刷新token端点
exports.refreshToken = async (req, res) => {
  const { refreshToken } = req.body;
  
  if (!refreshToken) {
    return res.status(401).json({
      status: 'fail',
      message: '未提供刷新token'
    });
  }
  
  try {
    const decoded = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET);
    
    const user = await User.findById(decoded.id);
    if (!user) {
      return res.status(401).json({
        status: 'fail',
        message: '用户不存在'
      });
    }
    
    const newAccessToken = user.createAuthToken();
    
    res.status(200).json({
      status: 'success',
      accessToken: newAccessToken
    });
  } catch (err) {
    return res.status(403).json({
      status: 'fail',
      message: '无效的刷新token'
    });
  }
};

多因素认证实现

增加额外的安全层:

userSchema.add({
  twoFactorEnabled: {
    type: Boolean,
    default: false
  },
  twoFactorSecret: String
});

// 启用双因素认证
exports.enableTwoFactor = async (req, res) => {
  const secret = speakeasy.generateSecret({ length: 20 });
  
  const user = await User.findByIdAndUpdate(
    req.user.id,
    {
      twoFactorEnabled: true,
      twoFactorSecret: secret.base32
    },
    { new: true }
  );
  
  res.status(200).json({
    status: 'success',
    data: {
      otpauthUrl: secret.otpauth_url,
      secret: secret.base32
    }
  });
};

// 验证双因素代码
exports.verifyTwoFactor = async (req, res) => {
  const { token } = req.body;
  
  const user = await User.findById(req.user.id);
  
  const verified = speakeasy.totp.verify({
    secret: user.twoFactorSecret,
    encoding: 'base32',
    token
  });
  
  if (!verified) {
    return res.status(400).json({
      status: 'fail',
      message: '验证码无效'
    });
  }
  
  // 标记为已验证,可以继续登录流程
  req.session.twoFactorVerified = true;
  
  res.status(200).json({
    status: 'success',
    message: '双因素认证成功'
  });
};

审计日志与安全监控

记录关键操作以便审计:

const auditSchema = new mongoose.Schema({
  user: {
    type: mongoose.Schema.ObjectId,
    ref: 'User'
  },
  action: String,
  entityType: String,
  entityId: mongoose.Schema.Types.Mixed,
  metadata: Object,
  ipAddress: String,
  userAgent: String,
  timestamp: {
    type: Date,
    default: Date.now
  }
});

// 审计日志中间件
userSchema.post('save', function(doc) {
  if (doc.isNew) {
    AuditLog.create({
      user: doc._id,
      action: 'user_created',
      entityType: 'User',
      entityId: doc._id,
      metadata: {
        email: doc.email,
        role: doc.role
      }
    });
  }
});

// 删除操作审计
userSchema.pre('remove', function(next) {
  AuditLog.create({
    user: this._id,
    action: 'user_deleted',
    entityType: 'User',
    entityId: this._id
  });
  next();
});

性能优化与最佳实践

优化认证系统的性能:

// 使用索引加速查询
userSchema.index({ email: 1 });
userSchema.index({ passwordResetToken: 1 }, { expireAfterSeconds: 0 });

// 批量操作时的优化
exports.deactivateInactiveUsers = async () => {
  const thirtyDaysAgo = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
  
  const result = await User.updateMany(
    {
      lastLogin: { $lt: thirtyDaysAgo },
      isActive: true
    },
    {
      $set: { isActive: false }
    }
  );
  
  return result.nModified;
};

// 使用lean()提高查询性能
exports.getUserProfile = async (userId) => {
  return User.findById(userId)
    .select('-password -passwordResetToken -passwordResetExpires')
    .lean();
};

测试与调试

编写测试用例确保认证系统可靠:

describe('用户认证', () => {
  beforeEach(async () => {
    await User.deleteMany();
  });
  
  test('成功注册新用户', async () => {
    const res = await request(app)
      .post('/api/v1/auth/signup')
      .send({
        username: 'testuser',
        email: 'test@example.com',
        password: 'password123',
        passwordConfirm: 'password123'
      });
    
    expect(res.statusCode).toBe(201);
    expect(res.body.token).toBeDefined();
    
    const user = await User.findOne({ email: 'test@example.com' });
    expect(user).toBeTruthy();
    expect(user.password).not.toBe('password123');
  });
  
  test('登录失败-密码错误', async () => {
    await User.create({
      username: 'testuser',
      email: 'test@example.com',
      password: await bcrypt.hash('correctpassword', 12)
    });
    
    const res = await request(app)
      .post('/api/v1/auth/login')
      .send({
        email: 'test@example.com',
        password: 'wrongpassword'
      });
    
    expect(res.statusCode).toBe(401);
    expect(res.body.message).toMatch(/邮箱或密码不正确/);
  });
});

本站部分内容来自互联网,一切版权均归源网站或源作者所有。

如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn

前端川

前端川,陈川的代码茶馆🍵,专治各种不服的Bug退散符💻,日常贩卖秃头警告级的开发心得🛠️,附赠一行代码笑十年的摸鱼宝典🐟,偶尔掉落咖啡杯里泡开的像素级浪漫☕。‌