几种封装 HTTP Authorization 的分装方式

建议点击 查看原文 查看最新内容。

原文链接: https://typonotes.com/posts/2024/01/25/authz-in-http-request/

大家都知道, 在做 HTTP 请求的时候, 通常需要提供 账号名和密码, 例如

1
$ curl -u username:password http://api.example.com

其实, 这种就是 HTTP Authentication 中的 Basic 模式(Schema)

翻译一下

  1. 首先将账号密码使用 冒号: 链接
  2. 随后进行 base64 编码
  3. 最后放在 Header 的 Authorization 中。
1
2
$ val=base64("username:password")
$ curl -H "Authorization: Basic ${username:password} http://api.example.com

除了 Basic 之外, HTTP 标准 Schema 包括以下

  • Basic: 常见
  • Bearer: 常见
  • Digest
  • HOBA
  • Mutual
  • Negotiate / NTLM
  • VAPID
  • SCRAM
  • AWS4-HMAC-SHA256

除了以上之外, 你当然可以 自定义 自己服务器的 验证 模式, 走非标路线让黑客琢磨不透。

几种常见的 Authorization 封装方式

分享几种我见过的封装方式

1. 直接封装

这种数据比较初级阶段, 只提供 固定验证方式的封装。

如果需要再增加一种验证方式, 例如 Bearer Token,就比较无力了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
type BasicConfig struct {
	Username string `json:"user"`
	Password string `json:"name"`
}

func (bc *BasicConfig) request(method string, url string, body io.Reader) {
	req, _ := http.NewRequest(method, url, body)

	// 硬编码
	auth := bc.Username + ":" + bc.Password
	authz := base64.StdEncoding.EncodeToString([]byte(auth))
	req.Header.Set("Authorization", "Basic "+authz)

	_, _ = http.DefaultClient.Do(req)
}

2. Context 传递

这种方式使用 Context 进行验证参数的传递, 具有一定扩展性, 依旧存在 枚举 验证方式的问题。

2.1 定义 Context Key 类型

注意: 在定义 context key 的时候, 不能直接使用 简单类型, 例如 stirng, int 等。 而是 通过这些简单类型创建一个新类型,再使用。 这是类型的基础知识!!!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
type contextKey string

func (c contextKey) String() string {
	return "auth " + string(c)
}

var (
	// ContextOAuth2 takes a oauth2.TokenSource as authentication for the request.
	ContextOAuth2 = contextKey("token")
	// ContextBasicAuth takes BasicAuth as authentication for the request.
	ContextBasicAuth = contextKey("basic")
	// ContextAccessToken takes a string oauth2 access token as authentication for the request.
	ContextAccessToken = contextKey("accesstoken")
	// ContextAPIKey takes an APIKey as authentication for the request
	ContextAPIKey = contextKey("apikey")
)

源代码在 go-bamboo-v1/configuation.go - Github

2.2 通过 Context 获取验证信息

接下来, 在构建 request 的时候, 就通过 ctx.Value 获取 相应值。 如果 断言 成功, 则添加到 HTTP Header 中。

 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
// prepareRequest build the request
func (c *APIClient) prepareRequest(
	ctx context.Context) (localVarRequest *http.Request, err error) {

	if ctx != nil {
		// add context to the request
		localVarRequest = localVarRequest.WithContext(ctx)
		// Walk through any authentication.
		// OAuth2 authentication
		if tok, ok := ctx.Value(ContextOAuth2).(oauth2.TokenSource); ok {
			// We were able to grab an oauth2 token from the context
			var latestToken *oauth2.Token
			if latestToken, err = tok.Token(); err != nil {
				return nil, err
			}
			latestToken.SetAuthHeader(localVarRequest)
		}
		// Basic HTTP Authentication
		if auth, ok := ctx.Value(ContextBasicAuth).(BasicAuth); ok {
			localVarRequest.SetBasicAuth(auth.UserName, auth.Password)
		}
		// AccessToken Authentication
		if auth, ok := ctx.Value(ContextAccessToken).(string); ok {
			localVarRequest.Header.Add("Authorization", "Bearer "+auth)
		}
	}
	return localVarRequest, nil
}

源代码在 go-bamboo-v1/api_client.go - Github

3. DefaultHeader 传递

这种方式就解决了 枚举 带来的有限性。 简单粗暴

在定义配置的时候, 直接设置一个 DefaultHeader 来承载所有。 当然, 这个 DefaultHeader 不仅仅只用来保存验证方式。

1
2
3
4
// Configuration provides the configuration to connect
type Configuration struct {
	DefaultHeader map[string]string `json:"defaultHeader,omitempty"`
}

源代码在 go-bamboo-v1/configuration#L52 - Github

在初始化 Request 的时候, 直接从 Map 转移到 Request Header 中即可。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// prepareRequest build the request
func (c *APIClient) prepareRequest(
	ctx context.Context, cfg *Configuration) (localVarRequest *http.Request, err error) {

	for header, value := range cfg.DefaultHeader {
		localVarRequest.Header.Add(header, value)
	}

	return localVarRequest, nil
}

源代码在 go-bamboo-v1/api_client.go - Github

4. 接口传递

这种方式同样不存在 枚举 的局限性。 同时看起来要比 DefaultHeader 更优雅。

4.1 定义接口

首先定义一个接口, 用于返回 HTTP Request Authorization 的值(包含 验证模式验证值)。

1
2
3
type Authorizer interface {
	Authorization() string
}

在 Client 作为字段使用, 并在初始化 Client 的时候传入

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
type Client struct {
	authorizer Authorizer // User credentials
}

func NewClient(httpClient *http.Client, creds Authorizer) *Client {
	c := &Client{
		client:     httpClient,
		BaseURL:    baseURL,
		authorizer: creds,
	}
	return c
}

4.2 通过接口获取验证信息

在构建 HTTP Request, 直接调用 Authorzer 接口即可返回值。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
func (c *Client) NewRequest(method, urlStr string, body interface{}) (*http.Request, error) {

	req, err := http.NewRequest(method, u.String(), buf)
	if err != nil {
		return nil, err
	}

	creds := c.authorizer
	req.Header.Set("Authorization", creds.Authorization())
	req.Header.Set("Accept", "application/json")

	return req, nil
}

源码在 go-bamboo/client.go#L100 - Github

4.3. 案例扩展

注意: 这个案例可以扩展, 同时返回 Header 名称和值

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
type Authorizer interface {
	Authorization() (string, string)
}


func (c *Client) NewRequest(method, urlStr string, body interface{}) (*http.Request, error) {

	req, err := http.NewRequest(method, u.String(), buf)
	if err != nil {
		return nil, err
	}

	k, creds := c.authorizer
	req.Header.Set(k, creds)
	req.Header.Set("Accept", "application/json")

	return req, nil
}