JSON Web Token

引言

本文由我的同事 @like 投稿,介绍了 JWT(JSON Web Token)是什么,以及它的一些应用场景。

背景

前段时间看 Kubernetes 认证相关的内容,发现如果 Pod 要和 API Server 交互,需要为该 Pod 创建 ServiceAccount 并绑定相应的角色。比如 Dashboard 的配置文件如下:

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
# ------------------- Dashboard Service Account ------------------- #

apiVersion: v1
kind: ServiceAccount
metadata:
labels:
k8s-app: kubernetes-dashboard
name: kubernetes-dashboard
namespace: kube-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: kubernetes-dashboard-minimal
namespace: kube-system
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: kubernetes-dashboard-minimal
subjects:
- kind: ServiceAccount
name: kubernetes-dashboard
namespace: kube-system
# ------------------- Dashboard Deployment ------------------- #

kind: Deployment
apiVersion: apps/v1
metadata:
name: kubernetes-dashboard
namespace: kube-system
spec:
template:
metadata:
labels:
k8s-app: kubernetes-dashboard
spec:
containers:
- name: kubernetes-dashboard
image: k8s.gcr.io/kubernetes-dashboard-amd64:v1.10.1
serviceAccountName: kubernetes-dashboard
---

这里创建了名称为 kubernetes-dashboard 的 ServiceAccount,并绑定到角色 kubernetes-dashboard-minimal,最后在 PodSpecserviceAccountName 字段引用了该 ServiceAccount。

究竟 ServiceAccount 中存放了哪些内容,Kubernetes 背后又做了哪些事情呢?

1
kubectl -n kube-system get serviceaccount kubernetes-dashboard -o yaml
1
2
3
4
5
6
apiVersion: v1
kind: ServiceAccount
metadata:
# ...
secrets:
- name: kubernetes-dashboard-token-sm862

与 ServiceAccount 关联的 Secret 中持有 API Server 的 CA 证书和签名的 JSON Web Token(JWT)。

1
kubectl -n kube-system get secret kubernetes-dashboard -o yaml
1
2
3
4
5
6
7
8
9
apiVersion: v1
data:
ca.crt: (APISERVER'S CA BASE64 ENCODED)
namespace: a3ViZS1zeXN0ZW0=
token: (BEARER TOKEN BASE64 ENCODED)
kind: Secret
metadata:
# ...
type: kubernetes.io/service-account-token

这个签名的 JWT 可以用作 bearer token 来认证该 ServiceAccount,即在 HTTP 请求中添加头部 Authorization: Bearer <JWT>。通常情况下,该 Secret 会被挂载到 Pod 下每个容器的 /var/run/secrets/kubernetes.io/serviceaccount 路径,用以 API 访问。

JWT

JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret (with the HMAC algorithm) or a public/private key pair using RSA or ECDSA.

从JWT的定义看出其特点:

  • 信息格式为 json
  • 带签名,保证内容不被篡改

client-credentials-grant.png
目前 JWT 的主要用途是认证,用户成功登录后,认证服务器生成 token 并返回。接下来客户端所有的请求都会携带该 JWT,应用服务器验证 token 的合法性,识别出用户身份。

JWT数据结构

JWT 包含三部分内容,相互之间用 ‘.’ 分隔

  • Header
  • Payload
  • Signature

所以 JWT 字符串看起来如下:

1
xxxxx.yyyyy.zzzzz

头部由两部分组成: token 类型,为固定值 JWT;使用的签名算法,如 HMAC SHA256 或 RSA。
例子:

1
2
3
4
{
"alg": "HS256",
"typ": "JWT"
}

这个 JSON 对象经过 Base64Url 编码后形成 JWT 的第一部分。

Payload

JWT 的第二部分是 Payload,包含若干条声明(对用户的陈述),这些声明分为三种类型:

  • Registered claims - 预先定义的,包括以下7个字段

    • iss (Issuer): 签发人

    • sub (Subject): 主题

    • aud (Audience): 受众

    • exp (Expiration Time): 过期时间

    • nbf (Not Before): 生效时间

    • iat (Issued At): 签发时间

    • jti (JWT ID): 编号

    • 注意:这些声明的名称都是三字符长度,所以说 JWT 是紧凑的*

  • Public claims - 第三方扩展的,为避免冲突,使用前参考 IANA JSON Web Token Registry

  • Private claims - 由通信双方自定义的

同样,Payload 部分也要使用 Base64Url 编码。

Signature

Signature 部分是对 Header 和 Payload 的签名,防止数据篡改。
首先,指定一个密钥,该密钥只有认证服务器知道,然后根据 Header 中指定的签名算法,生成签名。

例如你想使用 HMAC SHA256 算法,那么生成签名的公式如下:

1
2
3
4
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)

签名能保证数据在传输过程中不被篡改,如果生成签名时使用的是私钥,能同时验证 JWT 生成方的身份。

注意:签名只保证信息不被篡改,Header 和 Payload 对任何人都是可读的。所以不要放一些敏感信息,除非是加密的。

最后把三部分字符串用逗号连接起来,就得到最终的 JWT。

实例

回到开头的例子,我们拿到了与 ServiceAccount 关联的JWT,利用官方提供的 Debugger 进行解码得到
Header:

1
2
3
4
{
"alg": "RS256",
"kid": ""
}

Payload:

