# token认证的设计

本文目录如下

* 请求Token
* 生成Token
* 使用Token
* 验证Token
* 扩展阅读

## 请求Token

在请求Token的API中，有以下参数

**Query Parameters**

* `service`：（neccessary）授权服务的标识，表示要向谁请求token
* `scope`：（neccessary）
* `client-id`：（optinal）请求token的客户端id，比如docker-daemon发起的请求会将该字段设置为`docker`，harbor的复制策略中会将该字段设置为`harbor-registry-client`

**Header Parameters**

* `Authorization`：（optional）携带的用户信息

**Response Body**

响应body为一个json，有三个字段

* `token`：（neccessary）授权服务器返回的带有授权信息的token
* `issued_at`：（optional）token的签发时间，UTC标准时间格式
* `expires_in`：（optional）token在多少秒以后过期，如果没有说明则默认为60秒

**示例**

```
$ curl 192.168.1.103:8021/service/token?service=token-service\&scope=repository:library/registry:pull\&client_id=curl
{
  "expires_in": 1800,
  "issued_at": "2018-09-05T08:34:40Z",
  "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6IkhNNjY6NkNYUzpaQlBROk1ENVo6QlJZVTpTVE9EOkNCUEs6Uk5ORjpYN0VDOkZMUUw6TFNFMjpLUUtTIn0.eyJpc3MiOiJyZWdpc3RyeS10b2tlbi1pc3N1ZXIiLCJzdWIiOiIiLCJhdWQiOiJ0b2tlbi1zZXJ2aWNlIiwiZXhwIjoxNTM2MTM4MjgwLCJuYmYiOjE1MzYxMzY0ODAsImlhdCI6MTUzNjEzNjQ4MCwianRpIjoiZFpxVkgxVDFjZkhXdnFZTiIsImFjY2VzcyI6W3sidHlwZSI6InJlcG9zaXRvcnkiLCJuYW1lIjoibGlicmFyeS9yZWdpc3RyeSIsImFjdGlvbnMiOlsicHVsbCJdfV19.ilCKa2-oJ9bKQpAo8ntcx1lHpbs0BcWYtbRrvItHAProaDEDpll9EZrzkzg6XR9OOLByFm_oJKKk8Y_wYwQfxYdjvhLbFjCNXzE6MckY8dEcSR5BmYxOK54zAqNVkw24ugUcagGFi7p8Gy0YZqBqf7AP8qCarhuWhKsZ7B4esMQk2xBEn1hh8r_9tb6wnZOkDl7trW0IWbPkqKSaP8ycq8oS9J0T6zaItyTLnERsV_GFJOh6DdfhSYzGwoWUFQH6cmp05ZHXF_-4O6N6d8tosGH9gTsam-ffeVHmWp8da_gpS_R15z3ELR5I2FO0s4gWo1UbTI3yuyV8stSURrCs6GHZSMb2C9_2R2r_Q-uDKmdpoazw2G1DxM3PgfXEwANWEjPJMjD0areUXmjwz_hefSMqYFxLi26TaQinG0th7pNz5m0qroefOy1AGyhRZK-t8rsduZJ9EWQCqtXHrPbTES0FoItJmcMqcJZcvQsrJsBMirtijvGdNn55l44-eDFyrIuExerHzU1dJoSijCtqIYxbdnclLE8HSP-vnBD5TOAJoUUdUfA1N8TvF2QqDjr_LATUOctahrFoWiuDjrFXH-ptmcJJ6lPjo1oCOne3ImKe_mieRR7YCOQLejuCbItIIweuqwBzJU5d33k3Drra0qvbvk-MkO7iBNgpCtfWqD8"
}
```

