跳到主要内容

Feathers.js 认证与授权 - 构建安全的API系统

· 阅读需 12 分钟
一介布衣
全栈开发者

发布时间:2024-05-29
作者:一介布衣
标签:Feathers.js, 认证, 授权, JWT, 安全

前言

上一篇文章我们深入学习了钩子系统,今天咱们来重点学习 Feathers.js 的认证与授权系统。说实话,安全性是任何 Web 应用都不能忽视的重要环节,而 Feathers.js 在这方面提供了非常完善的解决方案。

我记得刚开始做 Web 开发的时候,总是把认证和授权搞混,以为登录了就万事大吉。后来踩了不少坑才明白,认证只是确认"你是谁",而授权是确认"你能做什么"。Feathers.js 的认证系统设计得很巧妙,不仅支持多种认证策略,还能很好地与钩子系统结合实现细粒度的权限控制。

今天我就带大家从基础的用户认证开始,一步步构建一个完整的安全系统。

认证系统概述

认证 vs 授权

// 认证 (Authentication) - 确认用户身份
"你是 John Doe 吗?" → 验证用户名密码、JWT token 等

// 授权 (Authorization) - 确认用户权限
"John Doe 可以删除这篇文章吗?" → 检查角色、权限、资源所有权等

Feathers.js 认证架构

// 认证流程
Client Request

Authentication Strategy (local, jwt, oauth)

User Verification

Token Generation/Validation

User Object in context.params.user

Authorization Hooks

Service Method Execution

基础认证配置

1. 安装认证插件

npm install @feathersjs/authentication @feathersjs/authentication-local @feathersjs/authentication-oauth

2. 认证配置

// config/default.json
{
"authentication": {
"secret": "your-super-secret-key",
"strategies": ["jwt", "local"],
"path": "/authentication",
"service": "users",
"jwt": {
"header": { "typ": "access" },
"audience": "https://yourdomain.com",
"subject": "anonymous",
"issuer": "feathers",
"algorithm": "HS256",
"expiresIn": "1d"
},
"local": {
"usernameField": "email",
"passwordField": "password"
}
}
}

3. 应用配置

// src/authentication.js
const { AuthenticationService, JWTStrategy } = require('@feathersjs/authentication');
const { LocalStrategy } = require('@feathersjs/authentication-local');
const { expressOauth } = require('@feathersjs/authentication-oauth');

module.exports = app => {
const authentication = new AuthenticationService(app);

authentication.register('jwt', new JWTStrategy());
authentication.register('local', new LocalStrategy());

app.use('/authentication', authentication);
app.configure(expressOauth());
};

用户模型和服务

1. 用户模型

// src/models/users.model.js
module.exports = function (app) {
const modelName = 'users';
const mongooseClient = app.get('mongooseClient');
const { Schema } = mongooseClient;

const schema = new Schema({
email: {
type: String,
unique: true,
lowercase: true,
required: true,
validate: {
validator: function(v) {
return /^\w+([.-]?\w+)*@\w+([.-]?\w+)*(\.\w{2,3})+$/.test(v);
},
message: '邮箱格式不正确'
}
},
password: {
type: String,
required: true,
minlength: 6
},

// 基本信息
username: {
type: String,
unique: true,
required: true,
trim: true,
minlength: 3,
maxlength: 30
},
firstName: {
type: String,
trim: true,
maxlength: 50
},
lastName: {
type: String,
trim: true,
maxlength: 50
},
avatar: {
type: String,
default: null
},

// 角色和权限
role: {
type: String,
enum: ['user', 'moderator', 'admin'],
default: 'user'
},
permissions: [{
type: String,
enum: [
'read:posts', 'write:posts', 'delete:posts',
'read:users', 'write:users', 'delete:users',
'read:comments', 'write:comments', 'delete:comments',
'manage:system'
]
}],

// 账户状态
status: {
type: String,
enum: ['active', 'inactive', 'suspended', 'pending'],
default: 'pending'
},
emailVerified: {
type: Boolean,
default: false
},
emailVerificationToken: {
type: String,
default: null
},

// 安全相关
lastLoginAt: {
type: Date,
default: null
},
loginAttempts: {
type: Number,
default: 0
},
lockUntil: {
type: Date,
default: null
},
passwordResetToken: {
type: String,
default: null
},
passwordResetExpires: {
type: Date,
default: null
},

// 偏好设置
preferences: {
theme: {
type: String,
enum: ['light', 'dark', 'auto'],
default: 'light'
},
language: {
type: String,
default: 'zh-CN'
},
notifications: {
email: { type: Boolean, default: true },
push: { type: Boolean, default: true },
sms: { type: Boolean, default: false }
}
}
}, {
timestamps: true
});

// 索引
schema.index({ email: 1 });
schema.index({ username: 1 });
schema.index({ role: 1 });
schema.index({ status: 1 });

// 虚拟字段
schema.virtual('fullName').get(function() {
return `\${this.firstName || ''} \${this.lastName || ''}`.trim();
});

schema.virtual('isLocked').get(function() {
return !!(this.lockUntil && this.lockUntil > Date.now());
});

// 实例方法
schema.methods.toSafeObject = function() {
const obj = this.toObject();
delete obj.password;
delete obj.emailVerificationToken;
delete obj.passwordResetToken;
delete obj.loginAttempts;
delete obj.lockUntil;
return obj;
};

schema.methods.hasPermission = function(permission) {
// 管理员拥有所有权限
if (this.role === 'admin') {
return true;
}

// 检查具体权限
return this.permissions.includes(permission);
};

schema.methods.hasRole = function(roles) {
const roleArray = Array.isArray(roles) ? roles : [roles];
return roleArray.includes(this.role);
};

schema.methods.canAccess = function(resource, action = 'read') {
const permission = `\${action}:\${resource}`;
return this.hasPermission(permission);
};

return mongooseClient.model(modelName, schema);
};

