Skip to content
使用dex实现一个满足基本需求的身份认证系统
Go CSS HTML Makefile
Branch: master
Clone or download
Latest commit dba685e Aug 4, 2019
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
cmd/dexclient first commit Aug 4, 2019
config first commit Aug 4, 2019
web first commit Aug 4, 2019
.gitignore first commit Aug 4, 2019
Makefile
README.md first commit Aug 4, 2019
go.mod first commit Aug 4, 2019
go.sum first commit Aug 4, 2019

README.md

身份认证系统

读完dex的官方文档,感觉官方文档写得比较晦涩难懂,本项目尝试使用dex实现一个满足基本需求的身份认证系统,并加以简要说明。

如何运行

前提是已经搭建好了go语言的开发环境,并设置好了GOPATH。

然后按以下步骤运行本程序:

# 编译dexserver
$ make build-dexserver

# 编译dexclient
$ make build-dexclient

# 运行dexserver
$ make run-dexserver

# 运行dexclient
$ run-dexclient

然后用浏览器访问http://127.0.0.1:8080, 页面会自动跳转至dexserver的登录页面,输入用户名admin@example.com、密码password之后,会跳回dexclient的callback页面http://127.0.0.1:8080/callback

技术细节说明

dexserver

这里使用的dexserver是由官方代码直接编译得出的,没有修改任何代码。只不过使用了自定义的配置文件dexserver-config.yaml,这里分析一下这个配置文件。

# The base path of dex and the external name of the OpenID Connect service.
# This is the canonical URL that all clients MUST use to refer to dex. If a
# path is provided, dex's HTTP service will listen at a non-root URL.
issuer: http://127.0.0.1:5556/dex

# The storage configuration determines where dex stores its state. Supported
# options include SQL flavors and Kubernetes third party resources.
#
# See the storage document at Documentation/storage.md for further information.
storage:
  type: sqlite3
  config:
    file: config/dex.db

# Configuration for the HTTP endpoints.
web:
  http: 0.0.0.0:5556
  # Uncomment for HTTPS options.
  # https: 127.0.0.1:5554
  # tlsCert: /etc/dex/tls.crt
  # tlsKey: /etc/dex/tls.key

# Configuration for telemetry
telemetry:
  http: 0.0.0.0:5558

# Uncomment this block to enable the gRPC API. This values MUST be different
# from the HTTP endpoints.
# grpc:
#   addr: 127.0.0.1:5557
#  tlsCert: examples/grpc-client/server.crt
#  tlsKey: examples/grpc-client/server.key
#  tlsClientCA: /etc/dex/client.crt

# Uncomment this block to enable configuration for the expiration time durations.
# expiry:
#   signingKeys: "6h"
#   idTokens: "24h"

# Options for controlling the logger.
# logger:
#   level: "debug"
#   format: "text" # can also be "json"

# Uncomment this block to control which response types dex supports. For example
# the following response types enable the implicit flow for web-only clients.
# Defaults to ["code"], the code flow.
# oauth2:
#   responseTypes: ["code", "token", "id_token"]

oauth2:
  skipApprovalScreen: true

# Instead of reading from an external storage, use this list of clients.
#
# If this option isn't chosen clients may be added through the gRPC API.
staticClients:
- id: demo-dexclient
  redirectURIs:
  - 'http://127.0.0.1:8080/callback'
  name: 'Demo dex client'
  secret: ZXhhbXBsZS1hcHAtc2VjcmV0

connectors: []
# - type: mockCallback
#   id: mock
#   name: Example
# - type: oidc
#   id: google
#   name: Google
#   config:
#     issuer: https://accounts.google.com
#     # Connector config values starting with a "$" will read from the environment.
#     clientID: $GOOGLE_CLIENT_ID
#     clientSecret: $GOOGLE_CLIENT_SECRET
#     redirectURI: http://127.0.0.1:5556/dex/callback
#     hostedDomains:
#     - $GOOGLE_HOSTED_DOMAIN

# Let dex keep a list of passwords which can be used to login to dex.
enablePasswordDB: true

# A static list of passwords to login the end user. By identifying here, dex
# won't look in its underlying storage for passwords.
#
# If this option isn't chosen users may be added through the gRPC API.
# staticPasswords: 
# - email: "admin@example.com"
#   # bcrypt hash of the string "password"
#   hash: "$2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W"
#   username: "admin"
#   userID: "08a8684b-db88-4b73-90a9-3cd1661f5466"

web段配置的是dexserver的监听地址及HTTPS证书参数,issuer配置的是外部会访问到的系统URL,这两者一般要对应地设置。

telemetry段配置的是监控指标抓取地址,本例中dexserver启动完毕后,可访问http://127.0.0.1:5558/metrics抓取到该dexserver的监控指标。

storage段配置的是dexserver的存储设置。dexserver在运行时跟踪refresh_tokenauth_codekeyspassword等的状态,因此需要将这些状态保存下来。dex提供了多种存储方案,如etcdCRDsSQLite3PostgresMySQLmemory,总有一款能满足需求。如果要其它需求,还可以参考现有Storage文档扩展一个。我这里使用的是比较简单的SQLite3Storage,提前插入了一条测试的用户数据:

