DEBUG.WIKI
京东云;让 AI 进入真实业务;零售 · 物流 · 客服 · 硬件;查看方案 →
晚安云;香港 CN2 ¥15/月
雨云;给项目一台云服务器

MySQL 用户名含冒号被截断:Go 驱动直接接收 Config

go-sql-driver/mysql 的 DSN 会把用户名中的首个冒号当作密码分隔符。1.10.1 可通过 Config、NewConnector 与 sql.OpenDB 保留完整用户名,避免 FormatDSN 后再次解析。

Go 程序使用包含冒号的 MySQL 用户名,连接时却像用了另一组凭据。若字符串交给了 mysql.ParseDSN() 或 sql.Open("mysql", dsn),先检查用户名是否被第一个冒号截断。

例如,用户名本来是 tenant:reader,拼成 tenant:reader:secret@tcp(db.example:3306)/app 后,解析器会把 tenant 作为用户名,把 reader:secret 作为密码。账号在到达服务端前已经换了含义。

字符串格式里的冒号已经承担分隔作用

go-sql-driver/mysql 1.10.1 的正式解析实现,在凭据部分寻找第一个 :,它前面的内容进入 User,后面的内容进入 Passwd。这里没有一层可供用户名使用的 URL 百分号解码;把冒号换成 %3A,不能假定服务端最终收到的仍是原用户名。

先核对应用进入驱动的真实路径。有些配置工具会先处理连接字符串,另一些直接使用驱动的 Config。只输出最终用户名是否等于预期、网络类型和目标库等必要信息,不把整个 DSN、密码或其他认证材料写入日志。真正的密码错误、账号授权或网络失败仍需分别处理。

保留结构化配置,直接建立 Connector

用户名需要保留冒号时,避开这次字符串拆分。使用 mysql.NewConfig() 创建默认配置,或沿用项目已有的 Config,然后交给 mysql.NewConnector() 和 sql.OpenDB()。

下面是接收既有配置的辅助函数。base 应已按项目原流程设好网络、地址、数据库、TLS 和超时;user、password 来自已有凭据管理入口:

import (
    "database/sql"

    "github.com/go-sql-driver/mysql"
)

func OpenDatabase(base *mysql.Config, user, password string) (*sql.DB, error) {
    cfg := base.Clone()
    cfg.User = user
    cfg.Passwd = password

    connector, err := mysql.NewConnector(cfg)
    if err != nil {
        return nil, err
    }
    return sql.OpenDB(connector), nil
}

base 必须非 nil。克隆后只改凭据字段,避免调用方仍共享的配置被意外修改;需要新配置时先调用 NewConfig,再明确填写实际 Net、Addr 和 DBName。不能为迁移入口而丢掉原来已审定的 TLS、字符集、时区和超时设置。

不要再接一遍 cfg.FormatDSN() 并传给 sql.Open()。1.10.1 的 FormatDSN 仍直接写入用户名;重新解析字符串会回到同一个分隔歧义。结构化配置的价值在于让 Connector 直接接收字段,而不只是让拼接字符串看起来更整齐。

检查连接真正使用了哪个账号

sql.OpenDB() 返回连接池不等于认证成功,它可以到首次使用时才建立连接。在项目已有的受控连接检查中,使用原来的上下文、超时和只读验证,确认包含冒号的用户名完整传入,能连接预期数据库,并且普通无冒号账号仍正常。不要只把“函数没有返回错误”当作登录成功。

若调用链只允许提供 DSN 字符串,先评估集成层是否能接收 Connector 或 Config。更换或重命名数据库账号会影响其他消费者,不应为了绕开一段字符串解析而直接在共享数据库中修改身份。此处处理的是驱动入口的表达能力,不承诺所有认证失败都能通过同一修改解决。