SaaS用户修改资料看似简单 权限验证为什么绝不能只靠前端
在SaaS系统中,“用户修改自己的资料”看起来是一个非常简单的功能。前端显示姓名、邮箱、头像和其他资料,用户点击保存,浏览器向服务器发送一个API请求,数据库更新对应记录。真正容易出问题的地方恰恰在这个“对应”两个字上:服务器究竟如何确认这个用户修改的是自己的资料,而不是通过修改请求里的ID,把目标换成了另一个用户甚至另一个组织的数据。
假设一个SaaS系统存在这样的接口:
PUT /api/users/10086/profile
当前登录用户是10086时,一切正常。但如果用户把请求改成:
PUT /api/users/10087/profile
服务器仍然按照URL中的用户ID执行更新,而没有验证10087与当前登录身份之间的关系,那么这个系统即使前端已经隐藏了其他用户的编辑按钮,也仍然存在典型的越权漏洞。攻击者甚至不需要破解密码,只需要绕过浏览器界面,直接构造HTTP请求。
这就是前端权限控制和后端授权控制之间最根本的区别。前端负责告诉用户“你可以看到什么、点击什么”,后端负责决定“这个请求究竟能不能执行”。浏览器运行的是攻击者可以控制的代码,JavaScript、请求参数、Local Storage、Cookie之外的普通客户端状态以及页面上的权限判断都不能作为最终安全边界。
因此,前端可以根据当前用户权限隐藏“编辑”“删除”“导出”等按钮,但服务器收到请求以后必须重新进行授权判断。攻击者可以使用浏览器开发者工具、Postman或者自己编写HTTP客户端直接调用API,根本不需要经过前端按钮。
身份认证和权限授权也必须分开。用户登录成功,说明服务器已经能够确认这个请求属于某个身份,但这并不意味着这个身份可以执行所有操作。Access Token、Session或者JWT解决的是认证上下文和访问凭证问题,授权仍然需要由服务器根据用户、组织、角色、权限以及目标资源进行判断。
以JWT为例,服务器不能因为客户端提交了一个看起来像这样的Claim就直接相信:
{
"user_id": 10086,
"role": "admin"
}
JWT的安全性首先依赖服务端正确验证签名以及issuer、audience、有效期等必要条件。即使JWT完全合法,也只说明服务器接受其中的身份和授权上下文;服务器仍然需要判断这个用户当前是否有权访问指定资源。一个合法用户的合法Token,同样可以被拿来攻击资源级授权存在漏洞的API。
SaaS系统尤其需要处理组织和租户问题。假设同一个用户同时属于公司A和公司B,在A公司是管理员,在B公司只是普通成员,那么“用户角色”就不能简单理解成一个永远固定的全局属性。更加合理的数据模型应该把User、Organization、Membership和Role分开。用户进入某个组织以后,服务器根据当前组织上下文找到对应Membership,再确定这个用户在这个组织中的权限。
因此,客户端即使发送:
{
"organization_id": 2002
}
也不能代表用户已经获得组织2002的访问权。服务器必须自己确认当前用户是否属于这个组织。客户端提供的是访问目标,而不是授权证明。
资源级授权则解决另一个问题。假设用户确实属于当前组织,而且拥有profile:write权限,服务器还需要判断他修改的究竟是哪一个资源。如果接口允许管理员修改其他员工资料,那么需要根据角色和组织规则决定;如果接口只允许用户修改自己的资料,则服务器甚至不需要相信客户端提交的user_id,完全可以直接使用认证上下文中的当前用户ID。
例如,一个只允许修改本人资料的接口,在设计上更合理的思路是:
$currentUserId = $auth->userId();
$user = $userRepository->findById($currentUserId);
if (!$user) {
throw new NotFoundException();
}
$user->updateProfile($input);
$userRepository->save($user);
而不是:
$userId = $request->input('user_id');
$user = $userRepository->findById($userId);
$user->updateProfile($input);
后者的问题并不在于SQL写错了,而在于把“客户端要求修改哪个用户”错误地当成了“客户端有权修改哪个用户”。
对于多租户数据,这个问题还要继续向数据库访问层延伸。假设CRM系统有一个customers表,记录客户资料。如果查询只按照:
SELECT * FROM customers WHERE id = :id
那么业务代码一旦忘记检查租户关系,就可能让一个组织读取另一个组织的客户。更加稳健的设计会把租户范围作为数据访问边界的一部分,例如:
SELECT *
FROM customers
WHERE id = :id
AND organization_id = :organization_id
这样做并不是说SQL条件本身就等于完整权限系统,而是让数据访问层至少不会轻易跨越租户边界。业务授权仍然需要判断当前用户有没有访问这条记录的资格。
这也是SaaS权限设计中非常重要的一层防御。API入口可能已经检查用户是否登录,业务服务可能已经检查用户是否具有某项权限,但数据访问层仍然应该尽量保证查询范围不会脱离当前租户。这样可以降低某个业务接口漏掉租户条件之后造成大规模数据泄露的风险。
对于安全要求较高的系统,还可以进一步利用数据库提供的Row-Level Security等机制建立额外的数据隔离。例如PostgreSQL可以根据数据库会话上下文限制某个用户能够看到哪些行。它适合作为纵深防御,但并不意味着所有SaaS都必须把全部授权逻辑搬进数据库。数据库负责的数据边界、应用负责的业务规则,两者最好保持清晰职责。
RBAC可以解决角色和权限之间的关系,但也不能单独解决所有越权问题。例如系统可以规定employee拥有profile:write权限,可这个权限究竟代表“修改自己的资料”,还是“修改组织内所有员工的资料”,需要结合资源范围解释。如果把所有权限都设计成简单的“能”或者“不能”,最终很容易得到一个角色数量不断膨胀的系统。
所以实际SaaS往往需要把RBAC和资源级授权结合起来。RBAC负责回答“这个角色允许执行什么操作”,资源授权负责回答“这个操作可以作用在哪些对象上”。再复杂一点,还需要加入资源状态、部门关系、所有权、时间条件或者其他属性,这时才有必要考虑ABAC或者更加细粒度的Policy Engine。
权限检查的位置也需要合理划分。API Gateway可以验证Token、处理基础认证、限流和一些粗粒度访问控制,但它通常不应该承担所有业务授权。网关不知道某个CRM客户是否属于当前销售,也不知道一张订单是不是已经进入结算状态。只有真正掌握业务资源的服务,才能判断某个具体操作是否应该被允许。
业务服务因此应该成为资源授权的核心位置。例如:
身份认证
用户与组织Membership
角色与Permission
资源所属关系
业务状态
最终授权决定
这里最重要的并不是让所有层重复执行完全相同的checkPermission(),而是让每一层承担不同的安全职责。认证系统确认身份,业务服务判断操作权限和资源关系,数据访问层强制租户范围,数据库可以提供额外隔离,网关保护系统入口。
权限撤销也是大型SaaS经常出现的问题。假设管理员刚刚取消了某个员工的管理员权限,如果系统把权限长期缓存到Redis,或者Access Token携带了很长的有效期,那么数据库中的角色已经发生变化,旧权限却可能仍然继续有效。系统因此需要设计权限缓存失效、Token生命周期、Session撤销以及高风险操作重新认证等机制。
对于特别敏感的操作,例如删除组织、批量导出客户、修改管理员、生成API密钥或者改变账单信息,还可以要求重新认证、MFA或者其他step-up authentication。这不是因为普通权限检查“不够安全”,而是因为这些操作的风险明显高于普通资料修改。
审计日志则负责解决“发生以后怎么办”。系统至少应该记录关键授权事件的操作者、组织、资源、操作、时间和结果。对于批量删除、数据导出、权限提升等操作,还应该能够追踪完整的操作上下文。日志不能记录密码、Access Token、Refresh Token等敏感凭证,否则用于调查安全事件的系统反而可能制造新的泄露风险。
前端权限控制仍然有价值,只是它的职责是用户体验而不是安全。比如普通成员没有删除组织的权限,前端隐藏删除按钮能够减少误操作,也能够让界面更加清晰。但攻击者直接发送DELETE /api/organizations/123时,服务器必须再次判断权限。即使前端完全失效,后端授权也应该继续成立。
同样,客户端发送的role、permission、is_admin、organization_id、owner_id等字段,都不能自动成为可信授权依据。服务器可以读取这些参数作为业务输入,但最终权限必须来自服务器能够验证的身份和数据关系。把客户端提交的数据当成权限来源,是SaaS越权漏洞最常见的设计错误之一。
对于一个成熟的SaaS系统,权限验证应该形成一条完整的授权链,而不是在前端放几个if判断。用户首先通过认证系统建立身份,服务器确定其当前组织Membership和角色,然后根据具体Action检查Permission,再针对目标Resource判断所有权、租户关系和业务状态,最后在数据访问层确保查询不会越过租户边界。高风险操作还可以增加重新认证和审计。
用户修改自己的资料只是一个很小的功能,却能够暴露整个SaaS权限体系是否设计正确。如果服务器只相信浏览器提交的用户ID,那么一个简单的Profile API就可能成为越权入口;如果服务器始终把认证身份、组织关系和资源授权作为可信依据,即使攻击者完全绕过前端,也只能获得服务器明确授予他的能力。
SaaS安全的核心从来不是让前端页面看起来“没有权限”,而是让服务器在每一次敏感操作发生时,都能够回答三个问题:这个请求是谁发出的,这个用户在当前组织拥有什么权限,以及他试图操作的这个具体资源是否属于他的授权范围。前端可以帮助用户正确操作,后端才拥有最终决定权;数据库和基础设施则应该尽可能让一次授权遗漏不会直接变成整个租户的数据灾难。
用户修改自己的资料很简单,但 SaaS 权限验证不能只依靠前端代码
喜欢这篇报道?
使用下面的功能,方便以后继续阅读和分享 MNewsTV
关于文章收藏
收藏不需要注册帐号,收藏信息仅保存在当前浏览器中。
删除收藏请进入「我的收藏」进行管理。
捐助(Paypal): https://www.paypal.me/observeccp 订阅中国观察电报 Telegram : https://t.me/s/ObserveCCP