sqlite3 config/dex.db
sqlite> insert info password values('admin@example.com', '$2a$10$2b2cU8CPhOTaGrs1HRQuAueS7JTT5ZHsHSzYiFPm1leZck7Mc8T4W', 'admin', '08a8684b-db88-4b73-90a9-3cd1661f5466');
sqlite> .quit

oauth2.skipApprovalScreen这个选项我设置成了true,这样就不会有提示用户同意的页面出现。

staticClients段配置的是该dexserver允许接入的dexclient信息,这个要跟dexclient那边的配置一致。

connectors段我并没有配置任何ConnectorConnectordex中一项重要特性,其可以将dex这个身份认证系统与其它身份认证系统串联起来。dex目前自带的ConnectorLDAPGitHubSAML 2.0GitLabOpenID ConnectLinkedInMicrosoftAuthProxyBitbucket Cloud,基本上满足绝大部分需求,如果要扩展,参考某个现成的Connector实现即可。我这个示例里因为直接使用保存在DB里的帐户密码信息,因此只需要配置enablePasswordDBtrue,就会自动使用上passwordDB这个ConnectorpasswordDB的实现代码见这里

最近由于登录页面是由dexserver提供了,这里还将dex自带的登录页面web端资源带上了,具体的项目中根据场景对页面进行一些定制就可以了。

dexclient

dexclient就很简单了,就两个go文件,重点是cmd/dexclient/main.go

首先是根据一系列参数构造出oidc.Provideroidc.IDTokenVerifier,这个后面获取认证系统的跳转地址、获取id_token、校验id_token都会用到:

...
            a.provider = provider
            a.verifier = provider.Verifier(&oidc.Config{ClientID: a.clientID})

然后声明处理三个请求地址,并启动Web Server:

			http.HandleFunc("/", a.handleIndex)
			http.HandleFunc("/login", a.handleLogin)
			http.HandleFunc(u.Path, a.handleCallback)

			switch listenURL.Scheme {
			case "http":
				log.Printf("listening on %s", listen)
				return http.ListenAndServe(listenURL.Host, nil)
			case "https":
				log.Printf("listening on %s", listen)
				return http.ListenAndServeTLS(listenURL.Host, tlsCert, tlsKey, nil)
			default:
				return fmt.Errorf("listen address %q is not using http or https", listen)
			}

很明显handleIndex就是WEB应用的主页,这里一般逻辑应该是检查用户的登录身份信息是否合法,如果不合法�则跳至dexserver的登录页面。

var indexTmpl = template.Must(template.New("index.html").Parse(`<html>
  <!-- TODO Redirect to login page if not logged  -->
  <body>
    <form action="/login" method="post">
       <p>
         Authenticate for:<input type="text" name="cross_client" placeholder="list of client-ids">
       </p>
       <p>
         Extra scopes:<input type="text" name="extra_scopes" placeholder="list of scopes">
       </p>
       <p>
         Connector ID:<input type="text" name="connector_id" placeholder="connector id">
       </p>
       <p>
         Request offline access:<input type="checkbox" name="offline_access" value="yes" checked>
       </p>
       <input type="submit" value="Login" id="submitBtn">
    </form>
  </body>
  <script type="text/javascript">
    <!-- Redirect to login page -->
	document.getElementById("submitBtn").click();
  </script>
</html>`))

handleLogin根据浏览器发来的cross_clientextra_scopesconnector_idoffline_access参数构造出登录页跳转地址,并提示浏览器跳至该地址:

    ...
    if r.FormValue("offline_access") != "yes" {
		authCodeURL = a.oauth2Config(scopes).AuthCodeURL(exampleAppState)
	} else if a.offlineAsScope {
		scopes = append(scopes, "offline_access")
		authCodeURL = a.oauth2Config(scopes).AuthCodeURL(exampleAppState)
	} else {
		authCodeURL = a.oauth2Config(scopes).AuthCodeURL(exampleAppState, oauth2.AccessTypeOffline)
	}
	if connectorID != "" {
		authCodeURL = authCodeURL + "&connector_id=" + connectorID
	}

	http.Redirect(w, r, authCodeURL, http.StatusSeeOther)

handleCallback处理登录成功后的回调请求,其根据回调请求中的code参数,调用dexserver的相关接口换取包含用户身份信息的Token

        code := r.FormValue("code")
		if code == "" {
			http.Error(w, fmt.Sprintf("no code in request: %q", r.Form), http.StatusBadRequest)
			return
		}
		if state := r.FormValue("state"); state != exampleAppState {
			http.Error(w, fmt.Sprintf("expected state %q got %q", exampleAppState, state), http.StatusBadRequest)
			return
		}
		token, err = oauth2Config.Exchange(ctx, code)

一般来说,会将该Token中的id_token进行适当的编码发回到浏览器中保存(以Cookie或WebStorage等方式),这样浏览器中就保存了用户的身份信息。

安全起见,dexserver签发的id_token有效期通常不会太长,这就需要dexclient凭借Token中的refresh_token隔段时间重新换取新的Token,并通过某种机制将新Token中的id_token重新发回浏览器端保存。以refresh_token重新换取新的Token的代码实现如下:

		t := &oauth2.Token{
			RefreshToken: refresh,
			Expiry:       time.Now().Add(-time.Hour),
		}
		token, err = oauth2Config.TokenSource(ctx, t).Token()
You can’t perform that action at this time.