只在create view的select子句中排除敏感字段即可隐藏它们,但必须同步撤回基表select权限、仅授予视图权限,显式列出字段禁用select*,脱敏字段重命名(如phone_masked),预处理null值,并防止视图可更新。

只在 CREATE VIEW 的 SELECT 子句里不写敏感字段,它就真的查不到——但光这么做毫无安全意义,必须同步撤掉基表权限。
CREATE VIEW 必须显式列出字段,禁用 SELECT *
视图不会自动过滤列,它只暴露你白纸黑字写进 SELECT 的那些。用 SELECT * 就等于把基表所有列(包括后续新增的 internal_notes、salary_revision_date)全兜进来,哪怕你只在应用里查 name 和 email,元数据里照样暴露 password_hash 字段名和类型。
- 正确写法:
CREATE VIEW user_safe AS SELECT id, name, email, created_at FROM users; - MySQL 中若基表加了新列,
SELECT *视图会自动包含它;显式列表则完全免疫 - PostgreSQL 要求含表达式时必须声明列名,比如:
CREATE VIEW v AS SELECT email AS user_email FROM users;
必须 REVOKE 基表权限,否则视图形同虚设
用户能直查 users 表,SELECT * FROM users 一次就全暴露了。视图只是查询模板,真正起作用的是权限回收动作。
- 先执行:
REVOKE SELECT ON users FROM app_user;(PostgreSQL/MySQL 通用) - 再执行:
GRANT SELECT ON user_safe TO app_user; - PostgreSQL 额外要:
REVOKE USAGE ON SCHEMA public FROM app_user;,否则可能通过public.users显式引用绕过 - 验证方式:
SHOW GRANTS FOR 'app_user'@'%';,输出里不能出现ON mydb.users
脱敏字段必须重命名,别留语义陷阱
写 CONCAT(LEFT(phone, 3), '****', RIGHT(phone, 4)) AS phone,列名还是 phone。前端代码可能直接拿这个值做短信校验,ORM 可能反向映射更新原字段,审计工具也识别不出这是掩码值。
- 强制后缀:
AS phone_masked或AS email_domain,传递明确语义信号 - NULL 值必须预处理:
CONCAT(LEFT(IFNULL(TRIM(phone), ''), 3), '****', RIGHT(IFNULL(TRIM(phone), ''), 4)) AS phone_masked - WHERE 条件别依赖
phone_masked——它是运行时计算值,无法走索引,会导致全表扫描
别让视图变成可更新的“后门”
单表、无聚合、无表达式的视图,在 MySQL 和 PostgreSQL 中默认允许 UPDATE。用户虽看不到 is_deleted 列,但通过 UPDATE user_safe SET name = 'xxx' WHERE id = 1,可能意外恢复软删除记录。
- MySQL:默认可更新,加
WITH CHECK OPTION仅校验条件,不阻止写入 - PostgreSQL:查
pg_views.definition确认是否含聚合或表达式;如需只读,显式用CREATE VIEW ... WITH LOCAL CHECK OPTION并配合GRANT撤回INSERT/UPDATE - 最稳妥做法:不授写权限,且视图定义避开可更新结构(比如加
CASE WHEN ... END AS status_masked)
最容易被忽略的不是怎么写视图,而是忘了检查 DEFINER 权限是否越界、角色是否激活、或者某个中间件账号悄悄继承了基表权限——这些地方一漏,前面所有操作都归零。