2. 用户服务钩子

// src/services/users/users.hooks.js
const { authenticate } = require('@feathersjs/authentication').hooks;
const { hashPassword, protect } = require('@feathersjs/authentication-local').hooks;
const { disallow, iff, isProvider } = require('feathers-hooks-common');

// 验证用户数据
const validateUser = () => {
return async context => {
const { data } = context;

// 检查用户名唯一性
if (data.username) {
const existingUser = await context.app.service('users').find({
query: {
username: data.username,
$limit: 1
}
});

if (existingUser.total > 0 && existingUser.data[0]._id.toString() !== context.id) {
throw new Error('用户名已存在');
}
}

// 检查邮箱唯一性
if (data.email) {
const existingUser = await context.app.service('users').find({
query: {
email: data.email,
$limit: 1
}
});

if (existingUser.total > 0 && existingUser.data[0]._id.toString() !== context.id) {
throw new Error('邮箱已被注册');
}
}

return context;
};
};

// 权限检查
const checkPermissions = () => {
return async context => {
const { user } = context.params;

if (!user) {
throw new Error('用户未登录');
}

// 用户只能查看自己的信息,除非是管理员
if (context.method === 'get' && context.id) {
if (context.id !== user._id.toString() && user.role !== 'admin') {
throw new Error('无权限访问其他用户信息');
}
}

// 用户只能修改自己的信息,除非是管理员
if (['update', 'patch'].includes(context.method)) {
if (context.id !== user._id.toString() && user.role !== 'admin') {
throw new Error('无权限修改其他用户信息');
}

// 普通用户不能修改角色和权限
if (user.role !== 'admin' && (context.data.role || context.data.permissions)) {
delete context.data.role;
delete context.data.permissions;
}
}

// 只有管理员可以删除用户
if (context.method === 'remove' && user.role !== 'admin') {
throw new Error('无权限删除用户');
}

return context;
};
};

// 限制查询结果
const restrictFind = () => {
return async context => {
const { user } = context.params;

if (!user) {
throw new Error('用户未登录');
}

// 普通用户只能查看激活的用户基本信息
if (user.role !== 'admin') {
context.params.query.status = 'active';
context.params.query.$select = ['_id', 'username', 'firstName', 'lastName', 'avatar'];
}

return context;
};
};

// 设置默认值
const setDefaults = () => {
return context => {
if (context.data) {
// 新用户默认状态
if (context.method === 'create') {
context.data.status = 'pending';
context.data.emailVerified = false;
context.data.role = context.data.role || 'user';
}
}
return context;
};
};

