Harbor DB 模式用户登录登出实战指南:从测试用例 1-02 到源码级认证链路解析
【免费下载链接】harborAn open source trusted cloud native registry project that stores, signs, and scans content.项目地址: https://gitcode.com/GitHub_Trending/ha/harbor
本文以 Harbor 测试用例库中的 1-02-DB-user-log-in-log-out.md 为主体,完整还原"DB 模式(db_auth)下非管理员用户登录/登出"这一用户管理场景的测试目标、环境要求、操作步骤与验收标准,并结合 UI 登录控制器、认证路由层、DB 认证器 等源码,讲清从表单提交到密码校验、会话建立的完整底层链路,帮助读者既掌握可复现的验收方法,又理解 Harbor 本地认证的实现原理。
1. 用例定位:验证 DB 模式下的登录登出能力
该用例出自 tests/testcases/Group1-user-management/ 用户管理测试组,属于手工/BAAT(BAT)测试脚本体系中的一环。其Purpose(测试目的)是:
验证在用户由 Harbor 本地数据库托管(DB 模式)时,一个非管理员用户能够正常登录并登出。
需要注意用例开头的显式提示:本用例必须使用非管理员用户执行,管理员账号有独立用例覆盖。测试关注的是普通用户的视角——登录后看到的仪表盘(dashboard)与导航栏应当是"非管理员"版本,不应出现任何管理员选项。
用例的 References 指向 Harbor 用户指南,Environment 一节明确了三项前提条件,下面逐项展开。
2. 环境准备与 auth_mode 配置
2.1 用例声明的环境要求
原用例 Environment 部分列出:
- 一个正在运行且可访问的 Harbor 实例;
- Harbor 使用本地数据库做认证,即
auth_mode设置为db_auth,用户数据存储在本地数据库中; - 一台安装了 Docker CLI 的 Linux 主机(Docker client),用于第 6、7 步的
docker login验证。
2.2auth_mode在源码中的定义与取值
auth_mode不是随意字符串,Harbor 对其做了严格元数据定义。在 src/lib/config/metadata/metadatalist.go 中可以看到其配置元数据:
{Name: common.AUTHMode, Scope: UserScope, Group: BasicGroup, EnvKey: "AUTH_MODE", DefaultValue: "db_auth", ItemType: &AuthModeType{}, Editable: false, Description: `The auth mode of current system, such as "db_auth", "ldap_auth", "oidc_auth"`},从中可以确认三个关键事实:
- 对应环境变量为
AUTH_MODE(通常出现在harbor.yml部署配置中); - 默认值就是
db_auth——即使未显式配置,系统也回退到本地数据库认证(见 src/lib/config/userconfig.go,AuthMode()在环境值为空时返回"db_auth"); - 该配置不可在运行时通过 API 在线编辑(
Editable: false),与认证模式切换属于部署期配置一致。
合法取值由 src/lib/config/metadata/type.go 中的AuthModeType.validate限定为:
| 取值 | 含义 |
|---|---|
db_auth | 本地数据库认证(本用例使用) |
ldap_auth | LDAP 目录认证 |
uaa_auth | UAA(Cloud Foundry)认证 |
http_auth | 外部 HTTP 认证服务 |
oidc_auth | OIDC 单点登录 |
此外,src/lib/config/metadata/metadatalist.go 还定义了PRIMARY_AUTH_MODE(默认false)用于多认证主模式语义,本用例不涉及。
2.3 测试基础设施
仓库tests/目录下提供了支撑这套用例运行的设施:tests/docker-compose.test.yml 用于拉起测试实例、tests/configharbor.py 承载测试参数,tests/ci/下的 ui_ut_run.sh、ut_run.sh 等脚本负责 CI 侧的 UI/单元测试执行。tests/testcases/中这些 Markdown 用例则是面向 QA 的 BAT 测试脚本正文。
3. 完整测试步骤(原用例 7 步完整继承)
以下 7 步完整继承原用例 Test Steps 一节(执行者为一个非管理员用户):
- 用用户名(username)登录 UI:非管理员用户在登录页输入用户名登录;
- 从 UI 登出;
- 用邮箱(email)登录 UI:同一用户改用邮箱作为登录名再次登录;
- 从 UI 登出;
- 错误凭据登录:使用错误的密码配合该用户的用户名或邮箱登录 UI,检查错误提示文案;
- Docker 客户端登录:在 Docker client 主机上执行
docker login <harbor_host>,分别使用用户名和邮箱两种方式登录(两种方式都要验证); - Docker 客户端错误密码登录:使用
docker login <harbor_host>以错误的密码、配合用户名或邮箱登录,预期失败。
其中第 6 步的示例命令:
# 用户名方式 docker login <harbor_host> # Username: <username> # Password: <password> # 邮箱方式(Harbor 允许以邮箱作为 principal 登录) docker login <harbor_host> # Username: <email> # Password: <password>4. 预期结果与验收标准
原用例 Expected Outcome 一节给出 5 条验收标准,逐条列示:
| 步骤 | 预期结果 |
|---|---|
| 步骤 1 & 3 | 用户可分别以用户名、邮箱通过 UI 登录;登录后确认仪表盘与导航栏为"非管理员"样式(不应看到任何管理员选项) |
| 步骤 2 & 4 | 登出后重新显示登录页 |
| 步骤 5 | 错误提示不得泄露哪个输入项是错误的,只应显示"用户名(邮箱)与密码组合不正确" |
| 步骤 6 | Docker 客户端使用用户名或邮箱均可登录成功 |
| 步骤 7 | Docker 客户端使用错误密码登录失败 |
用例 Possible Problems 一节标注为None,即该用例没有已知的干扰项或环境陷阱。
5. 源码级链路解析:登录请求在 Harbor 内部发生了什么
以下结合当前仓库源码,还原上述 UI/Docker 登录背后的实现链路(文件路径均相对仓库根目录)。
5.1 UI 登录入口:CommonController.Login
UI 表单提交由 src/core/controllers/base.go 中的CommonController.Login处理,核心逻辑约 30 行:
func (cc *CommonController) Login() { principal := cc.GetString("principal") password := cc.GetString("password") // OIDC 模式下且该用户为 OIDC 用户时,重定向到 OIDC provider 登录页 if redirectForOIDC(cc.Ctx.Request.Context(), principal) { ... } user, err := auth.Login(cc.Context(), models.AuthModel{ Principal: principal, Password: password, }) if err != nil { log.Errorf("Error occurred in UserLogin: %v", err) cc.CustomAbort(http.StatusUnauthorized, "") } if user == nil { cc.CustomAbort(http.StatusUnauthorized, "") } cc.PopulateUserSession(*user) }三个要点:
- 登录名参数名是
principal,与"用户名或邮箱均可登录"的用例步骤 1/3 对应——服务端并不区分二者,交由认证器做模糊匹配(见 5.4 节); - 认证失败时
CustomAbort(http.StatusUnauthorized, "")——响应体为空,这正是用例步骤 5"错误提示不泄露具体哪个字段错误"的服务端依据:UI 只能收到 401,随后展示统一的"用户名(邮箱)或密码不正确"文案; - 认证成功后调用
cc.PopulateUserSession(*user)建立会话。
5.2 认证路由层:auth.Login按模式分发
src/core/auth/authenticator.go 是各认证模式的统一入口:
func Login(ctx context.Context, m models.AuthModel) (*models.User, error) { authMode, err := config.AuthMode(ctx) ... if authMode == "" || IsSuperUser(ctx, m.Principal) { authMode = common.DBAuth } ... authenticator, ok := registry[authMode] if !ok { return nil, fmt.Errorf("unrecognized auth_mode: %s", authMode) } if lock.IsLocked(m.Principal) { return nil, nil } user, err := authenticator.Authenticate(ctx, m) if err != nil { if _, ok = err.(ErrAuth); ok { log.Warningf("Login failed, locking %s, and sleep for %v", m.Principal, frozenTime) lock.Lock(m.Principal) time.Sleep(frozenTime) } return nil, err } err = authenticator.PostAuthenticate(ctx, user) return user, err }值得注意的实现细节:
- 注册表机制:各认证模式通过
auth.Register(name, h)注册进全局registrymap(L124-L134),Login按当前auth_mode取出对应AuthenticateHelper执行; - 空模式或超级用户回退:
authMode为空、或登录者恰好是内置超级用户(IsSuperUser,约定user_id == 1)时,强制走db_auth,保证初始管理员在 LDAP/OIDC 环境下仍可登录; - 失败锁定(防暴力破解):文件顶部定义
frozenTime = 1500 * time.Millisecond(L33),密码错误(返回ErrAuth类型错误)时对该 principal 加锁并延迟 1.5 秒,锁实现见 src/core/auth/lock.go;被锁期间再次登录直接返回nil, nil,上层同样以 401 处理。这对用例步骤 5/7 的快速重试场景有实际影响。
5.3 DB 认证器:db.Auth
db_auth模式的具体实现在 src/core/auth/db/db.go:
func (d *Auth) Authenticate(ctx context.Context, m models.AuthModel) (*models.User, error) { u, err := d.userMgr.MatchLocalPassword(ctx, m.Principal, m.Password) if err != nil { return nil, err } if u == nil { return nil, auth.NewErrAuth("Invalid credentials") } return u, nil } func init() { auth.Register(common.DBAuth, &Auth{ userMgr: user.New(), }) }init()把db.Auth注册为db_auth模式的认证器。当用户不存在或密码不匹配时返回auth.NewErrAuth("Invalid credentials")——注意ErrAuth在接口注释中被明确定义为"bad credentials(用户凭据错误)"类错误,与数据库连接失败等服务端错误区分开(见 authenticator.go L67-L84),这决定了上层是否触发失败锁定。
5.4 用户名或邮箱匹配与密码校验
真正的密码比对在 src/pkg/user/manager.go 的MatchLocalPassword:
func (m *manager) MatchLocalPassword(ctx context.Context, usernameOrEmail, password string) (*commonmodels.User, error) { l, err := m.dao.List(ctx, q.New(q.KeyWords{"username_or_email": usernameOrEmail})) ... for _, entry := range l { if utils.Encrypt(password, entry.Salt, entry.PasswordVersion) == entry.Password { entry.Password = "" return entry, nil } } return nil, nil }这段代码直接支撑了用例的两条设计:
username_or_email查询条件:DAO 层支持按"用户名或邮箱"任一定位用户记录,因此步骤 1(用户名)与步骤 3(邮箱)能走同一入口登录;- 加盐 + 版本化哈希:密码用
utils.Encrypt(password, salt, password_version)(实现位于 src/common/utils/encrypt.go)与库中存储值比对,匹配成功前会先将entry.Password置空再返回,避免密码哈希随用户对象外泄。
5.5 会话建立与登出
认证成功后,PopulateUserSession(src/core/api/base.go)负责会话:
func (b *BaseController) PopulateUserSession(u models.User) { err := b.SessionRegenerateID() // 重新生成 session ID,防止会话固定攻击 ... if err := b.SetSession(userSessionKey, u); err != nil { ... } }即先轮换会话 ID,再把用户模型写入 session(键为常量userSessionKey = "user",L40)。登出则由同文件的CommonController.LogOut处理,销毁会话后浏览器回到登录页——对应用例步骤 2/4"登出后重新显示登录页"的预期。
5.6docker login的认证通道
步骤 6/7 的docker login <harbor_host>走的是 Harbor 的 v2 registry API 认证链路。从源码结构看,v2 API 的 token 签发服务位于 src/core/service/token/(含token.go、creator.go、authutils.go),v2.0 HTTP API 路由与 handler 位于 src/server/v2.0/。Docker CLI 在docker login时会向 registry 的 token 端点提交凭据,其背后的用户凭据校验与 UI 登录同源(同样是db_auth认证器 +MatchLocalPassword),因此用户名与邮箱在docker login中同样有效——这正是用例要求两种 principal 都验证的原因。
6. 执行建议与相邻用例
该用例执行时的实操建议:
- 先确认实例的认证模式:部署配置中
auth_mode(环境变量AUTH_MODE)为db_auth,且待测用户已存在于 Harbor 本地数据库; - 准备一个非管理员账号,避免与管理员专用用例混淆;
- 按第 3 节 7 步顺序执行,逐条对照第 4 节验收表打勾;
- 若登录连续失败,注意 5.2 节所述的 1.5 秒 principal 级锁定,短时间内快速重试会出现"看似随机"的失败,间隔 2 秒以上重试更稳定。
同一测试组内与该用例强相关的相邻用例(均位于 tests/testcases/Group1-user-management/):
- 1-01-DB-user-registration.md:DB 模式用户注册;
- 1-03-DB-user-update-password.md:修改密码后再验证登录,可与本用例组合覆盖"改密后凭据生效";
- 1-07-LDAP-mode-general.md:切换为
ldap_auth后的对应用例,便于对比两种认证模式的行为差异; - 1-09-admin-create-delete-user.md 等管理员侧用例,覆盖管理员账号的登录路径。
7. 小结
本用例围绕"db_auth模式下普通用户的登录/登出正确性",用 7 个步骤覆盖了三个关键维度:用户名与邮箱双 principal 登录、错误凭据的模糊化报错、UI 与 Docker 客户端两条认证通道的一致性。结合源码可以看到,这些验收标准背后都有明确的实现支撑:username_or_email查询条件(src/pkg/user/manager.go)、空响应体的 401(src/core/controllers/base.go)、db_auth认证器注册与ErrAuth语义(src/core/auth/db/db.go),以及 1.5 秒失败锁定带来的暴力破解防护(src/core/auth/authenticator.go)。理解这条链路后,无论是执行该用例、排查登录问题,还是扩展 Harbor 的认证模式,都有了源码级的抓手。
【免费下载链接】harborAn open source trusted cloud native registry project that stores, signs, and scans content.项目地址: https://gitcode.com/GitHub_Trending/ha/harbor
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考