Envoy JWT
JWT 认证
-
目前,无状态的HTTP协议用以跟踪用户状态的常用解决方案有两种
-
基于Session的认证
- 用户成功登录后,服务器为其创建并存储一个Session(map结构,有SessionID),并通过Set-Cookie将SessionID返回给客户端作为Cookie
-
客户端随后的请求都将在标头中通过Cookie附带该SessionID,并由服务器端完成验证
-
基于Token的认证
-
用户成功登录后,服务器使用Secret创建JWT,并将JWT返回给客户端进行存储
-
客户端在随后的请求标头中附带JWT,并由服务器完成验证
-
-
-
传统Session认证的弊端
-
服务器端通常将Session存储于内存中,数量较大时,session存储具有相当程度的开销
-
Session成为了应用的状态数据,影响服务器的横向扩展
-
用户的cookie被截获后,易导致CSRF攻击
-
-
JWT:JSON Web Token
-
JWS令牌格式:Header.Payload.Signature,但要编码为base64,也可以进行加密(JWE)
-
在分布式环境中实现跨域认证时,无需服务器端存储session;
-
服务器认证以后生成一个 JSON 对象返回给客户端,而后客户端的每次请求都要附加此对象
-
为了防止客户端篡改此JSON对象,通常需要对其进行签名;
-
-
JSON Web Signature
JWT authentication flow
Envoy JWT
-
Envoy基于JWT Authentication过滤器完成终端用户认证
-
基于过滤器核验JWT的签名、受众和颁发者以确定其携带有有效的令牌;若验证失败则请求被拒绝;
-
支持在请求的各种条件下检查JWT,例如仅针对特定路径;
-
支持从请求的各个位置提取JWT,并可合并同一请求中的多个JWT需求;
-
JWT签名所需的JWKS(Json Web Key Set)可在过滤器配置中内联指定,也可通过HTTP/HTTPS从远程服务器获取
-
-
配置时,需要使用名称envoy.filters.http.jwt_authn配置此过滤器,主要由两个字段
-
providers:定义如何验证JWT,例如提取令牌的位置、获取公钥的位置以及输出Payload的位置等;
-
rules:定义匹配规则及相关要求;
-
Envoy JWT过滤器配置语法
--
listeners:
...
filter_chains:
...
name: ...
filters: # 组成过滤器链的单个网络过滤器列表,用于与侦听器建立连接。顺序很重要,因为过滤器在连接事件发生时按顺序处理。注意:如果过滤器列表为空,则默认关闭连接。
name: envoy.filters.network.http_connection_manager # 过滤器配置的名称。取决于typed_config配置的过滤器指定的名称。
typed_config: # 过滤器特定配置,这取决于被实例化的过滤器。
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
...
http_filters:
- name: envoy.filters.http.jwt_authn
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.jwt_authn.v3.JwtAuthentication
providers:
provider1: # 定义provider名称及其相关的属性,以指定如何验证JWT;
issuer: ... # JWT的签发者,通常是URL或email;
audiences: [] # 允许访问的JWT受众列表,含有此处指定的audiences其中之一的JWT即可被接受;
remote_jwks: # 可通过HTTP/HTTPS远程获取到的JWK,因此内嵌的最重要字段即为uri;
http_uri:
uri: ... # HTTP Server URI,格式为http(s)://SERVER:PORT/PATH
cluster: ... # Envoy Cluster Manager中定义的相应的集群名称,该集群是uri获取的位置
timeout: ... # 最大超时时长
cache_duration: # JWKS缓存的超时时长,默认为5分钟
seconds: ...
async_fetch: {...} #
retry_policy: # 重试Jwks的策略。可选择的默认情况下处于禁用状态。
retry_back_off: # 重试操作的退避策略
base_interval: {...} # 基准时间间隔
max_interval: {...} # 最大时间间隔
num_retries: {...} # 重试次数
forward: ... # 布尔型值,定义是否无需在认证成功后从请求中移除JWT,默认为false,即需要移除;
local_jwks: {...} # 本地数据源可以访问到的JWK,本地DataSource格式,支持filename、inline_string和inline_bytes;
from_headers: [] # 定义可通过哪些HTTP协议标头获取JWT,支持Bear和自定义的标头,例如“Authorization: Bear <token>”等;
from_params: [] # 指定可以从哪些URL param中获取JWT,例如“/path?jwt_token=<JWT>”中的jwt_token参数;
from_cookies: [] # JWT包含在哪些Cookie中;
forward_payload_header: ... # 指定用于将经过验证的JWT的payload转发至后端的标头;
pad_forward_payload_header: ...
payload_in_metadata: ...
header_in_metadata: ...
clock_skew_seconds: ...
jwt_cache_config: {...}
provider2:
...
rules: # 定义特定路由条件下的JWT验证要求;
match: {...} # 路由匹配条件
requires: # JWT验证要求,以下验证方式仅能使用其中一种;
provider_name: ... # 需要的provider名称;
provider_and_audiences: {...} # 需要的provider和audiences;
requires_any: {...} # 由requirements参数指定需要的providers列表,其中任何一个provider验证通过,结果即为通过;
requires_all: {...} # 由requirements参数指定需要的providers列表,其中所有的provider验证通过,结果才为通过;
allow_missing_or_failed: {...} # 即便JWT缺失或验证失败,结果依然通过;example_jwks_cluster
allow_missing: {...}
requirement_name: ...
filter_state_rules: {...}
bypass_cors_preflight: ...
requirement_map: {...}
JWT认证配置示例
front-envoy.yaml
node:
id: front-envoy
cluster: mycluster
admin:
profile_path: /tmp/envoy.prof
access_log_path: /tmp/admin_access.log
address:
socket_address:
address: 0.0.0.0
port_value: 9901
layered_runtime:
layers:
- name: admin
admin_layer: {}
static_resources:
listeners:
- address:
socket_address:
address: 0.0.0.0
port_value: 80
name: listener_http
filter_chains:
- filters:
- name: envoy.filters.network.http_connection_manager
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
stat_prefix: ingress_http
codec_type: AUTO
access_log:
- name: envoy.access_loggers.file
typed_config:
"@type": type.googleapis.com/envoy.extensions.access_loggers.file.v3.FileAccessLog
path: "/dev/stdout"
log_format:
json_format: {"start": "[%START_TIME%] ", "method": "%REQ(:METHOD)%", "url": "%REQ(X-ENVOY-ORIGINAL-PATH?:PATH)%", "protocol": "%PROTOCOL%", "status": "%RESPONSE_CODE%", "respflags": "%RESPONSE_FLAGS%", "bytes-received": "%BYTES_RECEIVED%", "bytes-sent": "%BYTES_SENT%", "duration": "%DURATION%", "upstream-service-time": "%RESP(X-ENVOY-UPSTREAM-SERVICE-TIME)%", "x-forwarded-for": "%REQ(X-FORWARDED-FOR)%", "user-agent": "%REQ(USER-AGENT)%", "request-id": "%REQ(X-REQUEST-ID)%", "authority": "%REQ(:AUTHORITY)%", "upstream-host": "%UPSTREAM_HOST%", "remote-ip": "%DOWNSTREAM_REMOTE_ADDRESS_WITHOUT_PORT%"}
#text_format: "[%START_TIME%] \"%REQ(:METHOD)% %REQ(X-ENVOY-ORIGINAL-PATH?:PATH)% %PROTOCOL%\" %RESPONSE_CODE% %RESPONSE_FLAGS% %BYTES_RECEIVED% %BYTES_SENT% %DURATION% %RESP(X-ENVOY-UPSTREAM-SERVICE-TIME)% \"%REQ(X-FORWARDED-FOR)%\" \"%REQ(USER-AGENT)%\" \"%REQ(X-REQUEST-ID)%\" \"%REQ(:AUTHORITY)%\" \"%UPSTREAM_HOST%\" \"%DOWNSTREAM_REMOTE_ADDRESS_WITHOUT_PORT%\"\n"
stat_prefix: ingress_http
route_config:
name: local_route
virtual_hosts:
- name: vh_001
domains: ["*"]
routes:
- match:
prefix: "/"
route:
cluster: demoapp
http_filters:
- name: envoy.filters.http.jwt_authn
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.jwt_authn.v3.JwtAuthentication
providers:
jwt_provider:
issuer: "http://keycloak:8080"
#forward: true
remote_jwks:
http_uri:
uri: "http://keycloak:8080/auth/realms/demo/protocol/openid-connect/certs"
cluster: keycloak
timeout: 1s
rules:
- match:
prefix: "/"
requires:
provider_name: jwt_provider
- name: envoy.filters.http.router
typed_config: {}
clusters:
- name: demoapp
connect_timeout: 0.25s
type: STRICT_DNS
lb_policy: ROUND_ROBIN
load_assignment:
cluster_name: demoapp
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: demoapp
port_value: 8080
- name: keycloak
connect_timeout: 0.25s
type: STRICT_DNS
lb_policy: ROUND_ROBIN
load_assignment:
cluster_name: keycloak
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: keycloak
port_value: 8080
docker-compose.yaml
version: '3.3'
services:
front-envoy:
image: envoyproxy/envoy-alpine:v1.20.0
volumes:
- ./front-envoy.yaml:/etc/envoy/envoy.yaml
networks:
envoymesh:
ipv4_address: 172.31.100.10
aliases:
- front-envoy
expose:
# Expose ports 80 (for general traffic) and 9901 (for the admin server)
- "80"
- "9901"
ports:
- "8080:80"
demoapp:
image: ikubernetes/demoapp:v1.0
hostname: "upstream-demoapp"
environment:
- "PORT=8080"
networks:
envoymesh:
aliases:
- upstream-demoapp
expose:
- "8080"
keycloak:
image: quay.io/keycloak/keycloak:15.0.2
networks:
envoymesh:
ipv4_address: 172.31.100.66
environment:
KEYCLOAK_USER: admin
KEYCLOAK_PASSWORD: magedu.com
DB_VENDOR: postgres
DB_ADDR: postgres
DB_DATABASE: keycloak
DB_USER: kcadmin
DB_PASSWORD: kcpass
ports:
- "8081:8080"
depends_on:
- postgres
postgres:
image: postgres:13.4-alpine
restart: unless-stopped
networks:
envoymesh:
ipv4_address: 172.31.100.67
aliases:
- db
environment:
POSTGRES_DB: keycloak
POSTGRES_USER: kcadmin
POSTGRES_PASSWORD: kcpass
volumes:
- "postgres_data:/var/lib/postgresql/data"
networks:
envoymesh:
driver: bridge
ipam:
config:
- subnet: 172.31.100.0/24
volumes:
postgres_data:
driver: local
参考文档
https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_filters/jwt_authn_filter#config-http-filters-jwt-authn
https://www.envoyproxy.io/docs/envoy/latest/api-v3/extensions/filters/http/jwt_authn/v3/config.proto#extension-envoy-filters-http-jwt-authn