必须同步撤掉基表权限,因为视图只是查询模板而非访问控制开关;用户持有基表SELECT权限即可绕过视图直接查询所有字段,包括敏感信息。

只在 CREATE VIEW 的 SELECT 子句里不写敏感字段,它就真的查不到——但光这么做毫无安全意义,必须同步撤掉基表权限。
视图里不写敏感字段,为什么还不够?
视图只是查询模板,不是访问控制开关。用户只要还持有对基表的 SELECT 权限,就能绕过视图直接执行 SELECT * FROM users,一次拿到所有字段,包括 password_hash、ssn、salary。
- MySQL 和 PostgreSQL 的权限是白名单制,旧权限不会自动失效
- 验证是否生效:运行
SHOW GRANTS FOR 'app_user'@'%',输出里不能出现ON mydb.users - 必须先执行
REVOKE SELECT ON mydb.users FROM 'app_user'@'%',再GRANT SELECT ON mydb.v_safe_users TO 'app_user'@'%' - PostgreSQL 还要额外
REVOKE USAGE ON SCHEMA public FROM app_user,否则可能通过public.users显式引用绕过
为什么必须显式列出字段,禁用 SELECT *?
SELECT * 在视图中极其危险:它会把基表当前所有列(包括后续新增的 internal_notes、audit_log)全兜进来,哪怕你只在应用里查 name 和 email,元数据里照样暴露 password_hash 字段名和类型。
- MySQL 8.0+ 中,基表加新列后,
SELECT *视图会自动包含它;显式列表则完全免疫 - PostgreSQL 要求含表达式时必须声明列名,例如:
CREATE VIEW v AS SELECT email AS user_email FROM users - 正确写法:
CREATE VIEW user_safe AS SELECT id, name, email, created_at FROM users
脱敏字段要重命名,别留语义陷阱
写 CONCAT(LEFT(phone, 3), '****', RIGHT(phone, 4)) AS phone,列名还是 phone。前端代码可能直接拿这个值做短信校验,ORM 可能反向映射更新原字段,审计工具也识别不出这是掩码值。
- 必须重命名为
phone_masked或mobile_anonymized,传递明确语义信号 - NULL 值必须预处理:
CONCAT(LEFT(IFNULL(TRIM(phone), ''), 3), '****', RIGHT(IFNULL(TRIM(phone), ''), 4)) AS phone_masked -
WHERE条件别依赖phone_masked——它是运行时计算值,无法走索引,会导致全表扫描 - 别让视图变成可更新的“后门”:单表、无聚合、无表达式的视图在 MySQL/PostgreSQL 中默认允许
UPDATE,用户可能意外恢复软删除记录
MySQL 视图脱敏必须避开的硬伤
MySQL 对非确定性函数极其严格,稍有不慎就会报错或引入风险。
- 禁用
NOW()、CURRENT_USER()、RAND()——CREATE VIEW会直接失败,报ERROR 1351 -
SQL SECURITY DEFINER是硬性要求,别用默认INVOKER;否则调用者权限过高可能导致越权读取 - 别碰
INVISIBLE COLUMN当作脱敏手段,它不阻止SELECT *或权限绕过 - 每个脱敏表达式都要包
IFNULL(, '')和TRIM(),防止空格或 NULL 导致整个字段为空,破坏业务逻辑校验
真正起作用的从来不是视图定义本身,而是那条被严格执行的 REVOKE 语句——它才是锁住门的那把钥匙。任何漏掉权限回收的视图,都只是纸糊的屏障。

