module.exports = {
before: {
all: [],
find: [
iff(isProvider('external'), authenticate('jwt')),
iff(isProvider('external'), restrictFind())
],
get: [
iff(isProvider('external'), authenticate('jwt')),
iff(isProvider('external'), checkPermissions())
],
create: [
hashPassword('password'),
validateUser(),
setDefaults()
],
update: [
iff(isProvider('external'), authenticate('jwt')),
iff(isProvider('external'), checkPermissions()),
hashPassword('password'),
validateUser()
],
patch: [
iff(isProvider('external'), authenticate('jwt')),
iff(isProvider('external'), checkPermissions()),
hashPassword('password'),
validateUser()
],
remove: [
iff(isProvider('external'), authenticate('jwt')),
iff(isProvider('external'), checkPermissions())
]
},

after: {
all: [
// 确保密码字段不会被返回
protect('password')
],
find: [],
get: [],
create: [],
update: [],
patch: [],
remove: []
},

error: {
all: [],
find: [],
get: [],
create: [],
update: [],
patch: [],
remove: []
}
};

高级认证策略

1. 自定义认证策略

// src/authentication/custom-strategy.js
const { AuthenticationBaseStrategy } = require('@feathersjs/authentication');

class ApiKeyStrategy extends AuthenticationBaseStrategy {
async authenticate(authentication, params) {
const { apiKey } = authentication;

if (!apiKey) {
throw new Error('API Key 不能为空');
}

// 验证 API Key
const user = await this.app.service('users').find({
query: {
apiKey: apiKey,
status: 'active'
}
});

if (user.total === 0) {
throw new Error('无效的 API Key');
}

return {
authentication: { strategy: this.name },
user: user.data[0]
};
}
}

module.exports = ApiKeyStrategy;

2. OAuth 认证

// src/authentication/oauth.js
const { OAuthStrategy } = require('@feathersjs/authentication-oauth');

class GitHubStrategy extends OAuthStrategy {
async getEntityData(profile, existing, params) {
const baseData = await super.getEntityData(profile, existing, params);

return {
...baseData,
githubId: profile.id,
username: profile.login,
email: profile.email,
avatar: profile.avatar_url,
firstName: profile.name ? profile.name.split(' ')[0] : '',
lastName: profile.name ? profile.name.split(' ').slice(1).join(' ') : '',
emailVerified: true,
status: 'active'
};
}

async getProfile(authResult, params) {
const profile = await super.getProfile(authResult, params);

// 获取用户的邮箱(如果公开的话)
if (!profile.email) {
try {
const response = await this.request({
url: 'https://api.github.com/user/emails',
headers: {
authorization: `token \${authResult.access_token}`
}
});

const emails = response.data;
const primaryEmail = emails.find(email => email.primary);
if (primaryEmail) {
profile.email = primaryEmail.email;
}
} catch (error) {
console.error('获取 GitHub 邮箱失败:', error);
}
}

return profile;
}
}

// 配置 OAuth
module.exports = app => {
const authentication = app.service('authentication');

authentication.register('github', new GitHubStrategy());

// 配置 OAuth 路由
app.get('/oauth/github', authentication.authenticate('github'));
app.get('/oauth/github/callback',
authentication.authenticate('github'),
(req, res) => {
res.redirect('/dashboard');
}
);
};

3. 多因素认证

// src/services/mfa/mfa.class.js
const speakeasy = require('speakeasy');
const QRCode = require('qrcode');

class MFAService {
constructor(options, app) {
this.options = options || {};
this.app = app;
}

// 生成 TOTP 密钥
async create(data, params) {
const { user } = params;

if (!user) {
throw new Error('用户未登录');
}

const secret = speakeasy.generateSecret({
name: `\${this.app.get('appName')}:\${user.email}`,
issuer: this.app.get('appName')
});

// 保存密钥到用户记录
await this.app.service('users').patch(user._id, {
mfaSecret: secret.base32,
mfaEnabled: false
});

// 生成二维码
const qrCodeUrl = await QRCode.toDataURL(secret.otpauth_url);

return {
secret: secret.base32,
qrCode: qrCodeUrl,
manualEntryKey: secret.base32
};
}

// 验证 TOTP 代码
async update(id, data, params) {
const { user } = params;
const { token } = data;

if (!user || !user.mfaSecret) {
throw new Error('MFA 未设置');
}

const verified = speakeasy.totp.verify({
secret: user.mfaSecret,
encoding: 'base32',
token: token,
window: 2 // 允许前后2个时间窗口
});

if (!verified) {
throw new Error('验证码错误');
}

// 启用 MFA
await this.app.service('users').patch(user._id, {
mfaEnabled: true
});

return { message: 'MFA 启用成功' };
}

// 验证登录时的 MFA
async patch(id, data, params) {
const { userId, token } = data;

const user = await this.app.service('users').get(userId);

if (!user.mfaEnabled || !user.mfaSecret) {
throw new Error('用户未启用 MFA');
}

const verified = speakeasy.totp.verify({
secret: user.mfaSecret,
encoding: 'base32',
token: token,
window: 2
});

if (!verified) {
throw new Error('验证码错误');
}

return { verified: true };
}

// 禁用 MFA
async remove(id, params) {
const { user } = params;

if (!user) {
throw new Error('用户未登录');
}

await this.app.service('users').patch(user._id, {
mfaSecret: null,
mfaEnabled: false
});

return { message: 'MFA 已禁用' };
}
}