1
2
3
4
5
6
7
8
{
"iss": "kubernetes/serviceaccount",
"kubernetes.io/serviceaccount/namespace": "kube-system",
"kubernetes.io/serviceaccount/secret.name": "kubernetes-dashboard-token-sm862",
"kubernetes.io/serviceaccount/service-account.name": "kubernetes-dashboard",
"kubernetes.io/serviceaccount/service-account.uid": "91a26bae-6bf1-11e9-aea5-b06ebfc62c4b",
"sub": "system:serviceaccount:kube-system:kubernetes-dashboard"
}

可以看出,Kubernetes 使用 RS256(RSA Signature With SHA-256)算法生成签名,默认使用的密钥是 API Server 的 TLS 私钥。这里采取 sub 字段的值作为当前的用户名, 进行后续鉴权。

注意: Kubernetes 的 Secret 中保存的数据是 base64 编码的,所以通过 kubectl 拿到的 token 需要先解码

另一个应用场景是 OpenID,在 OAuth 2.0 上扩展的认证协议,不在这里详细阐述。这里以 Google OAuth Playground 为例进行说明,整体过程与 OAuth 2.0 类似: 发送认证请求到 google -> 用户授权返回 code -> 拿 code 换取 access_token。不过这里的 scope 不再是需要授权的资源路径,而是 openid email/profile。认证 URL 可能如下:

1
2
3
4
5
6
7
8
9
10
https://accounts.google.com/o/oauth2/v2/auth?
client_id=424911365001.apps.googleusercontent.com&
response_type=code&
scope=openid%20email&
redirect_uri=https://oauth2-login-demo.example.com/code&
state=security_token%3D138r5719ru3e1%26url%3Dhttps://oauth2-login-demo.example.com/myHome&
login_hint=jsmith@example.com&
openid.realm=example.com&
nonce=0394852-3190485-2490358&
hd=example.com

最后返回的结果除了 access_token,还包括 id_token(由 Google 生成的标明用户身份的 JSON Web Token)。下面让我们看下里面有什么内容
Header:

1
2
3
4
5
{
"alg": "RS256",
"kid": "57b1928f2f63329f2e92f4f278f94ee1038c923c",
"typ": "JWT"
}

Payload

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"iss": "https://accounts.google.com",
"azp": "407408718192.apps.googleusercontent.com",
"aud": "407408718192.apps.googleusercontent.com",
"sub": "xxx",
"at_hash": "xxx",
"name": "xxx",
"picture": "xxx",
"given_name": "xxx",
"family_name": "xxx",
"locale": "zh-CN",
"iat": 1576927081,
"exp": 1576930681
}

可以看到该 JWT 包含了用户名、头像、语言等 profile 信息。

如果我们拿到这样的 JWT,怎么验证其是否是 Google 生成的?

Google把公钥放在 https://www.googleapis.com/oauth2/v3/certs 这个路径上

1
2
3
4
5
6
7
8
{
"alg": "RS256",
"n": "1Zi0-4bNwZ7gGefz17U2NoKT4xBq-nzAa899teHxB2Q9KVCZYDhbQkpiIrBNg2u8s6TtoSljpq6MJpsKJVJgpT70gDCCgaUsGNYql9-kwWNKd80FlU1sjDEGouUIVEoYHzooPyn9r027KzMnTv5LGRYjxb5lvGnb4UCw5MF_EeSTNpGD7zb0b6juXwBxPi0oIUbQxAcGgH3oS40hXAjJ_U2T3Hln8lBlnVhLbrh-5qF-uoYDxjtAY9XyEJQH_rGiRfXWgBfSM02t9DCB46sQbEMM2iLe7mkGrZtCHR4zbAsAP0s2VGqSmwszNTWqqsdOccbfXp3i_ThkR3pDdTSIQQ",
"use": "sig",
"kid": "57b1928f2f63329f2e92f4f278f94ee1038c923c",
"e": "AQAB",
"kty": "RSA"
}

可以看到,其 kid 与前面 JWT 的 header 中的 kid 匹配,下面我们用这个 key 来验证签名的合法性。

1
2
3
4
5
6
7
8
9
10
String e = <上面key的e字段>;
String n = <上面key的n字段>;
BigInteger modulus = new BigInteger(1, Base64.getUrlDecoder().decode(n));
BigInteger exponent = new BigInteger(1, Base64.getUrlDecoder().decode(e));
RSAPublicKeySpec spec = new RSAPublicKeySpec(modulus, exponent);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
PublicKey publicKey = keyFactory.generatePublic(spec);

String jwtStr = <你的id_token>;
Jwt jwt = Jwts.parser().setSigningKey(publicKey).parseClaimsJwt(jwtStr);

执行上述代码后,会显示如下异常信息:

1
JWT expired at 2019-12-21T12:18:01Z. Current time: 2019-12-22T09:22:02Z, a difference of 75841420 milliseconds.

哈哈 过期啦😂

参考内容

[1] JWT Introduction
[2] JSON Web Token入门教程
[3] Kubernetes Authenticating
[4] Google OpenID Connect

贝克街的流浪猫 wechat
您的打赏将鼓励我继续分享!
  • 本文作者: 贝克街的流浪猫
  • 本文链接: https://www.beikejiedeliulangmao.top/JSON-Web-Token/
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
  • 创作声明: 本文基于上述所有参考内容进行创作,其中可能涉及复制、修改或者转换,图片均来自网络,如有侵权请联系我,我会第一时间进行删除。