当前位置: 代码网 > 服务器>服务器>Linux > Zadig 如何用 Dex 实现账号系统管理

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

2024年08月04日 • Linux •我要评论
确保企业应用的安全性,用户认证与授权是不可或缺的核心环节。在构建安全的企业级应用时,一个稳健的认证与授权机制显得尤为关键。 Zadig 账号系统以其强大的管理能力,超越了基本的内部账号管理,进一步升级以满足企业级需求。我们增强了账号授权的接入功能,除支持自身内部账号管理能力外,额外支持 LDAP、OAuth 等广泛认可的账号授权标准,同时实现了与 Active Directory、GitHub、GitLab 等主流平台的无缝集成,全面满足企业...

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

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)
}

三方账号系统接入

目前 zadig 系统支持集成 microsoft active directory、openldap、github 以及 oauth 等外部账号系统,更多自定义系统的接入可参考文档 自定义账号系统集成 | zadig 文档。

扫码即刻咨询
解锁企业专属最佳实践方案!

赞 (0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2026  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com