首页 > 大数据 > 数据挖掘 > 如何用 Dex 实现账号系统管理
确保企业应用的安全性, 用户认证与授权是不可或缺的核心环节。在构建安全的企业级应用时,一个 稳健的认证与授权机制显得尤为关键。
Zadig 账号系统以其强大的管理能力,超越了基本的内部账号管理,进一步升级以满足 企业级需求。我们增强了账号授权的接入功能,除支持自身内部账号管理能力外,额外支持 LDAP、 OAuth 等广泛认可的账号授权标准,同时实现了与 Active Directory、GitHub、GitLab 等主流平台的 无缝集成,全面满足企业级应用的 安全性和通用性需求。
以下是我们选型策略和架构设计实践,与社区读者共享,以期为构建 更安全的应用程序提供参考和启发。
研究市面上的主流方案后,针对 Dex、原生 Client、Keycloak,我们做了如下对比:
| 方案 | Dex | 使用原生 Client | Keycloak |
|---|---|---|---|
| 语言 | Go | Go | Java |
| 扩展性 | 高,可通过自行实现 connector 扩展 | 需自己实现 | 高 |
| 开发成本 | 低 | 高 | 低 |
| 用户同步 | 目前不支持 sync 模式,可自己扩展 | 需自己实现 | 支持 LDAP 用户同步,其它不支持 |
| 配置热更新 | 支持 | 可自己实现 | 支持 |
| 目前支持丰富度 | 已有丰富的 connector | 需自己实现 | 已有丰富的 connector |
| 后期维护成本 | 低,后期有强有力的社区支持 | 高,扩展性需自己保障 | 低 |
| 云原生标准适配度 | K8s 推荐,以后成为标准可能性较高 | 低 | 低 |
Zadig 充分考虑扩展性、维护成本、云原生友好度等因素最终选择用 Dex 作为基础组件。
Dex 是来自 CoreOS 的基于 OpenID Connect 的开源身份认证服务解决方案。内置的 Connectors 包括 LDAP、GitHub、GitLab、Google、OIDC 等。对于非标准的登录方式,用户也可以通过自定义 Connector 来实现接入 Zadig。

Dex 使用 OpenID Connect 来驱动应用程序的身份验证,当用户通过 Dex 登录时,该用户的身份通常存储在另一个用户管理系统中:LDAP 目录,GitHub 组织等。Dex 充当客户端应用程序和上游身份提供者之间的中介。客户端只需要了解 OpenID Connect 即可查询 Dex,而 Dex 实现了一系列用于查询其他用户管理系统的协议。
"连接器" 是 Dex 用于根据一个身份提供者对用户进行身份验证的策略。Dex 实现了针对特定平台(例如 GitHub,LinkedIn 和 Microsoft)以及已建立的协议(例如 LDAP 和 SAML)的连接器。
在完成了第三方系统登录的组件选型后,剩下的问题就是如何将 Dex 提供的第三方用户信息加入 zadig 自己的用户体系中, 完成 Zadig 用户体系的打造,根据 Zadig 系统的实际要求,我们确定了以下的技术方案:

用户登录环节主要涉及到的组件:
用户认证环节主要涉及到的组件:

