应用账号最小权限实践:读写账号、报表账号、运维账号分层
应用能连上数据库之后,很多团队为了省事,直接给应用用高权限账号。短期看问题少,长期风险很大:误删表、越权查询、测试代码写入生产、报表任务拖慢核心交易、审计时无法区分谁做了什么。
本文围绕金仓数据库应用接入,设计一套简单但实用的账号分层方案:应用读写账号、报表只读账号、运维管理账号分开使用,并通过最小权限控制降低风险。 
@toc
一、为什么应用不能用高权限账号
其实啊。高权限账号的问题。不在于它"能做更多事"。那问题在哪呢?问题在于它让应用有了不该有的能力。咱们来看看:
- 应用有漏洞的话。可能就被放大成数据库层级里的风险了。
- 代码有 Bug。可能不小心把表给删了。
- 报表 SQL 可能去访问一些敏感表。
- 审计日志没法区分。哪个是业务访问。哪个是人工操作的。
- 密码一旦泄漏。影响范围就太大了。
那么生产环境的应用账号。通常来说。应该只拥有业务跑起来需要的最小权限就行了。
二、账号分层设计
咱们来看个示例分层:
| 账号 | 用途 | 权限特点 |
|---|---|---|
app_user |
业务应用读写 | 只访问业务 Schema 下必要表 |
report_user |
报表和查询 | 只读权限 |
ops_user |
运维检查 | 只允许查看必要状态和执行受控维护 |
| 管理员账号 | 建库、建用户、授权 | 不给应用配置使用 |
这篇咱们继续用第一篇已经建好的 kb_app。还有 shop。以及 app_user 和 report_user。如果你是直接看这篇的话。那你先回到第一篇。把基础库、账号还有测试表都弄好再说。
三、准备业务 Schema
连到业务库 kb_app 之后。咱们先看看业务 Schema 在不在:
sql
SELECT schema_name
FROM information_schema.schemata
WHERE schema_name = 'shop';
要是没返回结果。那你就再执行这个:
sql
CREATE SCHEMA shop;
接着咱们建一张商品表。专门用来演示权限的:
sql
CREATE TABLE shop.t_product (
product_id INT PRIMARY KEY,
product_name VARCHAR(100) NOT NULL,
price NUMERIC(12,2) NOT NULL,
status VARCHAR(20) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
插几条测试数据进去:
sql
INSERT INTO shop.t_product(product_id, product_name, price, status)
VALUES
(1, 'keyboard', 199.00, 'ON_SALE'),
(2, 'mouse', 99.00, 'ON_SALE');
四、确认应用账号和报表账号
app_user 和 report_user 这俩。咱们在第一篇已经建过了。通常来说。在生产环境的情况里。建账号啊。密码多复杂啊。还有密码多久过期啊。这些都应该按企业层级里的安全规范来弄。我不建议每篇文章都去重复建同名账号。
五、授予 Schema 使用权限
你光给表权限其实还不够。账号还得能进到目标 Schema 里面去才行:
sql
GRANT USAGE ON SCHEMA shop TO app_user;
GRANT USAGE ON SCHEMA shop TO report_user;
要是少了这个 Schema 使用权限的话。应用可能就会报对象不存在啊。或者权限不足这类的错。
六、给应用账号授予读写权限
sql
GRANT SELECT, INSERT, UPDATE, DELETE
ON shop.t_product
TO app_user;
要是应用需要建点临时对象。或者要写更多表的话。那你应该一个表一个表地去评估。我不建议直接把整个数据库的高权限都给它。
咱们来验证一下:
sql
-- 使用 app_user 连接后执行
SELECT * FROM shop.t_product;
INSERT INTO shop.t_product(product_id, product_name, price, status)
VALUES (3, 'monitor', 899.00, 'ON_SALE');

七、给报表账号授予只读权限
sql
GRANT SELECT
ON shop.t_product
TO report_user;
验证一下:
sql
-- 使用 report_user 连接后执行
SELECT * FROM shop.t_product;
接着咱们再试试往里写数据:
sql
INSERT INTO shop.t_product(product_id, product_name, price, status)
VALUES (4, 'speaker', 299.00, 'ON_SALE');

这里它应该报权限错误。报错了才是正确的结果。这就说明只读的边界已经管用了。
八、新表权限怎么处理
很多团队都遇到过这个情况。应用账号对老表是有权限的。但是建了新表之后。应用一访问就失败了。那为什么会这样呢?原因在于啊。权限往往仅仅只授给了已经存在的对象。新对象它是不会自动继承权限的。
处理这个情况一般有两类办法。
第一类就是每次建完表。你手动去授权:
sql
GRANT SELECT, INSERT, UPDATE, DELETE ON shop.新表名 TO app_user;
GRANT SELECT ON shop.新表名 TO report_user;
第二类是用 ALTER DEFAULT PRIVILEGES 这个东西。也就是让当前管理账号。在这个 Schema 下面。以后建的表。都自动授上指定的权限:
sql
-- 以 system 账号连接 kb_app 后执行
ALTER DEFAULT PRIVILEGES IN SCHEMA shop
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO app_user;
ALTER DEFAULT PRIVILEGES IN SCHEMA shop
GRANT SELECT ON TABLES TO report_user;
这里要注意一下哈。ALTER DEFAULT PRIVILEGES 这个命令。它只对执行这个命令的用户后面建的对象管用。对已经存在的表是没用的。也不影响别的账号建的表。通常来说。新环境初始化的时候跑一次就行了。后面建表就不用每次手动去授权了。
那么不管你用哪种方式。都要把"建表后授权"这个事。放到发布流程里去。在测试环境里。记得用应用的真实账号去验证一下权限到底生效没。
九、应用配置中使用低权限账号
开发环境里你可以这么配:
yaml
spring:
datasource:
url: jdbc:kingbase8://192.168.10.101:54321/kb_app
username: app_user
password: App_user_123
报表系统呢。你就用这个:
yaml
spring:
datasource:
url: jdbc:kingbase8://192.168.10.101:54321/kb_app
username: report_user
password: Report_user_123
千万别让报表系统去复用业务的写账号。这样的话。就算报表 SQL 写错了。它至少也不会直接把业务数据给改了。
十、权限排查常用 SQL
当应用提示权限不足的时候。你先确认一下当前的连接身份是谁:
sql
SELECT current_database(), current_user;
接着确认一下对象在不在:
sql
SELECT * FROM shop.t_product WHERE product_id = 1;
要是报错了。你就检查这几个方面:
- 是不是连错数据库了。
- SQL 里的 Schema 名写对了没。
- 当前用户有没有 Schema 的使用权限。
- 当前用户对目标表有没有权限。
权限这个问题啊。不要拿管理员账号测一下通了就算完事。你必须用应用的真实账号去验证。那才算数。
十一、账号安全建议
我建议大家这么来弄:
- 不同的系统用不同的账号。
- 不同的环境也用不同的账号。
- 读写的账号和只读的账号一定要分开。
- 给应用的账号。别给它建库、删库、还有超级权限这些。
- 密码千万别写到代码仓库里去。
- 有人离职了。或者项目下线了。系统迁移了。那账号要及时回收掉。
- 定期去看看。有没有那种长期不用的账号。
十二、小结
其实搞最小权限。真不是为了给开发找麻烦。那是为了啥呢?是为了把事故的影响。控制在一个合理的范围里。应用读写账号。报表只读账号。运维账号。还有管理员账号。咱们分层来用。这样不管哪一层出了问题。损害都不会蔓延到别的层去。每次上线之前啊。用真实的应用账号去连一下。查一下。写一下。再测一下权限失败的情况。这都是必须要做的验收动作。
那么下一篇呢。咱们来整理个连接异常排障手册。把拒连啊。超时啊。认证失败啊。空闲断开这些问题。集中起来处理一下。