OceanBase 中,"谁能连上数据库"由两层机制共同限制:
ob_tcp_invited_nodes(创建租户时设置,或事后修改),约束整个租户可接受连接的来源 IP;'user'@'host' 中的主机名部分(MySQL 模式创建用户时指定),约束该用户可登录的来源 IP,例如
'app_reader'@'10.0.0.21' 只允许来自 10.0.0.21 的连接。两层各自独立配置,由此引出两个问题:
参数性质与默认值
ob_tcp_invited_nodes 是
租户级系统变量(GLOBAL_SCOPE),不是配置项(config parameter):只能用系统变量语法
SET GLOBAL 修改(配置项语法
ALTER SYSTEM SET 会报
5099 (42000): System config unknown),修改对后续新连接立即生效。'127.0.0.1,::1',即新租户在未显式设置时,除本机回环地址外一切来源都会被拒绝——这是建租户时容易忽略的默认行为。租户白名单支持多种灵活形式
-- 精确 IP
SET
GLOBAL ob_tcp_invited_nodes=
'192.168.5.87';
-- 通配符
SET
GLOBAL ob_tcp_invited_nodes=
'192.168.5.%';
-- CIDR
SET
GLOBAL ob_tcp_invited_nodes=
'192.168.1.0/24';
-- 混合逗号列表
SET
GLOBAL ob_tcp_invited_nodes=
'192.168.5.%,127.0.0.1';
底层的匹配规则
% 匹配任意长度、
_ 匹配单个字符)与 IP 地址匹配(精确 IP 或 CIDR 网段,如
192.168.1.0/24);192.168.5.%)、CIDR、混合逗号列表。在什么时候起作用?
连接处理的主流程为:解析握手包并确定客户端 IP → 连接校验(租户白名单在此步骤执行)→ 创建 session → 身份认证(账号检查在此步骤执行)。
ob_tcp_invited_nodes,与客户端 IP 做匹配;不匹配即拒绝连接,返回
OB_ERR_NO_PRIVILEGE,并在日志中记录
client is not invited into this tenant;root@sys 从
127.0.0.1/
::1 登录跳过检查(防止误设变量后无法登录);UNIX socket 连接跳过;
匹配不上的典型错误表现:
ER_SPECIFIC_ACCESS_DENIED_ERROR(1227)、SQLSTATE
42501。
MySQL 租户中,一个用户由"用户名 + 主机名"二元组唯一确定,主机名是用户身份的一部分(存储于
mysql.user 视图,底层为
__all_user 等表的 USER_NAME/HOST_NAME 字段)。
匹配规则:与租户白名单共用同一套匹配逻辑,即同样支持
%、
_ 通配与精确 IP、CIDR。
检查时机(
src/share/schema/ob_schema_getter_guard_user.cpp)
账号检查的处理逻辑:
OB_PASSWORD_WRONG;同名多条记录时,
任一主机名匹配且该记录密码校验通过即登录成功。
匹配不上的典型错误表现:
ER_ACCESS_DENIED_ERROR(1045)、SQLSTATE
42000,报错文案为
Access denied for user 'u'@'ip' (using password: ...)。
{client_ip ∈ 租户白名单} ∩
{client_ip ∈ 该用户名下所有主机名记录的并集};1227 (42501) 而不是
1045——说明请求在到达账号检查之前已被拦下,与源码顺序一致。| 项 | 值(实测) |
|---|---|
| 数据库版本 |
5.7.25-OceanBase-v4.4.2.3(
VERSION()) |
| 以下语句手动执行,共三次连接,分别覆盖账号层拦截、租户层拦截、检查顺序三种情况,可以快速验证上面的原理。 |
|
窗口 1:以 root@msq 登录,全程在该会话执行 SQL。
SELECT
NOW(
6)
AS 开始;
SELECT @@GLOBAL.ob_tcp_invited_nodes;
-- 主机名为保留地址,必不匹配来源 IP
CREATE
USER
'u_t'@
'192.0.2.2'
IDENTIFIED
BY
'Test_123';
验证 1 账号层拦截:白名单保持原值(放行全部来源),仅账号主机名不匹配。窗口 2 另开新连接执行:
date
'+%F %T 验证 1 连接(白名单原值)'
obclient -h192.168.5.86 -P2883 -u
'u_t@msq#ob435' -p
'Test_123' -e
'SELECT 1;'
# 预期 ERROR 1045 (42000): Access denied for user 'u_t'@'192.168.5.87' (using password: YES)
# 判定:白名单 % 放行,账号主机名不匹配 —— 账号层拦截
验证 2 租户层拦截:先在窗口 1 将白名单限制为不含实际来源的保留地址:
SELECT
NOW(
6)
AS 限制白名单时刻;
SET
GLOBAL ob_tcp_invited_nodes=
'192.0.2.1';
-- 保留地址,不含任何真实来源
再在窗口 2 另开新连接,连 root 自己(root 主机名放行任意来源,账号层不构成拦截):
date
'+%F %T 验证 2 连接(白名单已受限)'
obclient -h192.168.5.86 -P2883 -u
'root@msq#ob435' -p -e
'SELECT 1;'
# 预期 ERROR 1227 (42501): Access denied —— 拦截发生在账号层之前
验证 3 检查顺序:白名单保持受限,连接 u_t,两层均不匹配。窗口 2 执行:
date
'+%F %T 验证 3 连接(白名单仍受限)'
obclient -h192.168.5.86 -P2883 -u
'u_t@msq#ob435' -p
'Test_123' -e
'SELECT 1;'
# 预期仍为 ERROR 1227 (42501) 而非 1045 —— 若账号检查在前应报 1045,报 1227 即白名单先查
窗口 1 收尾(本会话不受白名单影响,改完随时可还原):
SELECT
NOW(
6)
AS 还原白名单时刻;
SET
GLOBAL ob_tcp_invited_nodes=
'%';
-- 还原为最初记下的原值
DROP
USER
IF
EXISTS
'u_t'@
'192.0.2.2';
以上语句完整执行一遍,输出如下
# ===== 窗口 1:准备(root@msq 登录会话内执行) ===== obclient [(none)]> SELECT NOW(6) AS 开始; +----------------------------+ | 开始 | +----------------------------+ | 2026-09-30 18:30:15.120456 | +----------------------------+ 1 row in set (0.00 sec) obclient [(none)]> SELECT @@GLOBAL.ob_tcp_invited_nodes; +-------------------------------+ | @@GLOBAL.ob_tcp_invited_nodes | +-------------------------------+ | % | +-------------------------------+ 1 row in set (0.00 sec) obclient [(none)]> CREATE USER 'u_t'@'192.0.2.2' IDENTIFIED BY 'Test_123'; Query OK, 0 rows affected (0.05 sec) # ===== 窗口 2:验证 1 ===== [root@192 ~]# date '+%F %T 验证 1 连接(白名单原值)' 2026-09-30 18:31:02 验证 1 连接(白名单原值) [root@192 ~]# obclient -h192.168.5.86 -P2883 -u'u_t@msq#ob435' -p'Test_123' -e 'SELECT 1;' ERROR 1045 (42000): Access denied for user 'u_t'@'192.168.5.87' (using password: YES) # ===== 窗口 1:限制白名单 ===== obclient [(none)]> SELECT NOW(6) AS 限制白名单时刻; +----------------------------+ | 限制白名单时刻 | +----------------------------+ | 2026-09-30 18:31:38.663120 | +----------------------------+ 1 row in set (0.00 sec) obclient [(none)]> SET GLOBAL ob_tcp_invited_nodes='192.0.2.1'; Query OK, 0 rows affected (0.03 sec) # ===== 窗口 2:验证 2 ===== [root@192 ~]# date '+%F %T 验证 2 连接(白名单已受限)' 2026-09-30 18:31:52 验证 2 连接(白名单已受限) [root@192 ~]# obclient -h192.168.5.86 -P2883 -u'root@msq#ob435' -p -e 'SELECT 1;' Enter password: ERROR 1227 (42501): Access denied # ===== 窗口 2:验证 3 ===== [root@192 ~]# date '+%F %T 验证 3 连接(白名单仍受限)' 2026-09-30 18:32:07 验证 3 连接(白名单仍受限) [root@192 ~]# obclient -h192.168.5.86 -P2883 -u'u_t@msq#ob435' -p'Test_123' -e 'SELECT 1;' ERROR 1227 (42501): Access denied # ===== 窗口 1:收尾还原 ===== obclient [(none)]> SELECT NOW(6) AS 还原白名单时刻; +----------------------------+ | 还原白名单时刻 | +----------------------------+ | 2026-09-30 18:32:20.451073 | +----------------------------+ 1 row in set (0.00 sec) obclient [(none)]> SET GLOBAL ob_tcp_invited_nodes='%'; Query OK, 0 rows affected (0.03 sec) obclient [(none)]> DROP USER IF EXISTS 'u_t'@'192.0.2.2'; Query OK, 0 rows affected (0.04 sec)
若窗口 1 会话中断且白名单未还原,按上文白名单误锁后的人工恢复处理。
结果分析:实际报错与预期一致时,三次连接对应如下结论。
验证 1 报 1045:白名单保持原值,连接已通过租户层检查,拦截发生在账号匹配——账号主机名
192.0.2.2 与实际来源
192.168.5.87 不一致。账号层检查与密码认证共用错误码 1045,本实验以密码正确、仅主机名不匹配的方式排除了密码因素。
验证 2 报 1227:root 主机名为
%,放行任意来源,若仅执行账号匹配不会被拒绝;实际在进入账号检查之前即被拦截,且被拦截的是设置白名单的 root 自己——租户白名单是先于账号检查的独立检查,对租户内全部账号生效,与账号配置无关。
验证 3 报 1227 而非 1045:两层同时不匹配时返回的是白名单的错误码。若账号检查在前此处应报 1045,实际报 1227,证明租户白名单先于用户主机名执行。
验证 1 与验证 2 合看,即开篇第二个问题(包含关系以谁为准)的答案:
| 验证 | 白名单 | 账号主机名 | 包含关系 | 结果 | 由谁决定 |
|---|---|---|---|---|---|
| 验证 1 |
%(大) |
192.0.2.2(小) |
白名单 ⊇ 主机名 | 1045 | 更小的账号层 |
| 验证 2 |
192.0.2.1(小) |
%(大) |
白名单 ⊆ 主机名 | 1227 | 更小的白名单层 |
更严的一层决定结果,报错码直接指认该层——实际可连接范围恒为两层允许范围的交集。
总结:三项验证结果与前面的原理一致——MySQL 模式租户的建连检查按“租户白名单 → 用户主机名 → 密码认证”顺序执行,任一层不匹配即中断;错误码可直接定位拦截层:1227 为租户白名单层,1045 为账号层(主机名不匹配或密码错误)。
如果在操作过程中遇到问题,欢迎在评论区留言交流,后续我们会持续分享更多实战运维干货,记得 关注不迷路 ,下次见~
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/70047467/viewspace-3188633/ ,如需转载,请注明出处,否则将追究法律责任。