第三方账号的登录逻辑如下:
用户服务的数据库模型节选:
CREATE TABLE `user_login` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `uid` varchar(64) NOT NULL DEFAULT '0' COMMENT '用户id', `login_id` varchar(64) NOT NULL DEFAULT '0' COMMENT '用户登录id,如账号名', `login_type` int(4) unsigned NOT NULL DEFAULT '0' COMMENT '登录类型,0.账号名', `password` varchar(64) DEFAULT '' COMMENT '密码', `last_login_time` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '最后登录时间', `created_at` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '创建时间', `updated_at` int(11) unsigned NOT NULL DEFAULT '0' COMMENT '更新时间', UNIQUE KEY `login` (`uid`,`login_id`,`login_type`), PRIMARY KEY (`id`), KEY `idx_uid` (`uid`) USING BTREE ) ENGINE = InnoDB AUTO_INCREMENT = 59 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '账号登录表' ROW_FORMAT = Compact;CREATE TABLE `user` ( `uid` varchar(64) NOT NULL COMMENT '用户ID', `account` varchar(32) NOT NULL DEFAULT '' COMMENT '用户账号', `name` varchar(32) NOT NULL DEFAULT '' COMMENT '用户名', `identity_type` varchar(32) NOT NULL DEFAULT 'unknown' COMMENT '用户来源', `phone` varchar(16) NOT NULL DEFAULT '' COMMENT '手机号码', `email` varchar(100) NOT NULL DEFAULT '' COMMENT '邮箱', `created_at` int(11) unsigned NOT NULL COMMENT '创建时间', `updated_at` int(11) unsigned NOT NULL COMMENT '修改时间', UNIQUE KEY `account` (`account`,`identity_type`), PRIMARY KEY (`uid`) ) ENGINE = InnoDB AUTO_INCREMENT = 59 CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '用户信息表' ROW_FORMAT = Compact;
Dex 服务数据库模型节选:
// Zadig 系统账号集成配置存在该表中create table connector ( id text not null primary key COMMENT 'connectorID', type text not null COMMENT 'connector类型,如 LDAP、AD 等', name text not null COMMENT 'connector名称', resource_version text not null COMMENT '资源版本', config bytea COMMENT 'connector 配置内容');
第三方登录的实现源码位于 koderover/zadig 库,核心代码说明如下:
func provider() *oidc.Provider {
ctx := oidc.ClientContext(context.Background(), http.DefaultClient)
provider, err := oidc.NewProvider(ctx, config.IssuerURL()) if err != nil {
log.Panicf(fmt.Sprintf("init provider error:%s", err))
} return provider
}// 用户登录会率先访问此方法func Login(c *gin.Context) {
ctx := internalhandler.NewContext(c) defer func() { internalhandler.JSONResponse(c, ctx) }()
// Dex 封装的 oauth2 config 信息
oauth2Config := &oauth2.Config{
ClientID: config.ClientID(),
ClientSecret: config.ClientSecret(),
Endpoint: provider().Endpoint(),
Scopes: config.Scopes(),
RedirectURL: config.RedirectURI(),
}
// 根据配置生成 Dex 登录页访问地址
authCodeURL := oauth2Config.AuthCodeURL(config.AppState, oauth2.AccessTypeOffline)
systemConfig, err := aslan.New(configbase.AslanServiceAddress()).GetDefaultLogin() if err != nil {
ctx.Err = err return
}
defaultLogin := ""
replaceURL := configbase.SystemAddress() + "/dex/auth"
if systemConfig.DefaultLogin != setting.DefaultLoginLocal {
defaultLogin = systemConfig.DefaultLogin
replaceURL = replaceURL + "/" + defaultLogin
} // 外部访问可以通过此方式转为内部访问
authCodeURL = strings.Replace(authCodeURL, config.IssuerURL()+"/auth", replaceURL, -1) // 跳转访问 Dex 提供的登录页
c.Redirect(http.StatusSeeOther, authCodeURL)
}// 根据 authCode 去资源服务器换取 accessToken, 并解密校验后并返回用户信息func verifyAndDecode(ctx context.Context, code string) (*login.Claims, error) {
oidcCtx := oidc.ClientContext(ctx, http.DefaultClient)
oauth2Config := &oauth2.Config{
ClientID: config.ClientID(),
ClientSecret: config.ClientSecret(),
Endpoint: provider().Endpoint(),
Scopes: nil,
RedirectURL: config.RedirectURI(),
} var token *oauth2.Token // 根据 authCode 换取 accessToken
token, err := oauth2Config.Exchange(oidcCtx, code) if err != nil { return nil, e.ErrCallBackUser.AddDesc(fmt.Sprintf("failed to get token: %v", err))
}
rawIDToken, ok := token.Extra("id_token").(string) if !ok { return nil, e.ErrCallBackUser.AddDesc("no id_token in token response")
} // 校验 accessToken
idToken, err := provider().Verifier(&oidc.Config{ClientID: config.ClientID()}).Verify(ctx, rawIDToken) if err != nil { return nil, e.ErrCallBackUser.AddDesc(fmt.Sprintf("failed to verify ID token: %v", err))
} var claimsRaw json.RawMessage // 获取用户信息
if err := idToken.Claims(&claimsRaw); err != nil { return nil, e.ErrCallBackUser.AddDesc(fmt.Sprintf("error decoding ID token claims: %v", err))
}
buff := new(bytes.Buffer) if err := json.Indent(buff, claimsRaw, "", " "); err != nil { return nil, e.ErrCallBackUser.AddDesc(fmt.Sprintf("error indenting ID token claims: %v", err))
} var claims login.Claims
err = json.Unmarshal(claimsRaw, &claims) if err != nil { return nil, err
} if len(claims.Name) == 0 {
claims.Name = claims.PreferredUsername
} return &claims, nil}// 第三方账号系统密码校验成功后的回调方法func Callback(c *gin.Context) {
ctx := internalhandler.NewContext(c) defer func() { internalhandler.JSONResponse(c, ctx) }()
if errMsg := c.Query("error"); errMsg != "" {
ctx.Err = e.ErrCallBackUser.AddDesc(errMsg) return
} // 获取 authCode
code := c.Query("code") if code == "" {
ctx.Err = e.ErrCallBackUser.AddDesc(fmt.Sprintf("no code in request: %q", c.Request.Form)) return
} if state := c.Query("state"); state != config.AppState {
ctx.Err = e.ErrCallBackUser.AddDesc(fmt.Sprintf("expected state %q got %q", config.AppState, state)) return
} // 根据 authCode 去资源服务器换取 accessToken, 并解密校验后并返回用户信息
claims, err := verifyAndDecode(c.Request.Context(), code) if err != nil {
ctx.Err = err return
} // 同步用户信息到 zadig user 数据库
user, err := user.SyncUser(&user.SyncUserInfo{
Account: claims.PreferredUsername,
Name: claims.Name,
Email: claims.Email,
IdentityType: claims.FederatedClaims.ConnectorId,
}, ctx.Logger) if err != nil {
ctx.Err = err return
}
claims.UID = user.UID
claims.StandardClaims.ExpiresAt = time.Now().Add(time.Duration(config.TokenExpiresAt()) * time.Minute).Unix() // 根据用户信息生成 token
userToken, err := login.CreateToken(claims) if err != nil {
ctx.Err = err return
}
v := url.Values{}
v.Add("token", userToken)
redirectUrl := "/?" + v.Encode() // 携带 token 返回首页
c.Redirect(http.StatusSeeOther, redirectUrl)
}
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/70041896/viewspace-3040138/ ,如需转载,请注明出处,否则将追究法律责任。