module.exports = MFAService;

权限控制系统

1. 基于角色的访问控制(RBAC)

// src/hooks/rbac.js
const roles = {
admin: {
permissions: ['*'], // 所有权限
inherits: []
},
moderator: {
permissions: [
'read:posts', 'write:posts', 'delete:posts',
'read:comments', 'write:comments', 'delete:comments',
'read:users'
],
inherits: ['user']
},
user: {
permissions: [
'read:posts', 'write:own:posts',
'read:comments', 'write:comments',
'read:profile', 'write:own:profile'
],
inherits: []
}
};

const getUserPermissions = (userRole) => {
const role = roles[userRole];
if (!role) return [];

let permissions = [...role.permissions];

// 继承父角色权限
role.inherits.forEach(inheritedRole => {
permissions = permissions.concat(getUserPermissions(inheritedRole));
});

return [...new Set(permissions)]; // 去重
};

const hasPermission = (user, permission, resource = null) => {
const userPermissions = getUserPermissions(user.role);

// 检查通配符权限
if (userPermissions.includes('*')) {
return true;
}

// 检查精确权限
if (userPermissions.includes(permission)) {
return true;
}

// 检查资源所有权权限
if (resource && userPermissions.includes(permission.replace(':', ':own:'))) {
return resource.userId && resource.userId.toString() === user._id.toString();
}

return false;
};

// 权限检查钩子
const requirePermission = (permission) => {
return async (context) => {
const { user } = context.params;

if (!user) {
throw new Error('用户未登录');
}

let resource = null;

// 对于需要资源的操作,获取资源信息
if (context.id && ['get', 'update', 'patch', 'remove'].includes(context.method)) {
try {
resource = await context.service.get(context.id, {
...context.params,
query: {}
});
} catch (error) {
// 资源不存在
throw new Error('资源不存在');
}
}

if (!hasPermission(user, permission, resource)) {
throw new Error(`需要权限: \${permission}`);
}

return context;
};
};

module.exports = {
getUserPermissions,
hasPermission,
requirePermission
};

2. 资源级权限控制

// src/hooks/resource-permissions.js
const { requirePermission } = require('./rbac');

// 动态权限检查
const checkResourcePermission = (resourceType) => {
return async (context) => {
const { method, user } = context.params;

const actionMap = {
find: 'read',
get: 'read',
create: 'write',
update: 'write',
patch: 'write',
remove: 'delete'
};

const action = actionMap[context.method];
const permission = `\${action}:\${resourceType}`;

return requirePermission(permission)(context);
};
};

// 字段级权限控制
const filterFields = (allowedFields) => {
return (context) => {
const { user } = context.params;

if (user.role === 'admin') {
return context; // 管理员可以看到所有字段
}

const filterData = (data) => {
const filtered = {};
allowedFields.forEach(field => {
if (data[field] !== undefined) {
filtered[field] = data[field];
}
});
return filtered;
};

if (context.result) {
if (Array.isArray(context.result.data)) {
context.result.data = context.result.data.map(filterData);
} else {
context.result = filterData(context.result);
}
}

return context;
};
};

// 使用示例
app.service('posts').hooks({
before: {
all: [authenticate('jwt')],
find: [checkResourcePermission('posts')],
get: [checkResourcePermission('posts')],
create: [checkResourcePermission('posts')],
update: [checkResourcePermission('posts')],
patch: [checkResourcePermission('posts')],
remove: [checkResourcePermission('posts')]
},
after: {
all: [
filterFields(['id', 'title', 'content', 'author', 'createdAt', 'updatedAt'])
]
}
});

安全增强

1. 账户锁定机制

