引言
本文由我的同事 @like 投稿,介绍了 JWT(JSON Web Token)是什么,以及它的一些应用场景。
背景
前段时间看 Kubernetes 认证相关的内容,发现如果 Pod 要和 API Server 交互,需要为该 Pod 创建 ServiceAccount 并绑定相应的角色。比如 Dashboard 的配置文件如下:
# ------------------- 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
,最后在 PodSpec
的 serviceAccountName
字段引用了该 ServiceAccount。
究竟 ServiceAccount 中存放了哪些内容,Kubernetes 背后又做了哪些事情呢?
kubectl -n kube-system get serviceaccount kubernetes-dashboard -o yaml
apiVersion: v1
kind: ServiceAccount
metadata:
# ...
secrets:
- name: kubernetes-dashboard-token-sm862
与 ServiceAccount 关联的 Secret 中持有 API Server 的 CA 证书和签名的 JSON Web Token(JWT)。
kubectl -n kube-system get secret kubernetes-dashboard -o yaml
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
- 带签名,保证内容不被篡改
目前 JWT 的主要用途是认证,用户成功登录后,认证服务器生成 token 并返回。接下来客户端所有的请求都会携带该 JWT,应用服务器验证 token 的合法性,识别出用户身份。
JWT数据结构
JWT 包含三部分内容,相互之间用 '.' 分隔
- Header
- Payload
- Signature
所以 JWT 字符串看起来如下:
xxxxx.yyyyy.zzzzz
Header
头部由两部分组成: token 类型,为固定值 JWT
;使用的签名算法,如 HMAC SHA256 或 RSA。
例子:
{
"alg": "HS256",
"typ": "JWT"
}
这个 JSON 对象经过 Base64Url 编码后形成 JWT 的第一部分。
Payload
JWT 的第二部分是 Payload,包含若干条声明(对用户的陈述),这些声明分为三种类型:
Registered claims - 预先定义的,包括以下7个字段, 注意:这些声明的名称都是三字符长度,所以说 JWT 是紧凑的
- iss (Issuer): 签发人
- sub (Subject): 主题
- aud (Audience): 受众
- exp (Expiration Time): 过期时间
- nbf (Not Before): 生效时间
- iat (Issued At): 签发时间
- jti (JWT ID): 编号
- Public claims - 第三方扩展的,为避免冲突,使用前参考 IANA JSON Web Token Registry
- Private claims - 由通信双方自定义的
同样,Payload 部分也要使用 Base64Url 编码。
Signature
Signature 部分是对 Header 和 Payload 的签名,防止数据篡改。
首先,指定一个密钥,该密钥只有认证服务器知道,然后根据 Header 中指定的签名算法,生成签名。
例如你想使用 HMAC SHA256 算法,那么生成签名的公式如下:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
签名能保证数据在传输过程中不被篡改,如果生成签名时使用的是私钥,能同时验证 JWT 生成方的身份。
注意:签名只保证信息不被篡改,Header 和 Payload 对任何人都是可读的。所以不要放一些敏感信息,除非是加密的。
最后把三部分字符串用逗号连接起来,就得到最终的 JWT。
实例
回到开头的例子,我们拿到了与 ServiceAccount 关联的JWT,利用官方提供的 Debugger 进行解码得到
Header:
{
"alg": "RS256",
"kid": ""
}
Payload:
{
"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 可能如下:
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:
{
"alg": "RS256",
"kid": "57b1928f2f63329f2e92f4f278f94ee1038c923c",
"typ": "JWT"
}
Payload
{
"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 这个路径上
{
"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 来验证签名的合法性。
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);
执行上述代码后,会显示如下异常信息:
JWT expired at 2019-12-21T12:18:01Z. Current time: 2019-12-22T09:22:02Z, a difference of 75841420 milliseconds.
哈哈 过期啦😂
文章说明
更多有价值的文章均收录于贝贝猫的文章目录
版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
创作声明: 本文基于下列所有参考内容进行创作,其中可能涉及复制、修改或者转换,图片均来自网络,如有侵权请联系我,我会第一时间进行删除。
参考内容
[1] JWT Introduction
[2] JSON Web Token入门教程
[3] Kubernetes Authenticating
[4] Google OpenID Connect