Zadig 文档
Zadig
教程
博客
论坛
关于
中文英文
Zadig
教程
博客
论坛
关于
Zadig v3.4
Loading...
     编辑文档
     反馈问题
     社区讨论

    本页导航

    自定义账号系统

    目前 Zadig 用户系统采用了开源项目 Dex(opens new window) 作为身份连接器,但 Dex 支持的协议列表(Dex 支持的协议(opens new window))有限,Zadig 则基于 Dex 官方库实现了一些标准的扩展,如 OAuth 协议等。如果用户自身的账号系统在 Zadig 官方支持之外,可以通过 Fork koderover/dex(opens new window) 编写 Connector 实现自定义账号系统的集成。

    # 自定义账号系统登录流程图

    account_custom

    # 自定义账号系统集成流程

    # 步骤 1 :编写自定义 Dex Connector

    1. fork koderover/dex(opens new window)
    2. 编写 Dex 自定义 Connector (基于最新 Branch release-1.10.0)
    • 目前 Dex Connector 接口定义于 dex/connector/connector.go(opens new window) 中,官方抽象了PasswordConnector、CallbackConnector、RefreshConnector、SAMLConnector 四种接口,用户可以根据自己账号系统情况实现自己的 Connector,参考 dex/connector 的各种 Connector 组合实现这四种接口。

    以下是对各个接口的简要说明,根据账号系统登录的具体交互方式需要实现对应的接口:

    接口名使用说明目前可参考已有实现
    PasswordConnector1. 简单使用用户名密码方式登录授权获取用户信息的情况。
    2. Dex 和账号系统的交互是同步的情况
    keystone、atlassiancrowed、ldap、mock/passwordConnector、passwordDB(位于 dex/server/server.go(opens new window))
    CallbackConnector1. 通过重定向获取用户信息的情况
    2. Dex 和账号系统的交互是异步的情况
    mock/Callback、bitbucketcloud、authproxy、gitea、github、gitlab、google、linkedin、microsoft、oauth、oidc、openshift
    SAMLConnector实现 HTTP POST 绑定的 SAML 连接器。RelayState 由服务器处理saml
    RefreshConnector实现后可以更新客户端 claimsmock/Callback、bitbucketcloud、atlassiancrowed、gitea、github、gitlab、google、ldap、linkedin、microsoft、mock/passwordConnector、oidc、passwordDB(位于dex/server/server.go)
    • 将添加的自定义 connector type 和名称加入 dex/server/server.go 的 ConnectorsConfig 中。

    # 参考例子

    以 dex/connector 目录下的 OAuth connector 为例讲解。

    1. 实现 connector interface

    此 connector 实现了 CallbackConnector interface。

    以下为 OAuth connector 代码
    点击查看
    package oauth
    
    import (
       "context"
       "crypto/tls"
       "crypto/x509"
       "encoding/base64"
       "encoding/json"
       "errors"
       "fmt"
       "io/ioutil"
       "net"
       "net/http"
       "strings"
       "time"
    
       "golang.org/x/oauth2"
    
       "github.com/dexidp/dex/connector"
       "github.com/dexidp/dex/pkg/log"
    )
    
    type oauthConnector struct {
       clientID             string
       clientSecret         string
       redirectURI          string
       tokenURL             string
       authorizationURL     string
       userInfoURL          string
       scopes               []string
       userIDKey            string
       userNameKey          string
       preferredUsernameKey string
       emailKey             string
       emailVerifiedKey     string
       groupsKey            string
       httpClient           *http.Client
       logger               log.Logger
    }
    
    type connectorData struct {
       AccessToken string
    }
    
    type Config struct {
       ClientID           string   `json:"clientID"`
       ClientSecret       string   `json:"clientSecret"`
       RedirectURI        string   `json:"redirectURI"`
       TokenURL           string   `json:"tokenURL"`
       AuthorizationURL   string   `json:"authorizationURL"`
       UserInfoURL        string   `json:"userInfoURL"`
       Scopes             []string `json:"scopes"`
       RootCAs            []string `json:"rootCAs"`
       InsecureSkipVerify bool     `json:"insecureSkipVerify"`
       UserIDKey          string   `json:"userIDKey"` // defaults to "id"
       ClaimMapping       struct {
          UserNameKey          string `json:"userNameKey"`          // defaults to "user_name"
          PreferredUsernameKey string `json:"preferredUsernameKey"` // defaults to "preferred_username"
          GroupsKey            string `json:"groupsKey"`            // defaults to "groups"
          EmailKey             string `json:"emailKey"`             // defaults to "email"
          EmailVerifiedKey     string `json:"emailVerifiedKey"`     // defaults to "email_verified"
       } `json:"claimMapping"`
    }
    
    func (c *Config) Open(id string, logger log.Logger) (connector.Connector, error) {
       var err error
    
       if c.UserIDKey == "" {
          c.UserIDKey = "id"
       }
    
       if c.ClaimMapping.UserNameKey == "" {
          c.ClaimMapping.UserNameKey = "user_name"
       }
    
       if c.ClaimMapping.PreferredUsernameKey == "" {
          c.ClaimMapping.PreferredUsernameKey = "preferred_username"
       }
    
       if c.ClaimMapping.GroupsKey == "" {
          c.ClaimMapping.GroupsKey = "groups"
       }
    
       if c.ClaimMapping.EmailKey == "" {
          c.ClaimMapping.EmailKey = "email"
       }
    
       if c.ClaimMapping.EmailVerifiedKey == "" {
          c.ClaimMapping.EmailVerifiedKey = "email_verified"
       }
       oauthConn := &oauthConnector{
          clientID:             c.ClientID,
          clientSecret:         c.ClientSecret,
          tokenURL:             c.TokenURL,
          authorizationURL:     c.AuthorizationURL,
          userInfoURL:          c.UserInfoURL,
          scopes:               c.Scopes,
          redirectURI:          c.RedirectURI,
          logger:               logger,
          userIDKey:            c.UserIDKey,
          userNameKey:          c.ClaimMapping.UserNameKey,
          preferredUsernameKey: c.ClaimMapping.PreferredUsernameKey,
          groupsKey:            c.ClaimMapping.GroupsKey,
          emailKey:             c.ClaimMapping.EmailKey,
          emailVerifiedKey:     c.ClaimMapping.EmailVerifiedKey,
       }
    
       oauthConn.httpClient, err = newHTTPClient(c.RootCAs, c.InsecureSkipVerify)
       if err != nil {
          return nil, err
       }
    
       return oauthConn, err
    }
    
    func newHTTPClient(rootCAs []string, insecureSkipVerify bool) (*http.Client, error) {
       pool, err := x509.SystemCertPool()
       if err != nil {
          return nil, err
       }
    
       tlsConfig := tls.Config{RootCAs: pool, InsecureSkipVerify: insecureSkipVerify}
       for _, rootCA := range rootCAs {
          rootCABytes, err := ioutil.ReadFile(rootCA)
          if err != nil {
             return nil, fmt.Errorf("failed to read root-ca: %v", err)
          }
          if !tlsConfig.RootCAs.AppendCertsFromPEM(rootCABytes) {
             return nil, fmt.Errorf("no certs found in root CA file %q", rootCA)
          }
       }
    
       return &http.Client{
          Transport: &http.Transport{
             TLSClientConfig: &tlsConfig,
             Proxy:           http.ProxyFromEnvironment,
             DialContext: (&net.Dialer{
                Timeout:   30 * time.Second,
                KeepAlive: 30 * time.Second,
                DualStack: true,
             }).DialContext,
             MaxIdleConns:          100,
             IdleConnTimeout:       90 * time.Second,
             TLSHandshakeTimeout:   10 * time.Second,
             ExpectContinueTimeout: 1 * time.Second,
          },
       }, nil
    }
    
    func (c *oauthConnector) LoginURL(scopes connector.Scopes, callbackURL, state string) (string, error) {
       if c.redirectURI != callbackURL {
          c.logger.Errorf("expected callback URL %q did not match the URL in the config %q", callbackURL, c.redirectURI)
          return "", fmt.Errorf("expected callback URL %q did not match the URL in the config %q", callbackURL, c.redirectURI)
       }
    
       oauth2Config := &oauth2.Config{
          ClientID:     c.clientID,
          ClientSecret: c.clientSecret,
          Endpoint:     oauth2.Endpoint{TokenURL: c.tokenURL, AuthURL: c.authorizationURL},
          RedirectURL:  c.redirectURI,
          Scopes:       c.scopes,
       }
    
       return oauth2Config.AuthCodeURL(state), nil
    }
    
    func (c *oauthConnector) HandleCallback(s connector.Scopes, r *http.Request) (identity connector.Identity, err error) {
       q := r.URL.Query()
       if errType := q.Get("error"); errType != "" {
          c.logger.Errorf("get error:%s", q.Get("error_description"))
          return identity, errors.New(q.Get("error_description"))
       }
    
       oauth2Config := &oauth2.Config{
          ClientID:     c.clientID,
          ClientSecret: c.clientSecret,
          Endpoint:     oauth2.Endpoint{TokenURL: c.tokenURL, AuthURL: c.authorizationURL},
          RedirectURL:  c.redirectURI,
          Scopes:       c.scopes,
       }
    
       ctx := context.WithValue(r.Context(), oauth2.HTTPClient, c.httpClient)
    
       token, err := oauth2Config.Exchange(ctx, q.Get("code"))
       if err != nil {
          c.logger.Errorf("OAuth connector: failed to get token: %v", err)
          return identity, fmt.Errorf("OAuth connector: failed to get token: %v", err)
       }
    
       client := oauth2.NewClient(ctx, oauth2.StaticTokenSource(token))
    
       userInfoResp, err := client.Get(c.userInfoURL)
       if err != nil {
          c.logger.Errorf("OAuth Connector: failed to execute request to userinfo: %v", err)
          return identity, fmt.Errorf("OAuth Connector: failed to execute request to userinfo: %v", err)
       }
       defer userInfoResp.Body.Close()
    
       if userInfoResp.StatusCode != http.StatusOK {
          c.logger.Errorf("OAuth Connector: failed to execute request to userinfo: status %d", userInfoResp.StatusCode)
          return identity, fmt.Errorf("OAuth Connector: failed to execute request to userinfo: status %d", userInfoResp.StatusCode)
       }
    
       var userInfoResult map[string]interface{}
       err = json.NewDecoder(userInfoResp.Body).Decode(&userInfoResult)
       if err != nil {
          c.logger.Errorf("OAuth Connector: failed to parse userinfo: %v", err)
          return identity, fmt.Errorf("OAuth Connector: failed to parse userinfo: %v", err)
       }
    
       userID, found := userInfoResult[c.userIDKey].(string)
       if !found {
          c.logger.Errorf("OAuth Connector: not found %v claim", c.userIDKey)
          return identity, fmt.Errorf("OAuth Connector: not found %v claim", c.userIDKey)
       }
    
       identity.UserID = userID
       identity.Username, _ = userInfoResult[c.userNameKey].(string)
       identity.PreferredUsername, _ = userInfoResult[c.preferredUsernameKey].(string)
       identity.Email, _ = userInfoResult[c.emailKey].(string)
       identity.EmailVerified, _ = userInfoResult[c.emailVerifiedKey].(bool)
    
       if s.Groups {
          groups := map[string]struct{}{}
    
          c.addGroupsFromMap(groups, userInfoResult)
          c.addGroupsFromToken(groups, token.AccessToken)
    
          for groupName := range groups {
             identity.Groups = append(identity.Groups, groupName)
          }
       }
    
       if s.OfflineAccess {
          data := connectorData{AccessToken: token.AccessToken}
          connData, err := json.Marshal(data)
          if err != nil {
             c.logger.Errorf("OAuth Connector: failed to parse connector data for offline access: %v", err)
             return identity, fmt.Errorf("OAuth Connector: failed to parse connector data for offline access: %v", err)
          }
          identity.ConnectorData = connData
       }
       return identity, nil
    }
    
    func (c *oauthConnector) addGroupsFromMap(groups map[string]struct{}, result map[string]interface{}) error {
       groupsClaim, ok := result[c.groupsKey].([]interface{})
       if !ok {
          return errors.New("cannot convert to slice")
       }
    
       for _, group := range groupsClaim {
          if groupString, ok := group.(string); ok {
             groups[groupString] = struct{}{}
          }
       }
    
       return nil
    }
    
    func (c *oauthConnector) addGroupsFromToken(groups map[string]struct{}, token string) error {
       parts := strings.Split(token, ".")
       if len(parts) < 2 {
          return errors.New("invalid token")
       }
    
       decoded, err := decode(parts[1])
       if err != nil {
          return err
       }
    
       var claimsMap map[string]interface{}
       err = json.Unmarshal(decoded, &claimsMap)
       if err != nil {
          return err
       }
    
       return c.addGroupsFromMap(groups, claimsMap)
    }
    
    func decode(seg string) ([]byte, error) {
       if l := len(seg) % 4; l > 0 {
          seg += strings.Repeat("=", 4-l)
       }
    
       return base64.URLEncoding.DecodeString(seg)
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    147
    148
    149
    150
    151
    152
    153
    154
    155
    156
    157
    158
    159
    160
    161
    162
    163
    164
    165
    166
    167
    168
    169
    170
    171
    172
    173
    174
    175
    176
    177
    178
    179
    180
    181
    182
    183
    184
    185
    186
    187
    188
    189
    190
    191
    192
    193
    194
    195
    196
    197
    198
    199
    200
    201
    202
    203
    204
    205
    206
    207
    208
    209
    210
    211
    212
    213
    214
    215
    216
    217
    218
    219
    220
    221
    222
    223
    224
    225
    226
    227
    228
    229
    230
    231
    232
    233
    234
    235
    236
    237
    238
    239
    240
    241
    242
    243
    244
    245
    246
    247
    248
    249
    250
    251
    252
    253
    254
    255
    256
    257
    258
    259
    260
    261
    262
    263
    264
    265
    266
    267
    268
    269
    270
    271
    272
    273
    274
    275
    276
    277
    278
    279
    280
    281
    282
    283
    284
    285
    286
    287
    1. 将添加的自定义 connector type 和名称加入 dex/server/server.go 的 ConnectorsConfig 中,如下所示。
    点击查看
    // ConnectorsConfig variable provides an easy way to return a config struct
    // depending on the connector type.
    var ConnectorsConfig = map[string]func() ConnectorConfig{
       "keystone":        func() ConnectorConfig { return new(keystone.Config) },
       "mockCallback":    func() ConnectorConfig { return new(mock.CallbackConfig) },
       "mockPassword":    func() ConnectorConfig { return new(mock.PasswordConfig) },
       "ldap":            func() ConnectorConfig { return new(ldap.Config) },
       "gitea":           func() ConnectorConfig { return new(gitea.Config) },
       "github":          func() ConnectorConfig { return new(github.Config) },
       "gitlab":          func() ConnectorConfig { return new(gitlab.Config) },
       "google":          func() ConnectorConfig { return new(google.Config) },
       "oidc":            func() ConnectorConfig { return new(oidc.Config) },
       // 这个位置需要加自定义 connector type
       "oauth":           func() ConnectorConfig { return new(oauth.Config) },
       "saml":            func() ConnectorConfig { return new(saml.Config) },
       "authproxy":       func() ConnectorConfig { return new(authproxy.Config) },
       "linkedin":        func() ConnectorConfig { return new(linkedin.Config) },
       "microsoft":       func() ConnectorConfig { return new(microsoft.Config) },
       "bitbucket-cloud": func() ConnectorConfig { return new(bitbucketcloud.Config) },
       "openshift":       func() ConnectorConfig { return new(openshift.Config) },
       "atlassian-crowd": func() ConnectorConfig { return new(atlassiancrowd.Config) },
       // Keep around for backwards compatibility.
       "samlExperimental": func() ConnectorConfig { return new(saml.Config) },
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24

    # 步骤 2:构建自定义的 Dex 镜像

    1. 更改根目录下 Makefile(opens new window) 中的 DOCKER_REPO 变量为自己的公开镜像仓库
    2. 运行 make docker-image 构建镜像,并上传镜像至自己的公开镜像仓库
    3. 用自定义的镜像覆盖 Dex 的原始镜像
    kubectl get deployment -n <Zadig Namespace> | grep dex # 获得 Dex 的 deployment
    kubectl set image deployment/<Dex 的 deployment> dex=<新生成的 Dex 镜像> --record -n <Zadig Namespace> # 替换 Dex 镜像
    
    1
    2

    # 步骤 3:在 Zadig 平台中录入配置

    登录 Zadig 平台后,在系统设置 -> 账号系统集成-> 自定义账户类型录入 YAML 配置,如下图所示。

    YAML 配置由 Connector 中的 Config 结构体生成。

    account_custom

    # 步骤 4:使用自定义账号系统登录 Zadig

    访问 Zadig 登录页面,点击第三方登录,如下图所示。跳转到对应的登录页面,输入用户名密码即可登录 Zadig 系统。

    account_custom

    # [可选]设置为默认账号系统

    参考设置默认账号系统。

    ← 系统自定义工作流任务→

    资源
    教程
    论坛
    博客
    公司
    关于
    客户故事
    加入我们
    联系我们
    微信扫一扫
    hello@koderover.com

    © 2026 筑栈(上海)信息技术有限公司 沪 ICP 备 19000177 号 - 1

    •  跟随系统
    •  浅色模式
    •  深色模式
    •  阅读模式