// src/hooks/account-security.js
const MAX_LOGIN_ATTEMPTS = 5;
const LOCK_TIME = 2 * 60 * 60 * 1000; // 2小时

const handleLoginAttempt = () => {
return async (context) => {
const { email } = context.data;

const user = await context.app.service('users').find({
query: { email, $limit: 1 }
});

if (user.total === 0) {
throw new Error('用户不存在');
}

const userData = user.data[0];

// 检查账户是否被锁定
if (userData.isLocked) {
throw new Error(`账户已被锁定,请在 \${new Date(userData.lockUntil).toLocaleString()} 后重试`);
}

// 将用户信息存储到 context 中,供后续使用
context.userData = userData;

return context;
};
};

const handleLoginFailure = () => {
return async (context) => {
const { userData } = context;

if (!userData) return context;

const updates = {
loginAttempts: userData.loginAttempts + 1
};

// 如果达到最大尝试次数,锁定账户
if (updates.loginAttempts >= MAX_LOGIN_ATTEMPTS) {
updates.lockUntil = new Date(Date.now() + LOCK_TIME);
}

await context.app.service('users').patch(userData._id, updates);

throw new Error(`登录失败,剩余尝试次数: \${MAX_LOGIN_ATTEMPTS - updates.loginAttempts}`);
};
};

const handleLoginSuccess = () => {
return async (context) => {
const { user } = context.result;

// 清除登录尝试记录
await context.app.service('users').patch(user._id, {
loginAttempts: 0,
lockUntil: null,
lastLoginAt: new Date()
});

return context;
};
};

module.exports = {
handleLoginAttempt,
handleLoginFailure,
handleLoginSuccess
};

2. 密码安全策略

// src/hooks/password-security.js
const bcrypt = require('bcryptjs');

const validatePasswordStrength = () => {
return (context) => {
const { password } = context.data;

if (!password) return context;

const minLength = 8;
const hasUpperCase = /[A-Z]/.test(password);
const hasLowerCase = /[a-z]/.test(password);
const hasNumbers = /\d/.test(password);
const hasSpecialChar = /[!@#$%^&*(),.?":{}|<>]/.test(password);

const errors = [];

if (password.length < minLength) {
errors.push(`密码长度至少 \${minLength} 位`);
}

if (!hasUpperCase) {
errors.push('密码必须包含大写字母');
}

if (!hasLowerCase) {
errors.push('密码必须包含小写字母');
}

if (!hasNumbers) {
errors.push('密码必须包含数字');
}

if (!hasSpecialChar) {
errors.push('密码必须包含特殊字符');
}

if (errors.length > 0) {
throw new Error(`密码强度不足: \${errors.join(', ')}`);
}

return context;
};
};

const preventPasswordReuse = (historyCount = 5) => {
return async (context) => {
const { password } = context.data;
const { user } = context.params;

if (!password || !user) return context;

// 获取用户的密码历史
const passwordHistory = user.passwordHistory || [];

// 检查是否重复使用了最近的密码
for (const oldPassword of passwordHistory.slice(-historyCount)) {
if (await bcrypt.compare(password, oldPassword)) {
throw new Error(`不能重复使用最近 \${historyCount} 次的密码`);
}
}

// 更新密码历史
const newHistory = [...passwordHistory, user.password].slice(-historyCount);
context.data.passwordHistory = newHistory;

return context;
};
};

module.exports = {
validatePasswordStrength,
preventPasswordReuse
};

总结

通过这篇文章,我们深入学习了 Feathers.js 的认证与授权系统:

认证系统基础

  • 认证 vs 授权的区别
  • JWT 和本地认证策略
  • 用户模型设计

高级认证策略

  • 自定义认证策略
  • OAuth 第三方登录
  • 多因素认证(MFA)

权限控制系统

  • 基于角色的访问控制(RBAC)
  • 资源级权限控制
  • 字段级权限过滤

安全增强措施

  • 账户锁定机制
  • 密码安全策略
  • 登录失败处理

掌握了这些知识,你就能够构建一个安全可靠的 API 系统,包括:

  • 多层次的身份验证
  • 细粒度的权限控制
  • 完善的安全防护机制
  • 用户友好的安全体验

下一篇文章,我们将学习 Feathers.js 的实时功能,看看如何构建强大的 WebSocket 应用。


相关文章推荐:

有问题欢迎留言讨论,我会及时回复大家!