天天看点

Envoy JWT

JWT 认证

  • 目前,无状态的HTTP协议用以跟踪用户状态的常用解决方案有两种
  • 基于Session的认证
  • 用户成功登录后,服务器为其创建并存储一个Session(map结构,有SessionID),并通过Set-Cookie将SessionID返回给客户端作为Cookie
  • 客户端随后的请求都将在标头中通过Cookie附带该SessionID,并由服务器端完成验证
  • 基于Token的认证
  • 用户成功登录后,服务器使用Secret创建JWT,并将JWT返回给客户端进行存储
  • 客户端在随后的请求标头中附带JWT,并由服务器完成验证
Envoy JWT
  • 传统Session认证的弊端
  • 服务器端通常将Session存储于内存中,数量较大时,session存储具有相当程度的开销
  • Session成为了应用的状态数据,影响服务器的横向扩展
  • 用户的cookie被截获后,易导致CSRF攻击
  • JWT:JSON Web Token
  • JWS令牌格式:Header.Payload.Signature,但要编码为base64,也可以进行加密(JWE)
  • 在分布式环境中实现跨域认证时,无需服务器端存储session;
  • 服务器认证以后生成一个 JSON 对象返回给客户端,而后客户端的每次请求都要附加此对象
  • 为了防止客户端篡改此JSON对象,通常需要对其进行签名;
Envoy JWT

JSON Web Signature

Envoy JWT

JWT authentication flow

Envoy JWT

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​​

继续阅读