Skip to content

Latest commit

 

History

History
253 lines (202 loc) · 10.2 KB

README.md

File metadata and controls

253 lines (202 loc) · 10.2 KB

身份认证系统

读完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()