设计授权架构:多客户端资源隔离的高效实现方案咨询
优化客户端资源隔离的授权方案(Node.js/Express + PostgreSQL)
针对你遇到的多层关联权限校验效率低、扩展性差的问题,这里提供一套更优的实现思路,既能简化业务SQL,又能确保客户端数据隔离:
一、核心优化方向
- 扁平化数据库权限标识:给所有资源表(
shows/seasons/episodes)直接添加client_id字段,避免多层关联查询 - 集中式授权校验:用Express中间件统一处理权限逻辑,业务代码只专注资源操作
- JWT信息增强:在Token中携带
clientId(或结合缓存获取),减少授权时的用户信息查询
二、具体实现步骤
1. 调整数据库设计
给每个资源表添加client_id外键,同时用触发器自动维护该字段的一致性(避免手动插入错误):
-- 给shows表添加client_id(原本已有,作为基准) ALTER TABLE shows ADD CONSTRAINT fk_shows_client FOREIGN KEY (client_id) REFERENCES clients(id); -- 给seasons表添加client_id并创建触发器 ALTER TABLE seasons ADD COLUMN client_id INT; ALTER TABLE seasons ADD CONSTRAINT fk_seasons_client FOREIGN KEY (client_id) REFERENCES clients(id); CREATE OR REPLACE FUNCTION set_season_client_id() RETURNS TRIGGER AS $$ BEGIN SELECT client_id INTO NEW.client_id FROM shows WHERE id = NEW.show_id; RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER trigger_set_season_client_id BEFORE INSERT ON seasons FOR EACH ROW EXECUTE FUNCTION set_season_client_id(); -- 给episodes表添加client_id并创建触发器 ALTER TABLE episodes ADD COLUMN client_id INT; ALTER TABLE episodes ADD CONSTRAINT fk_episodes_client FOREIGN KEY (client_id) REFERENCES clients(id); CREATE OR REPLACE FUNCTION set_episode_client_id() RETURNS TRIGGER AS $$ BEGIN SELECT client_id INTO NEW.client_id FROM seasons WHERE id = NEW.season_id; RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER trigger_set_episode_client_id BEFORE INSERT ON episodes FOR EACH ROW EXECUTE FUNCTION set_episode_client_id();
这样所有资源的client_id会自动从上层关联资源继承,确保数据一致性。
2. 增强JWT内容
登录接口返回的JWT中,除了userId,额外加入clientId(如果用户的所属客户端可能变更,也可以在授权时从缓存/数据库查询):
// 登录生成JWT示例 import jwt from 'jsonwebtoken'; const generateToken = (user: { id: number; clientId: number }) => { return jwt.sign( { userId: user.id, clientId: user.clientId }, process.env.JWT_SECRET!, { expiresIn: '24h' } ); };
3. 编写通用权限校验中间件
创建一个可复用的中间件,根据资源类型自动校验用户与资源的客户端归属:
import { Request, Response, NextFunction } from 'express'; import { pool } from './db'; // 你的PostgreSQL连接池 type ResourceType = 'show' | 'season' | 'episode'; export const checkResourcePermission = (resourceType: ResourceType) => { return async (req: Request, res: Response, next: NextFunction) => { const resourceId = req.params.id; const { clientId } = req.user; // 假设JWT解析后挂载到req.user // 映射资源类型到数据库表名 const tableMap = { show: 'shows', season: 'seasons', episode: 'episodes' }; const tableName = tableMap[resourceType]; try { // 查询资源所属客户端 const result = await pool.query( `SELECT client_id FROM ${tableName} WHERE id = $1`, [resourceId] ); if (result.rows.length === 0) { return res.status(404).json({ error: '资源不存在' }); } // 校验客户端匹配 if (result.rows[0].client_id !== clientId) { return res.status(403).json({ error: '无权限操作该资源' }); } next(); } catch (err) { console.error('权限校验失败:', err); return res.status(500).json({ error: '服务器内部错误' }); } }; };
4. 业务路由与控制器简化
在路由中引入中间件,业务控制器只需执行纯资源操作SQL:
// 路由示例 import express from 'express'; import { checkResourcePermission } from './middlewares/permission'; import { updateEpisodeTime } from './controllers/episode'; const router = express.Router(); // 先校验权限,再执行更新 router.post('/episodes/:id', checkResourcePermission('episode'), updateEpisodeTime);
// 控制器示例 export const updateEpisodeTime = async (req: Request, res: Response) => { const episodeId = req.params.id; const { time } = req.body; try { // 无需权限条件,中间件已校验通过 await pool.query( 'UPDATE episodes SET time = $1 WHERE id = $2', [time, episodeId] ); res.status(200).json({ message: '剧集时长更新成功' }); } catch (err) { console.error('更新失败:', err); res.status(500).json({ error: '更新剧集时长失败' }); } };
三、扩展性说明
- 若新增资源层级(比如季下新增“部分”):只需给新表添加
client_id字段、创建对应的触发器,再在中间件的tableMap中新增映射即可,无需修改现有业务代码 - 未来角色权限变更:可在中间件中扩展角色校验逻辑(比如从JWT或数据库获取用户角色,判断是否允许CRUD)
内容的提问来源于stack exchange,提问作者Danny Hobo
相关产品推荐
相关产品推荐

