JWT 授权
首先了解一下 JWT( JSON Web Token ),是一种多方传递可信 JSON 数据的方案,一个 JWT token 由.
分隔的三部分组成:{Header}.{Payload}.{Signature}
,其中 Header 是 Base64 编码的 JSON 数据,包含令牌类型typ
、签名算法alg
以及秘钥 ID kid
等信息;Payload 是需要传递的 claims 数据,也是 Base64 编码的 JSON 数据,其中有些字段是 JWT 标准已有的字段如:exp
、iat
、iss
、sub
和aud
等,也可以根据需求添加自定义字段;Signature 是对前两部分的签名,防止数据被篡改,以此确保 token 信息是可信的,更多参考 Introduction to JSON Web Tokens。Istio 中验签所需公钥由 RequestAuthentication 资源的 JWKS 配置提供,详见终端用户认证。
前面介绍了 HTTP
、TCP
、gRPC
等不同协议的流量授权,而 JWT 授权则是对终端用户的访问控制,试想某个内部服务需要管理员才能够访问,这时候就需要验证终端用户的角色是否为管理员,可以在 JWT claims 中带有管理员角色信息,然后在授权策略中对该角色授权。不同协议的流量授权在操作to
方面有比较多的示范,本节则主要在来源from
和自定义条件 when
做示范。
本节使用 Istio 示例中的 httpbin 服务做演示,涉及不同场景下 JWT 授权的应用,主要包括:
- 无授权策略情况下的 JWT 认证
- 任意非空的 JWT 授权
- Principal 条件匹配授权
- Claims 条件匹配授权
- 分阶段认证和授权
准备工作
- Istio 环境
- Istio 1.5.1 示例下载(httpbin 示例)
httpbin 服务部署
创建命名空间
为了后续演示方便,设置命名空间环境变量 NS=authz-jwt
,然后创建命名空间。
$ export NS=authz-jwt
$ kubectl create namespace $NS
Sidecar 自动注入
为命名空间开启 Sidecar 自动注入
$ kubectl label namespace $NS istio-injection=enabled
部署 httpbin 服务
在下载的 Istio 目录下部署 httpbin 服务,并检查 pod
、service
的创建情况
$ cd {istio release path}
kubectl -n $NS apply -f samples/httpbin/httpbin.yaml
$ kubectl -n $NS get po
NAME READY STATUS RESTARTS AGE
httpbin-779c54bf49-df6mc 2/2 Running 0 6h9m
$ kubectl -n $NS get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
httpbin ClusterIP 10.111.151.12 <none> 8000/TCP 6h9m
httpbin 网关
添加 httpbin 服务的 gateway
为了避免与其他网关产生冲突,网关指定 HOST 为 authz-jwt.local
,这样在测试时通过指定 HOST 确定是路由到 httpbin 服务。
$ kubectl apply -f - <<EOF
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
name: httpbin-gateway
namespace: $NS
spec:
selector:
istio: ingressgateway
servers:
- port:
number: 80
name: http
protocol: HTTP
hosts:
- "authz-jwt.local"
---
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: httpbin
namespace: $NS
spec:
hosts:
- "*"
gateways:
- httpbin-gateway
http:
- route:
- destination:
host: httpbin
port:
number: 8000
EOF
获取 Ingress 网关的 IP 和 PORT
根据环境不同获取 ingress IP 和 Port 参考determining-the-ingress-ip-and-ports
# 演示使用的是 minikube 环境
$ export INGRESS_IP=$(minikube ip)
$ export INGRESS_PORT=$(kubectl -n istio-system get service istio-ingressgateway -o jsonpath='{.spec.ports[?(@.name=="http2")].nodePort}')
验证网关
请求正常,响应 200
。
$ curl -I -H "Host: authz-jwt.local" http://$INGRESS_IP:$INGRESS_PORT/headers
HTTP/1.1 200 OK
现在我们完成准备工作,部署了一个可以通过 ingress 网关访问的 httpbin 服务,接下来开始 JWT 授权相关的内容。
无授策略权情况下的 JWT 认证
要使用 JWT 授权的前提是有有效的 JWT 终端身份认证,所以在使用 JWT 授权前首先要为服务添加终端身份认证即 RequestAuthentication ,更多参考认证。
添加 RequestAuthentication
本节使用 Istio 代码库中提供的用于 JWT 演示的配置,包括 JWKS 端点配置,以及两个测试用的 token :demo 和 groups-scope
。其中 demo 是一个普通 token,claims 有 JWT 的基础属性;groups-scope
是一个带有自定义属性的 token,claims 除了基础属性还包括 group 和 scope,这两个 token 详细的 claims 结构在后续应用中会有介绍,相关连接:
$ kubectl apply -f - <<EOF
apiVersion: "security.istio.io/v1beta1"
kind: "RequestAuthentication"
metadata:
name: "jwt-example"
namespace: $NS
spec:
selector:
matchLabels:
app: httpbin
jwtRules:
- issuer: "testing@secure.istio.io"
jwksUri: "https://raw.githubusercontent.com/istio/istio/release-1.5/security/tools/jwt/samples/jwks.json"
EOF
JWT 认证测试
默认 JWT 认证的 token 是以 Bearer
为前缀放在 Authorization header 中,如:Authorization: Bearer token
。
使用以下三种方式测试服务,看请求的响应情况:
- 不带 Authorization header
- Authorization header 携带一个无效的 token
- Authorization header 携带一个有效的 token
不带 Authorization header 的请求正常,响应 200
。
$ curl -I -H "Host: authz-jwt.local" http://$INGRESS_IP:$INGRESS_PORT/headers
HTTP/1.1 200 OK
携带无效 token 的请求被绝,响应 401
。
$ curl -I -H "Authorization: Bearer invalidToken" -H "Host: authz-jwt.local" http://$INGRESS_IP:$INGRESS_PORT/headers
HTTP/1.1 401 Unauthorized
测试有效 token 前要先获取 demo token 并设置为环境变量,以便后续演示中使用。
$ export TOKEN=$(curl https://raw.githubusercontent.com/istio/istio/release-1.5/security/tools/jwt/samples/demo.jwt -s)
携带有效 token 的请求正常,响应200
。
$ curl -I -H "Authorization: Bearer $TOKEN" -H "Host: authz-jwt.local" http://$INGRESS_IP:$INGRESS_PORT/headers
HTTP/1.1 200 OK
添加 RequestAuthentication
后,并不是要求所有请求都要带有 JWT token,因为 RequestAuthentication
只负责验证 token 的有效性,token 的有无以及是否授权访问由 AuthorizationPolicy 的 JWT 授权策略决定。所以在只有 RequestAuthentication 时,可以同时支持无 token 请求和带有有效 token 的请求,而带有无效 token 的请求将被拒绝,此时 JWT 认证是一个非必要条件。
任意非空的 JWT 授权
在只有 RequestAuthentication
时不带 token 的请求是可以正常访问的,而需求可能会要求全部请求必须经过认证才能访问,这就需要使用 JWT 授权策略。
AuthorizationPolicy
rule 规则中与 JWT 相关的字段包括:
field | sub field | JWT claims |
---|---|---|
from.source | requestPrincipals | iss/sub |
from.source | notRequestPrincipals | iss/sub |
when.key | request.auth.principal | iss/sub |
when.key | request.auth.audiences | aud |
when.key | request.auth.presenter | azp |
when.key | request.auth.claims[key] | JWT 全部属性 |
其中from.source
的requestPrincipals
、notRequestPrincipals
和when.key
的request.auth.principal
都是对 Principal 条件的策略,Principal 由 JWT claims 的iss
和sub
用/
拼接组成{iss}/{sub}
,request.auth.audiences
和request.auth.presenter
分别对应 claims 的aud
和azp
属性,request.auth.claims[key]
则可以通过key
值获取 JWT claims 中的任意值作为条件。
这些字段的匹配都遵循授权的四种匹配规则:完全匹配、前缀匹配、后缀匹配和存在匹配,详见认证策略,其中存在匹配(*
)表示该字段可以匹配任意内容,但是不能为空,和不指定字段是不一样的,不指定是包括空在内的任意内容,所以使用存在匹配可以满足对任意非空的 JWT 授权的需求。
添加 AuthorizationPolicy
添加一个 from.source
为 requestPrincipals: ["*"]
的 JWT 授权策略,允许任意非空 Principal 的请求。
$ kubectl apply -f - <<EOF
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: require-jwt
namespace: $NS
spec:
selector:
matchLabels:
app: httpbin
action: ALLOW
rules:
- from:
- source:
requestPrincipals: ["*"]
EOF
JWT 授权测试
不带 Authorization header 的请求被拒绝,响应 403
.
$ curl -I -H "Host: authz-jwt.local" http://$INGRESS_IP:$INGRESS_PORT/headers
HTTP/1.1 403 Forbidden
带有有效 token 的请求访问正常,响应 200
。
$ curl -I -H "Authorization: Bearer $TOKEN" -H "Host: authz-jwt.local" http://$INGRESS_IP:$INGRESS_PORT/headers
HTTP/1.1 200 OK
在添加的 AuthorizationPolicy
中带有 JWT 相关条件字段后,不带 token 的请求将被拒绝,此时 JWT 认证变为了必要条件。
Principal 条件
前面已经介绍 from.source
和 when.key
中与 Principal 相关的三个字段,这里使用 source.requestPrincipals
做为示例,来看下 Principal 条件的应用。
要设置具体条件首先要看下 JWT 的 claims 信息,通过echo $TOKEN | cut -d '.' -f2 - | base64 -d -
管道操作解码 token 的 Payload 部分,查看 JWT claims 结构如下:
$ echo $TOKEN | cut -d '.' -f2 - | base64 -d -
{
"exp" : 4685989700,
"foo" : "bar",
"iss" : "testing@secure.istio.io",
"sub" : "testing@secure.istio.io",
"iat" : 1532389700
}
测试 principal 条件
根据 token claims 结构修改source.requestPrincipals
条件为testing@secure.istio.io/testing@secure.istio.io
。
$ kubectl patch AuthorizationPolicy require-jwt -n $NS --type merge -p '
spec:
rules:
- from:
- source:
requestPrincipals: ["testing@secure.istio.io/testing@secure.istio.io"]
'
或者使用等效的自定义条件when
的request.auth.principal
$ kubectl patch AuthorizationPolicy require-jwt -n $NS --type merge -p '
spec:
rules:
- when:
- key: request.auth.principal
values: ["testing@secure.istio.io/testing@secure.istio.io"]
'
请求正常,响应 200
。
$ curl -I -H "Authorization: Bearer $TOKEN" -H "Host: authz-jwt.local" http://$INGRESS_IP:$INGRESS_PORT/headers
HTTP/1.1 200 OK
拒绝授权示范
这时我们将 requestPrincipals
规则改为其它值,如requestPrincipals: ["testing@secure.istio.io/none"]
。
kubectl patch AuthorizationPolicy require-jwt -n $NS --type merge -p '
spec:
rules:
- from:
- source:
requestPrincipals: ["testing@secure.istio.io/none"]
'
请求被拒绝,响应 403
。
$ curl -I -H "Authorization: Bearer $TOKEN" -H "Host: authz-jwt.local" http://$INGRESS_IP:$INGRESS_PORT/headers
HTTP/1.1 403 Forbidden
恢复正常授权。
kubectl patch AuthorizationPolicy require-jwt -n $NS --type merge -p '
spec:
rules:
- from:
- source:
requestPrincipals: ["testing@secure.istio.io/testing@secure.istio.io"]
'
Claims 条件
更多的有关 JWT 属性的规则可以通过自定义条件when
补充,其中request.auth.principal
与source.requestPrincipals
一致已经演示,
request.auth.audiences
和request.auth.presenter
的使用不再赘述,接下来在 Principal 条件基础上增加 claims 条件,看下自定义条件request.auth.claims[]
的应用。
groups-scope token 测试 claims 条件
为了丰富 JWT claims 信息,增加另一个 JWT token: groups-scope。
获取 groups-scope token,并解码 JWT claims,其中包括两个自定义的 claims :scope
和groups
。
$ export TOKEN_GROUP=$(curl https://raw.githubusercontent.com/istio/istio/release-1.5/security/tools/jwt/samples/groups-scope.jwt -s) && echo $TOKEN_GROUP | cut -d '.' -f2 - | base64 -d -
{
"exp" : 3537391104,
"scope" : [
"scope1",
"scope2"
],
"iss" : "testing@secure.istio.io",
"groups" : [
"group1",
"group2"
],
"sub" : "testing@secure.istio.io",
"iat" : 1537391104
}
结合 JWT claims 结构这里使用 groups 作为自定义条件,如:仅允许 group1
。
$ kubectl patch AuthorizationPolicy require-jwt -n $NS --type merge -p '
spec:
rules:
- from:
- source:
requestPrincipals: ["testing@secure.istio.io/testing@secure.istio.io"]
when:
- key: request.auth.claims[groups]
values: ["group1"]
'
测试 $TOKEN 请求被拒绝,响应403
。
curl -I -H "Authorization: Bearer $TOKEN" -H "Host: authz-jwt.local" http://$INGRESS_IP:$INGRESS_PORT/headers
HTTP/1.1 403 Forbidden
测试 $TOKEN_GROUP 请求正常,响应200
。
$ curl -I -H "Authorization: Bearer $TOKEN_GROUP" -H "Host: authz-jwt.local" http://$INGRESS_IP:$INGRESS_PORT/headers
HTTP/1.1 200 OK
拒绝授权示范
尝试一个不在 groups-scope token 内的 group 值,如:values: ["group3"]
。
$ kubectl patch AuthorizationPolicy require-jwt -n $NS --type merge -p '
spec:
rules:
- from:
- source:
requestPrincipals: ["testing@secure.istio.io/testing@secure.istio.io"]
when:
- key: request.auth.claims[groups]
values: ["group3"]
'
测试 $TOKEN_GROUP 请求被拒绝,响应403
。
curl -I -H "Authorization: Bearer $TOKEN_GROUP" -H "Host: authz-jwt.local" http://$INGRESS_IP:$INGRESS_PORT/headers
HTTP/1.1 403 Forbidden
恢复正常授权。
$ kubectl patch AuthorizationPolicy require-jwt -n $NS --type merge -p '
spec:
rules:
- from:
- source:
requestPrincipals: ["testing@secure.istio.io/testing@secure.istio.io"]
when:
- key: request.auth.claims[groups]
values: ["group1"]
'
分阶段认证和授权
现在每次请求对 JWT 的认证和授权都是在 httpbin 服务上,而对于真实场景的请求到达内部服务,往往要经过 n 个服务,如果恰巧这个验证是最后一个服务,当因为 token 无效或者没有 token 导致请求失败时,服务的响应时间大大延长,并造成资源的浪费,所以可以将 token 的验证前置到 Ingress 网关。
通过前面的实践可以知道添加 RequestAuthentication 仅对带有 Authorization header 请求做认证,不影响无 Authorization header 的请求。具体是否需要分阶段验证,以及在什么位置验证,需要根据业务场景考虑,一般越是顶层条件越靠前如:from.source.requestPrincipals
、to.operation.hosts
,而when.request.auth.claims[group/scope]
和to.operation.methods/paths
组合可以在相关服务做详细的访问控制。
另外需要注意的是如果调用链路有多次使用同一个 token,则必须在 RequestAuthentication 的jwtRules
中开启forwardOriginalToken: true
以将 Authorization header 向下传递,也可以通过 fromHeaders / fromParams 携带多个不同场景的 token,具体参考 JWTRule 。说到 token 的传递,Authorization header 也可以在服务与服务间调用时添加,所以终端用户的定义并不限定为客户端,任何一个发起调用的服务都是一个终端用户。
Ingress JWT 认证
测试当前无效 token 请求,响应 401
,并且在响应的 header 中有上游主机处理请求消耗的时间x-envoy-upstream-service-time: 1
,通过这个 header 的有无可以确定请求是否是被 httpbin 服务拒绝,有是被 httpbin 服务拒绝,没有则是被网关拒绝。
$ curl -I -H "Authorization: Bearer invalidToken" -H "Host: authz-jwt.local" http://$INGRESS_IP:$INGRESS_PORT/headers
HTTP/1.1 401 Unauthorized
...
x-envoy-upstream-service-time: 1
为 Ingress 开启 JWT 认证。
$ kubectl apply -f - <<EOF
apiVersion: "security.istio.io/v1beta1"
kind: "RequestAuthentication"
metadata:
name: "jwt-gateway"
namespace: istio-system
spec:
selector:
matchLabels:
app: istio-ingressgateway
jwtRules:
- issuer: "testing@secure.istio.io"
jwksUri: "https://raw.githubusercontent.com/istio/istio/release-1.5/security/tools/jwt/samples/jwks.json"
forwardOriginalToken: true
EOF
再测试无效 token 请求,同样响应 401
,但没有了x-envoy-upstream-service-time
header,说明请求是在网关被拒绝。
$ curl -I -H "Authorization: Bearer invalidToken" -H "Host: authz-jwt.local" http://$INGRESS_IP:$INGRESS_PORT/headers
HTTP/1.1 401 Unauthorized
Ingress JWT 授权
前面只是 Ingress 的 JWT 认证,接下来看下在 Ingress 和 httpbin 服务使用不同策略时的响应情况,结合网关的入口特点可以添加根据 HOST 的不同限定scope
的授权,如:访问host=authz-jwt.local
要求scpoe=scope1
。
Ingress 添加 AuthorizationPolicy
。
$ kubectl apply -f - <<EOF
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: require-jwt
namespace: istio-system
spec:
selector:
matchLabels:
app: istio-ingressgateway
action: ALLOW
rules:
- when:
- key: request.auth.claims[scope]
values: ["scope1"]
to:
- operation:
hosts:
- authz-jwt.local
EOF
token 使用 $TOKEN_GROUP 请求正常,响应 200
。
$ curl -I -H "Authorization: Bearer $TOKEN_GROUP" -H "Host: authz-jwt.local" http://$INGRESS_IP:$INGRESS_PORT/headers
HTTP/1.1 200 OK
token 使用 $TOKEN 请求被网关拒绝,响应 403
。
$ curl -I -H "Authorization: Bearer $TOKEN" -H "Host: authz-jwt.local" http://$INGRESS_IP:$INGRESS_PORT/headers
HTTP/1.1 403 Forbidden
结合 Claims 条件的拒绝授权示范,httpbin AuthorizationPolicy 对group=group3
授权。
$ kubectl patch AuthorizationPolicy require-jwt -n $NS --type merge -p '
spec:
rules:
- from:
- source:
requestPrincipals: ["testing@secure.istio.io/testing@secure.istio.io"]
when:
- key: request.auth.claims[groups]
values: ["group3"]
'
token 使用 $TOKEN_GROUP 请求被 httpbin 服务拒绝,响应403
。
$ curl -I -H "Authorization: Bearer $TOKEN_GROUP" -H "Host: authz-jwt.local" http://$INGRESS_IP:$INGRESS_PORT/headers
HTTP/1.1 403 Forbidden
...
x-envoy-upstream-service-time: 1
恢复正常授权。
$ kubectl patch AuthorizationPolicy require-jwt -n $NS --type merge -p '
spec:
rules:
- from:
- source:
requestPrincipals: ["testing@secure.istio.io/testing@secure.istio.io"]
when:
- key: request.auth.claims[groups]
values: ["group1"]
'
总结如下表,通过 ingress 和 httpbin 分阶段授权策略的搭配,可以将不同授权的 token 在不同阶段进行验证拦截。
ingress 状态 | httpbin 策略 | Token | Ingress 状态 | httpbin 状态 |
---|---|---|---|---|
scpoe=scope1 | group=group1 | $GROUP_TOKEN | √ | √ |
scpoe=scope1 | group=group1 | $TOKEN | 拒绝 | - |
scpoe=scope1 | group=group3 | $GROUP_TOKEN | √ | 拒绝 |
小结
本节我们主要实践了在来源from
和自定义条件when
中与终端用户相关的属性条件,通过 JWT 标准的 iss
、sub
、aud
和azp
以及合理的自定义 claims 设计可以满足大部分访问控制场景的需求,既可以做签发者这样基础的授权,也可以做终端用户到服务接口/方法级的访问控制。
参考
- Introduction to JSON Web Tokens
- RFC 7519 - JSON Web Token (JWT)
- JSON Web Key Set
- Istio Concepts / Security#Request authentication
- Istio Tasks / Authorization with JWT
- Istio Reference / Authorization Policy
- Istio Reference / Authorization Policy Conditions
- OpenID Connect | Google Identity Platform | Google Developers