我们可以把上面的token拷贝到网页[jwt.io](https://github.com/pshizhsysu/docker/tree/f8e99cafd95650f9fc654ebc5fc9df01b2a5c0e2/registry/auth/jwt.io)中，查看token的明文形式。

在上面获取token的请求中，没有携带任何的用户信息。不过我们可以使用添加用户信息（用户名与密码）去获取token，如下：

```
$ curl -H "Authorization: Basic YWRtaW46SGFyYm9yMTIzNDU=" 192.168.1.103:8021/service/token?service=token-service\&scope=repository:library/registry:pull\&client_id=curl
```

其中`YWRtaW46SGFyYm9yMTIzNDU=`是`admin:Harbor12345`的base64编码

## 生成Token

token由三部分内容组成：Header、Payload和Signature。token的形式如下：

```
{token-header}.{token-payload}.{token-signature}
```

### Header

Header有三个字段

* `typ`：固定为`JWT`
* `alg`：签名算法，常用的有`HS256`、`RS256`等
* `kid`：key-id，签名算法中所使用的密钥的ID值

`kid`的生成有以下三个步骤

1、从签名算法使用的密钥中得到`DER`编码格式的公钥（public key） 2、对`DER`格式的公钥做sha256哈希，取前240bit 3、将这240bit使用base32编码，然后四个一组使用冒号`:`分隔

如下是Header的一个例子

```
{
    "typ": "JWT",
    "alg": "RS256",
    "kid"："HM66:6CXS:ZBPQ:MD5Z:BRYU:STOD:CBPK:RNNF:X7EC:FLQL:LSE2:KQKS"
}
```

生成`kid`的详细例子见本文末尾的扩展阅读

### Payload

payload中的字段有

* `iss`：（Issuer），token的签发者
* `sub`：（Subject），正在进行认证的用户的名字，如果是匿名用户则为空
* `aud`：（Audience），token的观众，即需要对token进行验证的服务的名字
* `exp`：（Expiration），过期时间，在这之后token应该看作是无效的；时间戳格式
* `nbf`：（Not Before），token有效的超始时间，在这之前token应当看作是无效的；时间戳格式
* `iat`：（Issued At），签发时间；时间戳格式
* `jti`：（JWT ID），token的id，（尚不清楚如何生成）
* `access`：权限集，下面还有三个字段
  * `type`
  * `name`
  * `actions`

payload的样例如下：

```
{
 "iss": "registry-token-issuer",
 "sub": "",
 "aud": "token-service",
 "exp": 1536204479,
 "nbf": 1536202679,
 "iat": 1536202679,
 "jti": "KF76FTQQ4tIvvCbR",
 "access": [
  {
   "type": "repository",
   "name": "library/registry",
   "actions": [
    "pull"
   ]
  }
 ]
}
```

### Signature

**Header**

对header内容去掉空白字符后得到

```
{"typ":"JWT","alg":"RS256","kid":"HM66:6CXS:ZBPQ:MD5Z:BRYU:STOD:CBPK:RNNF:X7EC:FLQL:LSE2:KQKS"}
```

然后对该字符串进行base64Url编码（[base64在线编码网址](https://base64encode.net)），得到token-header

base64Url就是先进行base64编码，再把得到的字符串中的`+`变成`-`，`/`变成`_`，去掉`=`

```
eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6IkhNNjY6NkNYUzpaQlBROk1ENVo6QlJZVTpTVE9EOkNCUEs6Uk5ORjpYN0VDOkZMUUw6TFNFMjpLUUtTIn0
```

**Payload**

payload内容去掉空白字符后得到

```
{"iss":"registry-token-issuer","sub":"","aud":"token-service","exp":1536204479,"nbf":1536202679,"iat":1536202679,"jti":"KF76FTQQ4tIvvCbR","access":[{"type":"repository","name":"library/registry","actions":["pull"]}]}
```

然后对该字符串进行base64Url编码，得到token-payload

```
eyJpc3MiOiJyZWdpc3RyeS10b2tlbi1pc3N1ZXIiLCJzdWIiOiIiLCJhdWQiOiJ0b2tlbi1zZXJ2aWNlIiwiZXhwIjoxNTM2MjA0NDc5LCJuYmYiOjE1MzYyMDI2NzksImlhdCI6MTUzNjIwMjY3OSwianRpIjoiS0Y3NkZUUVE0dEl2dkNiUiIsImFjY2VzcyI6W3sidHlwZSI6InJlcG9zaXRvcnkiLCJuYW1lIjoibGlicmFyeS9yZWdpc3RyeSIsImFjdGlvbnMiOlsicHVsbCJdfV19
```

**signature**

token-signature的计算方法如下，先对`token-header + "." + token-payload`做sha256哈希（RS256就是RSA+SHA256），然后再使用RSA的私钥进行签名（sign），最后用base64Url进行编码，得到signature-token

```
token-signature = base64Url( sign( sha256( token-header + "." + token-payload ) ) )
```

由前面的token-header与token-payload得到的token-signature如下（RSA密钥见扩展阅读）

```
e91bTpXSYNUTcUXr7zs62ZgCm1L6bhZbbW4ujXFY9Zzdkvy3DEHDssq6R4K9f5ESvv_LrWxxIxIXVREAATw-FaykcAewyjarC6Vlj2g0ea6D9L1HsIvsqtYcBOnHIJ5CRPJPhXWPwBtbujgNgbti-LLeVprOwaJ8fDk21UikmYFhX61_IobFukWw1ByXiNt8byU6tOrxkkDp-YXpz9y-XP5FdheGwNxOREph40znA9LddUcEuQUHB5WKQ3tdU4sqXOW3TUCtjLOl-kVREcus-83fLSuob1lZWRbzU9dEROd_5ZP4NNmD4ZY0DhcYbp75UqvB-MZIiC9MDeOheHAsPGB4Kqu2gBshRd_NJIrQkig7yvD2Wo7twn1KKSznHp6lcsK5phkkkWMVbZoD3qV76MqCDKVSkD2JOgQ0l4AhcYEGLtxx_ukk4NlDCYoljnGPw1oEynmFDROSvMg_bqhRVUF-5US83sU0l6YWwRCZT6StTvdSHp79wbSXgEn58-NO64AtVuMEb1XiDhDxtgaF0K61UwjBRmhpcCurw0laknBVVlta6otbfQcbyQn6ulsKgbrBKka-vkgo4_ymCyqnSXuZYC2Oz_PYawgGVz3s4JXhedoVWiUSDbyKYnFTXdtTign5oT6H6N-K1YKLoGSxma3uUwdDZP2hKH_UH_V9eOY
```

最后，对token-header、token-payload和token-signature进行组装，得到最终的token

```
eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsImtpZCI6IkhNNjY6NkNYUzpaQlBROk1ENVo6QlJZVTpTVE9EOkNCUEs6Uk5ORjpYN0VDOkZMUUw6TFNFMjpLUUtTIn0.eyJpc3MiOiJyZWdpc3RyeS10b2tlbi1pc3N1ZXIiLCJzdWIiOiIiLCJhdWQiOiJ0b2tlbi1zZXJ2aWNlIiwiZXhwIjoxNTM2NTY4NDcxLCJuYmYiOjE1MzY1NjY2NzEsImlhdCI6MTUzNjU2NjY3MSwianRpIjoiaU1kYm5td2dLQ2dUTk4xdyIsImFjY2VzcyI6W3sidHlwZSI6InJlcG9zaXRvcnkiLCJuYW1lIjoibGlicmFyeS9yZWdpc3RyeSIsImFjdGlvbnMiOlsicHVsbCJdfV19.e91bTpXSYNUTcUXr7zs62ZgCm1L6bhZbbW4ujXFY9Zzdkvy3DEHDssq6R4K9f5ESvv_LrWxxIxIXVREAATw-FaykcAewyjarC6Vlj2g0ea6D9L1HsIvsqtYcBOnHIJ5CRPJPhXWPwBtbujgNgbti-LLeVprOwaJ8fDk21UikmYFhX61_IobFukWw1ByXiNt8byU6tOrxkkDp-YXpz9y-XP5FdheGwNxOREph40znA9LddUcEuQUHB5WKQ3tdU4sqXOW3TUCtjLOl-kVREcus-83fLSuob1lZWRbzU9dEROd_5ZP4NNmD4ZY0DhcYbp75UqvB-MZIiC9MDeOheHAsPGB4Kqu2gBshRd_NJIrQkig7yvD2Wo7twn1KKSznHp6lcsK5phkkkWMVbZoD3qV76MqCDKVSkD2JOgQ0l4AhcYEGLtxx_ukk4NlDCYoljnGPw1oEynmFDROSvMg_bqhRVUF-5US83sU0l6YWwRCZT6StTvdSHp79wbSXgEn58-NO64AtVuMEb1XiDhDxtgaF0K61UwjBRmhpcCurw0laknBVVlta6otbfQcbyQn6ulsKgbrBKka-vkgo4_ymCyqnSXuZYC2Oz_PYawgGVz3s4JXhedoVWiUSDbyKYnFTXdtTign5oT6H6N-K1YKLoGSxma3uUwdDZP2hKH_UH_V9eOY
```

## 使用Token

在得到token后，我们就可以在API请求的Header中添加token信息，比如下载镜像的manifest

```
curl -H "Authorization: Bearer [token]" 192.168.1.103:8021/v2/library/registry/manifests/2.5.0
```

## 验证Token

当Registry接收到一个携带token的API请求时，Registry需要从以下几个方面来验证Token

* token的签发者（payload中的`iss`）是可信的，即和registry的配置参数`issuer`一致
* 确保registry是该token的观众，即payload中的`aud`与registry的配置参数`token-service`一致
* 检查payload中的`nbf`与`exp`确保token在有效期内
* 检查payload的access字段，确保该token能够访问该API
* 检查token的签名

## 扩展阅读

本文中使用到的RSA的私钥为（private\_key.pem）

```
-----BEGIN RSA PRIVATE KEY-----
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
-----END RSA PRIVATE KEY-----
```

### kid的生成

首先从RSA私钥中提取公钥，保存到文件`public_key.pem`中

```
$ openssl rsa -in private_key.pem -out public_key.pem -pubout
```

然后将公钥文件由pem格式生成der格式

```
$ openssl rsa -pubin -inform PEM -in public_key.pem -outform DER -out public_key.der
```

然后对der格式的公钥文件做sha256哈希

```
$ sha256sum public_key.der
3b3def0af2c85f060fb90c71494dc3105ea8b5a5bfc822ae0b5c89a54152e706
```

去掉十六进制的哈希值后四位得到

```
3b3def0af2c85f060fb90c71494dc3105ea8b5a5bfc822ae0b5c89a54152
```

然后用base32（RFC4648）进行编码（[base32在线编码网址](http://tomeko.net/online_tools/hex_to_base32.php?lang=en)），编码后得到

```
HM666CXSZBPQMD5ZBRYUSTODCBPKRNNFX7ECFLQLLSE2KQKS
```

每四位一组，中间用`:`隔开，得到kid的值

```
HM66:6CXS:ZBPQ:MD5Z:BRYU:STOD:CBPK:RNNF:X7EC:FLQL:LSE2:KQKS
```


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://pshizhsysu.gitbook.io/docker/registry/auth/tokenren-zheng-de-shi-xian.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
