ITPub博客

首页 > 大数据 > 数据挖掘 > 如何用 Dex 实现账号系统管理

如何用 Dex 实现账号系统管理

原创 数据挖掘 作者:月河彭勃 时间:2024-10-22 14:17:24 0 删除 编辑

确保企业应用的安全性, 用户认证与授权是不可或缺的核心环节。在构建安全的企业级应用时,一个 稳健的认证与授权机制显得尤为关键。

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 组件介绍

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 系统的实际要求,我们确定了以下的技术方案:

  1. 多个外部系统中的同名用户,不视为相同用户
  2. 所有账号系统,均使用 Zadig 的 Token 进行认证管理
  3. 使用 UID 信息作为用户的唯一主键,并且和权限、消息等进行关联
  4. Zadig 自身的用户体系认证采用无状态的方式来实现,相比有状态模式,服务端控制力和压力更小,数据迁移成本也会更低。

架构设计

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

  • Zadig aslan 服务 user 模块:主要负责 Zadig 平台用户账号管理 (包括 Zadig 自身平台账号和第三方同步过来的账号),用户登录管理以及用户授权信息。
  • Dex:主要负责作为链接器链接第三方账号系统,以及存储第三方账号的配置。
  • Upstream ldp:第三方账号系统

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

  • Gloo Edge:Zadig 的网关,会拦截进入 Zadig 后台的流量,并且将流量转发给 user 服务进行认证
  • Zadig aslan 服务:Zadig 后台核心业务服务

第三方登录流程

第三方账号的登录逻辑如下:

  1. 访问 Zadig 系统的第三方登录页面(登录页内嵌在 Dex 中),输入用户名和密码后发送到第三方账号系统进行校验
  2. 第三方账号系统校验成功且同意授权 Zadig 后,携带生成的 authCode 访问 Zadig 的回调地址
  3. aslan 服务收到请求后用 authCode 换取 accessToken 并解析用户信息
  4. 刷新第三方账号的登录信息,并生成其 Token 返回登录首页,登录成功

数据库模型

用户服务的数据库模型节选:

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/ ,如需转载,请注明出处,否则将追究法律责任。

请登录后发表评论 登录
全部评论

注册时间:2024-10-22

  • 博文量
    19
  • 访问量
    15804