diff --git a/deploy/dev/docker-compose.yml b/deploy/dev/docker-compose.yml index 65d7a88..214c2d9 100644 --- a/deploy/dev/docker-compose.yml +++ b/deploy/dev/docker-compose.yml @@ -38,8 +38,6 @@ services: rl-redis: image: redis:${REDIS_VERSION:-8} container_name: ${REDIS_CONTAINER_NAME:-rl-redis-dev-server} - profiles: - - infra ports: - "6380:6379" restart: unless-stopped @@ -86,42 +84,62 @@ services: condition: service_started envoy-gateway: - build: - context: ../deploy/dev/envoy - image: envoy-gateway:latest - container_name: ${ENVOY_GATEWAY_CONTAINER_NAME:-envoy-gateway-dev-server} + image: envoyproxy/envoy:v1.31-latest + container_name: juwan-envoy-gateway + restart: unless-stopped + command: + - /usr/local/bin/envoy + - -c + - /etc/envoy/envoy.yaml + - --log-level + - info ports: - - "8080:8080" - - "9901:9901" + - "18080:8080" + volumes: + - ./envoy.yaml:/etc/envoy/envoy.yaml:ro depends_on: authz-adapter: condition: service_started - required: false - user-api: + users-api: + condition: service_started + player-api: + condition: service_started + game-api: + condition: service_started + shop-api: + condition: service_started + order-api: + condition: service_started + wallet-api: + condition: service_started + community-api: + condition: service_started + objectstory-api: condition: service_started - required: false email-api: condition: service_started - required: false - restart: unless-stopped ratelimit: - image: ratelimit:latest + image: envoyproxy/ratelimit:05c08d03 container_name: rl-service restart: unless-stopped + command: /bin/ratelimit environment: - REDIS_SOCKET_TYPE=tcp - REDIS_URL=rl-redis:6379 - USE_STATSD=false - RUNTIME_ROOT=/data - RUNTIME_SUBDIRECTORY=ratelimit - - RUNTIME_WATCH_ROOT=true # 热重载 + - RUNTIME_WATCH_ROOT=true - LOG_LEVEL=debug volumes: - ./rls/ratelimit.yaml:/data/ratelimit/config/ratelimit.yaml:ro ports: - - "8081:8081" - - "6070:6070" + - "18081:8081" + - "16070:6070" + depends_on: + rl-redis: + condition: service_started # ==================== RPC 层 ==================== user-rpc: diff --git a/deploy/dev/envoy.yaml b/deploy/dev/envoy.yaml index db2bfdb..432b5b6 100644 --- a/deploy/dev/envoy.yaml +++ b/deploy/dev/envoy.yaml @@ -14,6 +14,7 @@ static_resources: codec_type: AUTO generate_request_id: true use_remote_address: true + xff_num_trusted_hops: 1 route_config: name: local_route virtual_hosts: @@ -36,6 +37,11 @@ static_resources: route: cluster: user_api_cluster timeout: 30s + rate_limits: + - actions: + - generic_key: + descriptor_value: login + - remote_address: {} typed_per_filter_config: envoy.filters.http.ext_authz: "@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthzPerRoute @@ -46,6 +52,11 @@ static_resources: route: cluster: user_api_cluster timeout: 30s + rate_limits: + - actions: + - generic_key: + descriptor_value: register + - remote_address: {} typed_per_filter_config: envoy.filters.http.ext_authz: "@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthzPerRoute @@ -76,6 +87,11 @@ static_resources: route: cluster: email_api_cluster timeout: 30s + rate_limits: + - actions: + - generic_key: + descriptor_value: forgot_password_send + - remote_address: {} typed_per_filter_config: envoy.filters.http.ext_authz: "@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthzPerRoute @@ -86,6 +102,11 @@ static_resources: route: cluster: email_api_cluster timeout: 30s + rate_limits: + - actions: + - generic_key: + descriptor_value: verify_code_send + - remote_address: {} typed_per_filter_config: envoy.filters.http.ext_authz: "@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthzPerRoute @@ -307,14 +328,37 @@ static_resources: body: inline_string: gateway route not found + access_log: + - name: envoy.access_loggers.stdout + typed_config: + "@type": type.googleapis.com/envoy.extensions.access_loggers.stream.v3.StdoutAccessLog + log_format: + json_format: + start_time: "%START_TIME%" + method: "%REQ(:METHOD)%" + path: "%REQ(X-ENVOY-ORIGINAL-PATH?:PATH)%" + protocol: "%PROTOCOL%" + authority: "%REQ(:AUTHORITY)%" + user_agent: "%REQ(USER-AGENT)%" + request_id: "%REQ(X-REQUEST-ID)%" + response_code: "%RESPONSE_CODE%" + response_flags: "%RESPONSE_FLAGS%" + bytes_received: "%BYTES_RECEIVED%" + bytes_sent: "%BYTES_SENT%" + duration_ms: "%DURATION%" + upstream_cluster: "%UPSTREAM_CLUSTER%" + upstream_host: "%UPSTREAM_HOST%" + upstream_service_time_ms: "%RESP(X-ENVOY-UPSTREAM-SERVICE-TIME)%" + route_name: "%ROUTE_NAME%" + http_filters: - name: envoy.filters.http.lua typed_config: "@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua inline_code: | local TOKEN_HEADER = "xsrf-token" - local TOKEN_COOKIE = "XSRF-TOKEN" - local GUARD_COOKIE = "XSRF-GUARD" + local TOKEN_COOKIE = "__Host-XSRF-TOKEN" + local GUARD_COOKIE = "__Host-XSRF-GUARD" local seeded = false @@ -420,14 +464,14 @@ static_resources: if metadata["need_set_token_cookie"] == true and token_value ~= nil and token_value ~= "" then response_handle:headers():add( "set-cookie", - TOKEN_COOKIE .. "=" .. token_value .. "; Path=/; Max-Age=7200; SameSite=Strict" + TOKEN_COOKIE .. "=" .. token_value .. "; Path=/; Max-Age=7200; SameSite=Strict; Secure" ) end if metadata["need_set_guard_cookie"] == true and guard_value ~= nil and guard_value ~= "" then response_handle:headers():add( "set-cookie", - GUARD_COOKIE .. "=" .. guard_value .. "; Path=/; Max-Age=7200; SameSite=Strict; HttpOnly" + GUARD_COOKIE .. "=" .. guard_value .. "; Path=/; Max-Age=7200; SameSite=Strict; Secure; HttpOnly" ) end end @@ -539,6 +583,20 @@ static_resources: cluster_name: authz_adapter_cluster timeout: 0.5s + - name: envoy.filters.http.ratelimit + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.ratelimit.v3.RateLimit + domain: api + failure_mode_deny: false + rate_limited_as_resource_exhausted: true + enable_x_ratelimit_headers: DRAFT_VERSION_03 + rate_limit_service: + transport_api_version: V3 + grpc_service: + envoy_grpc: + cluster_name: ratelimit_cluster + timeout: 0.2s + - name: envoy.filters.http.router typed_config: "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router @@ -685,6 +743,21 @@ static_resources: address: authz-adapter port_value: 9002 + - name: ratelimit_cluster + connect_timeout: 0.25s + type: STRICT_DNS + lb_policy: ROUND_ROBIN + http2_protocol_options: {} + load_assignment: + cluster_name: ratelimit_cluster + endpoints: + - lb_endpoints: + - endpoint: + address: + socket_address: + address: ratelimit + port_value: 8081 + admin: access_log_path: /tmp/admin.log address: diff --git a/deploy/dev/envoy/envoy.yaml b/deploy/dev/envoy/envoy.yaml deleted file mode 100644 index 48f0c4e..0000000 --- a/deploy/dev/envoy/envoy.yaml +++ /dev/null @@ -1,626 +0,0 @@ -static_resources: - listeners: - - name: ingress_http - address: - socket_address: - address: 0.0.0.0 - port_value: 8080 - 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 - generate_request_id: true - use_remote_address: true - xff_num_trusted_hops: 1 - route_config: - name: local_route - virtual_hosts: - - name: juwan_services - domains: [ "*" ] - routes: - - match: - path: /healthz - direct_response: - status: 200 - body: - inline_string: ok - typed_per_filter_config: - envoy.filters.http.ext_authz: - "@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthzPerRoute - disabled: true - - - match: - path: /api/v1/auth/login - route: - cluster: user_api_cluster - timeout: 30s - rate_limits: - - actions: - - generic_key: - descriptor_value: login - - remote_address: {} - typed_per_filter_config: - envoy.filters.http.ext_authz: - "@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthzPerRoute - disabled: true - - - match: - path: /api/v1/auth/register - route: - cluster: user_api_cluster - timeout: 30s - rate_limits: - - actions: - - generic_key: - descriptor_value: register - - generic_key: - descriptor_key: "period" - descriptor_value: "minute" - - remote_address: {} - typed_per_filter_config: - envoy.filters.http.ext_authz: - "@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthzPerRoute - disabled: true - - - match: - path: /api/v1/auth/forgot-password - route: - cluster: user_api_cluster - timeout: 30s - typed_per_filter_config: - envoy.filters.http.ext_authz: - "@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthzPerRoute - disabled: true - - - match: - path: /api/v1/auth/reset-password - route: - cluster: user_api_cluster - timeout: 30s - typed_per_filter_config: - envoy.filters.http.ext_authz: - "@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthzPerRoute - disabled: true - - - match: - path: /api/v1/auth/forgot-password/send - route: - cluster: email_api_cluster - timeout: 30s - rate_limits: - - actions: - - generic_key: - descriptor_value: forgot_password_send - - generic_key: - descriptor_key: "period" - descriptor_value: "minute" - - remote_address: {} - - actions: - - generic_key: - descriptor_value: forgot_password_send - - generic_key: - descriptor_key: "period" - descriptor_value: "hour" - - remote_address: {} - typed_per_filter_config: - envoy.filters.http.ext_authz: - "@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthzPerRoute - disabled: true - - - match: - prefix: /api/users - route: - cluster: user_api_cluster - timeout: 30s - typed_per_filter_config: - envoy.filters.http.ext_authz: - "@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthzPerRoute - disabled: true - - - match: - prefix: /api/v1/shop - route: - cluster: shop_api_cluster - timeout: 30s - - - match: - prefix: /api/v1/player - route: - cluster: player_api_cluster - timeout: 30s - - - match: - prefix: /api/v1/games - route: - cluster: game_api_cluster - timeout: 30s - - - match: - prefix: /api/v1/games - headers: - - name: :method - exact_match: GET - route: - cluster: game_api_cluster - timeout: 30s - typed_per_filter_config: - envoy.filters.http.ext_authz: - "@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthzPerRoute - disabled: true - - - match: - path: /api/v1/email/verification-code/send - route: - cluster: email_api_cluster - timeout: 30s - rate_limits: - - actions: - - generic_key: - descriptor_value: verify_code_send - - generic_key: - descriptor_key: "period" - descriptor_value: "minute" - - remote_address: {} - - actions: - - generic_key: - descriptor_value: verify_code_send - - generic_key: - descriptor_key: "period" - descriptor_value: "hour" - - remote_address: {} - typed_per_filter_config: - envoy.filters.http.ext_authz: - "@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthzPerRoute - disabled: true - - - match: - prefix: /api/v1/wallet - route: - cluster: wallet_api_cluster - timeout: 30s - - - match: - prefix: /api/v1/players - route: - cluster: player_api_cluster - timeout: 30s - - - match: - prefix: /api/v1/orders - route: - cluster: order_api_cluster - timeout: 30s - - - match: - prefix: /api/v1/email - route: - cluster: email_api_cluster - timeout: 30s - - - match: - prefix: /api/v1/auth - route: - cluster: user_api_cluster - timeout: 30s - - - match: - prefix: /api/v1/upload - route: - cluster: objectstory_api_cluster - timeout: 30s - - - match: - prefix: /api/v1/files - route: - cluster: objectstory_api_cluster - timeout: 30s - typed_per_filter_config: - envoy.filters.http.ext_authz: - "@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthzPerRoute - disabled: true - - - match: - prefix: /api/email - route: - cluster: email_api_cluster - timeout: 30s - - - match: - prefix: /api/v1/game - route: - cluster: game_api_cluster - timeout: 30s - - - match: - prefix: /api/v1 - route: - cluster: user_api_cluster - timeout: 30s - - - match: - prefix: / - direct_response: - status: 404 - body: - inline_string: gateway route not found - access_log: - - name: envoy.access_loggers.stdout - typed_config: - "@type": type.googleapis.com/envoy.extensions.access_loggers.stream.v3.StdoutAccessLog - log_format: - json_format: - start_time: "%START_TIME%" - method: "%REQ(:METHOD)%" - path: "%REQ(X-ENVOY-ORIGINAL-PATH?:PATH)%" - protocol: "%PROTOCOL%" - authority: "%REQ(:AUTHORITY)%" - user_agent: "%REQ(USER-AGENT)%" - request_id: "%REQ(X-REQUEST-ID)%" - response_code: "%RESPONSE_CODE%" - response_flags: "%RESPONSE_FLAGS%" - bytes_received: "%BYTES_RECEIVED%" - bytes_sent: "%BYTES_SENT%" - duration_ms: "%DURATION%" - upstream_cluster: "%UPSTREAM_CLUSTER%" - upstream_host: "%UPSTREAM_HOST%" - upstream_service_time_ms: "%RESP(X-ENVOY-UPSTREAM-SERVICE-TIME)%" - route_name: "%ROUTE_NAME%" - http_filters: - - name: envoy.filters.http.lua - typed_config: - "@type": type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua - inline_code: | - local TOKEN_HEADER = "xsrf-token" - local TOKEN_COOKIE = "__Host-XSRF-TOKEN" - local GUARD_COOKIE = "__Host-XSRF-GUARD" - - local seeded = false - - local function seed_random() - if seeded then - return - end - seeded = true - math.randomseed(os.time()) - end - - local function split_cookie(header) - local out = {} - if not header then - return out - end - for pair in string.gmatch(header, "([^;]+)") do - local key, value = string.match(pair, "^%s*([^=]+)=?(.*)$") - if key ~= nil and value ~= nil then - out[string.lower(key)] = value - end - end - return out - end - - local function is_safe_method(method) - return method == "GET" or method == "HEAD" or method == "OPTIONS" - end - - local function build_token(request_id) - seed_random() - local rnd = tostring(math.random(100000, 999999)) - local rid = request_id or "rid" - return tostring(os.time()) .. "-" .. rid .. "-" .. rnd - end - - function envoy_on_request(request_handle) - local headers = request_handle:headers() - local method = headers:get(":method") - - local cookie_header = headers:get("cookie") - local cookies = split_cookie(cookie_header) - local token_cookie = cookies[string.lower(TOKEN_COOKIE)] - local guard_cookie = cookies[string.lower(GUARD_COOKIE)] - - request_handle:streamInfo():dynamicMetadata():set("csrf", "need_set_token_cookie", token_cookie == nil or token_cookie == "") - request_handle:streamInfo():dynamicMetadata():set("csrf", "need_set_guard_cookie", guard_cookie == nil or guard_cookie == "") - - if token_cookie == nil or token_cookie == "" then - token_cookie = build_token(headers:get("x-request-id")) - request_handle:streamInfo():dynamicMetadata():set("csrf", "token_value", token_cookie) - else - request_handle:streamInfo():dynamicMetadata():set("csrf", "token_value", token_cookie) - end - - if guard_cookie == nil or guard_cookie == "" then - guard_cookie = build_token(headers:get("x-request-id")) - request_handle:streamInfo():dynamicMetadata():set("csrf", "guard_value", guard_cookie) - else - request_handle:streamInfo():dynamicMetadata():set("csrf", "guard_value", guard_cookie) - end - - if is_safe_method(method) then - return - end - - local token_header = headers:get(TOKEN_HEADER) - - if token_header == nil or token_header == "" then - request_handle:respond( - {[":status"] = "403", ["content-type"] = "application/json"}, - '{"code":403,"message":"missing XSRF-TOKEN header"}' - ) - return - end - - if token_cookie == nil or token_cookie == "" or guard_cookie == nil or guard_cookie == "" then - request_handle:respond( - {[":status"] = "403", ["content-type"] = "application/json"}, - '{"code":403,"message":"missing csrf cookies"}' - ) - return - end - - if token_header ~= token_cookie then - request_handle:respond( - {[":status"] = "403", ["content-type"] = "application/json"}, - '{"code":403,"message":"xsrf token mismatch"}' - ) - return - end - end - - function envoy_on_response(response_handle) - local metadata = response_handle:streamInfo():dynamicMetadata():get("csrf") - if metadata == nil then - return - end - - local token_value = metadata["token_value"] - local guard_value = metadata["guard_value"] - - if metadata["need_set_token_cookie"] == true and token_value ~= nil and token_value ~= "" then - response_handle:headers():add( - "set-cookie", - TOKEN_COOKIE .. "=" .. token_value .. "; Path=/; Max-Age=7200; SameSite=Strict; Secure" - ) - end - - if metadata["need_set_guard_cookie"] == true and guard_value ~= nil and guard_value ~= "" then - response_handle:headers():add( - "set-cookie", - GUARD_COOKIE .. "=" .. guard_value .. "; Path=/; Max-Age=7200; SameSite=Strict; Secure; HttpOnly" - ) - end - end - - - name: envoy.filters.http.jwt_authn - typed_config: - "@type": type.googleapis.com/envoy.extensions.filters.http.jwt_authn.v3.JwtAuthentication - providers: - juwan_user_jwt: - issuer: juwan-user-rpc - from_cookies: - - JToken - local_jwks: - inline_string: '{"keys":[{"kty":"oct","k":"TUdVeU1XRTNaRGhqTVRRNVpEZzFNV1ZpT1dVME1HTTNPVEUyTldWa1lUQmxPVEU1WldSa1pEVTFZall6T0dKak9XUmlOek0wTlRjNE5ESXlNamxrWlE","alg":"HS256","use":"sig","kid":"juwan-hs256-1"}]}' - forward: false - claim_to_headers: - - header_name: x-auth-user-id - claim_name: UserId - - header_name: x-auth-is-admin - claim_name: IsAdmin - rules: - - match: - path: /healthz - - match: - prefix: /api/v1 - headers: - - name: :method - exact_match: OPTIONS - - match: - path: /api/v1/auth/login - - match: - path: /api/v1/auth/register - - match: - path: /api/v1/auth/forgot-password - - match: - path: /api/v1/auth/reset-password - - match: - path: /api/v1/auth/forgot-password/send - - match: - path: /api/v1/email/verification-code/send - - match: - prefix: /api/v1 - requires: - provider_name: juwan_user_jwt - - match: - prefix: /api/users - requires: - provider_name: juwan_user_jwt - - match: - prefix: /api/email - requires: - provider_name: juwan_user_jwt - - - name: envoy.filters.http.ext_authz - typed_config: - "@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthz - transport_api_version: V3 - failure_mode_allow: false - with_request_body: - max_request_bytes: 8192 - allow_partial_message: true - grpc_service: - envoy_grpc: - cluster_name: authz_adapter_cluster - timeout: 0.5s - - # RLS 全局过滤器 - - name: envoy.filters.http.ratelimit - typed_config: - "@type": type.googleapis.com/envoy.extensions.filters.http.ratelimit.v3.RateLimit - domain: api - failure_mode_deny: false - rate_limited_as_resource_exhausted: true - enable_x_ratelimit_headers: DRAFT_VERSION_03 - rate_limit_service: - transport_api_version: V3 - grpc_service: - envoy_grpc: - cluster_name: ratelimit_cluster - timeout: 0.2s - - - name: envoy.filters.http.router - typed_config: - "@type": type.googleapis.com/envoy.extensions.filters.http.router.v3.Router - - clusters: - - name: user_api_cluster - connect_timeout: 2s - type: STRICT_DNS - lb_policy: ROUND_ROBIN - load_assignment: - cluster_name: user_api_cluster - endpoints: - - lb_endpoints: - - endpoint: - address: - socket_address: - address: user-api - port_value: 8888 - - - name: email_api_cluster - connect_timeout: 2s - type: STRICT_DNS - lb_policy: ROUND_ROBIN - load_assignment: - cluster_name: email_api_cluster - endpoints: - - lb_endpoints: - - endpoint: - address: - socket_address: - address: email-api - port_value: 8888 - - - name: shop_api_cluster - connect_timeout: 2s - type: STRICT_DNS - lb_policy: ROUND_ROBIN - load_assignment: - cluster_name: shop_api_cluster - endpoints: - - lb_endpoints: - - endpoint: - address: - socket_address: - address: shop-api - port_value: 8888 - - - name: game_api_cluster - connect_timeout: 2s - type: STRICT_DNS - lb_policy: ROUND_ROBIN - load_assignment: - cluster_name: game_api_cluster - endpoints: - - lb_endpoints: - - endpoint: - address: - socket_address: - address: game-api - port_value: 8888 - - - name: objectstory_api_cluster - connect_timeout: 2s - type: STRICT_DNS - lb_policy: ROUND_ROBIN - load_assignment: - cluster_name: objectstory_api_cluster - endpoints: - - lb_endpoints: - - endpoint: - address: - socket_address: - address: objectstory-api - port_value: 8888 - - - name: wallet_api_cluster - connect_timeout: 2s - type: STRICT_DNS - lb_policy: ROUND_ROBIN - load_assignment: - cluster_name: wallet_api_cluster - endpoints: - - lb_endpoints: - - endpoint: - address: - socket_address: - address: wallet-api - port_value: 8888 - - - name: order_api_cluster - connect_timeout: 2s - type: STRICT_DNS - lb_policy: ROUND_ROBIN - load_assignment: - cluster_name: order_api_cluster - endpoints: - - lb_endpoints: - - endpoint: - address: - socket_address: - address: order-api - port_value: 8888 - - - name: player_api_cluster - connect_timeout: 2s - type: STRICT_DNS - lb_policy: ROUND_ROBIN - load_assignment: - cluster_name: player_api_cluster - endpoints: - - lb_endpoints: - - endpoint: - address: - socket_address: - address: player-api - port_value: 8888 - - - name: authz_adapter_cluster - connect_timeout: 0.5s - type: STRICT_DNS - lb_policy: ROUND_ROBIN - http2_protocol_options: { } - load_assignment: - cluster_name: authz_adapter_cluster - endpoints: - - lb_endpoints: - - endpoint: - address: - socket_address: - address: authz-adapter - port_value: 9002 - - # RLS 集群 - - name: ratelimit_cluster - connect_timeout: 0.25s - type: STRICT_DNS - lb_policy: ROUND_ROBIN - http2_protocol_options: {} - load_assignment: - cluster_name: ratelimit_cluster - endpoints: - - lb_endpoints: - - endpoint: - address: - socket_address: - address: ratelimit # RLS 地址 - port_value: 8081 # RLS gRPC 端口 - -admin: - access_log_path: /tmp/admin_access.log - address: - socket_address: - address: 0.0.0.0 - port_value: 9901