
做微服务网关选型的时候Kong这套方案值得认真琢磨。我在Spring Cloud微服务架构里把Kong网关、JWT鉴权、接口级别限流和docker-compose部署串成了一套完整demo跑了半年多从最开始只有两三个服务到后面每个服务都挂不同的限流策略这个过程踩了不少坑也沉淀了不少可直接复用的脚本和配置。这篇文章会把整个方案摊开讲从为什么选Kong而不是Spring Cloud Gateway到JWT鉴权怎么跟Consumer机制配合再到接口级限流怎么做到比全局限流更精细最后把docker-compose部署和验证过程完整走一遍。这套东西适合谁看如果你正在微服务里做网关选型或者已经在用Spring Cloud但觉得应用层做一些通用鉴权和限流太重复想把这些横切能力下沉到网关层那这篇就是为你准备的。读完你不仅知道Kong怎么配更知道每个配置背后的道理遇到问题也知道去哪里排查。1. 方案设计与选型思路1.1 为什么是Kong而不是Spring Cloud GatewaySpring Cloud生态里的团队第一反应往往是Spring Cloud Gateway。它确实是Java技术栈的天然选择配置路由、集成Sentinel、配合Nacos这些都很顺手。但我在实际中碰到了几个痛点限流规则、接口白名单这类东西本质上是在不断变化和调整的每次改动都要改Java代码、走CI、重新发布服务频繁起来非常痛苦。而Kong把控制面和数据面拆开了数据面是OpenResty和Nginx控制面通过Admin API完成。调整一个限流阈值或者加一个Consumer一条curl命令几十秒就生效不需要动任何业务代码。另一个原因是Kong的插件体系非常成熟。JWT插件、Rate Limiting插件、ACL插件开箱即用而且这些插件都是C/Lua实现的性能和稳定性比Java层实现要好很多。网关本身就是所有流量的唯一入口这里挂了不能含糊。用Nginx内核扛高并发是很多生产系统验证过的路线。还有个容易被忽略的点Kong的配置模型是面向API生命周期的。它有一级对象叫Service下面挂Route再下面可以挂Plugin。这种“服务-路由-插件”的层级结构天然匹配“同一个微服务下不同接口不同策略”的需求。我在Spring Cloud Gateway里想表达同样的逻辑通常要在过滤器链里写一堆条件判断或者配置多个RouteDefinition维护成本明显更高。1.2 JWT鉴权的架构定位微服务里JWT校验有两条路一条是每个服务自己解析token另一条是网关统一校验、解析完把用户信息透传给下游。绝大多数团队最后都会走到第二条路因为这符合网关“横切关注点集中处理”的定位。业务服务只认网关传过来的用户身份自己不碰token的密钥和解析逻辑这样以后换token方案只需要动网关和认证服务业务代码几乎不用改。Kong的JWT插件设计得很贴合这个思路。它的工作原理是客户端把JWT放在Authorization头里发过来Kong先验签、验过期时间然后通过token里的iss字段默认配置去匹配Consumer匹配上了就放行认证通过后还会自动注入X-Consumer-Id、X-Consumer-Username这类请求头给上游服务。也就是说Spring Cloud服务在Controller里拿一下HttpServletRequest的Header就能拿到当前用户是谁不用自己解析token。这里我多说一句很多人以为配了Kong的JWT插件之后下游就能自动拿到JWT里所有自定义claim比如userId、role这些。实际上不是这样。Kong注入的头比较固定自定义claim通常要用Request Transformer插件去提取和拼接。所以方案设计阶段就要想清楚下游到底需要哪些用户属性是直接用Kong注入的X-Consumer-*头还是需要额外转换。1.3 限流为什么放在网关层应用层限流有很多成熟方案比如Spring Cloud Alibaba里的Sentinel功能很强支持熔断降级、热点参数限流。但这些方案都有一个共性每一个接入微服务体系的新服务都要重新引入依赖、重新配置规则。如果一个公司有十几个微服务你得在十几个服务里各自配一遍。而网关层限流是集中式的所有请求都经过Kong那我只需要在Kong里把插件挂到对应的Service或Route上规则全局就生效了。网关层限流的另一个优势是维度更丰富。Kong的Rate Limiting插件支持按Consumer限、“按IP限、按Credential限、按自定义Header限”。这四种维度在实际生产中都是刚需。按Consumer限可以做成每个用户每分钟最多请求某个接口多少次按IP限能防单机爬虫按Header限当你在做多租户时可以把租户ID放Header里每个租户单独的配额。这些策略如果全部在应用层做光是规则引擎就要写不少代码。当然网关层限流不是银弹。Sentinel的价值在于应用内部的细粒度保护比如某个数据库查询很慢你希望并发超过多少就直接拒绝这种和业务紧密相关的限流还是得放在应用层。网关限流更像是第一道大门先把大流量、异常流量挡在外面应用层的Sentinel做第二道防线。两个配合效果最好。1.4 docker-compose在部署环节的角色我一开始在生产环境装Kong是用裸机配systemd服务维护成本确实高。后来发现docker-compose不仅能轻松跑起Kong、PostgreSQL还能把Redis、Konga控制台、demo后端一起编排起来。对于本地开发、测试环境、甚至中小规模的生产部署一条docker compose up -d就把一整套网关环境拉起来了。很多人会质疑docker-compose的生产能力但说实话Kong网关本身是无状态的真正有状态的是它依赖的PostgreSQL和Redis。数据库和Redis用外部托管或者独立容器部署之后Kong容器扩容多少副本都是没问题的。前面挂一个负载均衡器后面一组Kong容器这就是一套非常标准的“逻辑简单、容易运维”的网关部署架构。我在用docker-compose当了很长时间的生产主力稳定性没让我操过心等规模真大了再平滑升级到Kubernetes也不迟。2. 环境准备与docker-compose编排2.1 基础镜像和服务规划动手之前先把需要的镜像想清楚。我这里用了四类容器Kong本身的网关镜像、PostgreSQL作为Kong的控制面数据库、Redis作为限流计数器存储、Konga作为可视化控制台可选。Kong镜像选的是3.x版本这个版本的JWT插件和Rate Limiting插件都在bundled插件列表里默认就能用。如果你用的是很老的0.x版本配置接口差异很大建议直接用3.x起步。PostgreSQL版本我用的13。Kong官方文档对PostgreSQL版本有兼容列表13到现在依然被支持得很稳。Redis用6.x或者7.x都行Rate Limiting插件走Redis模式用的是普通的字符串计数命令Redis 6完全够用。如果是刚接触Kong可以先不接Konga控制台纯用Admin API操作等熟悉了对象模型再上控制台也不迟。2.2镜像版本和端口规划这里把我习惯的docker-compose.yml拆开讲。先看完整结构version: 3.8 networks: kong-net: driver: bridge services: kong-db: image: postgres:13 container_name: kong-db environment: POSTGRES_USER: kong POSTGRES_PASSWORD: kong_pass POSTGRES_DB: kong ports: - 5432:5432 networks: - kong-net healthcheck: test: [CMD, pg_isready, -U, kong] interval: 5s timeout: 5s retries: 5 kong-redis: image: redis:7 container_name: kong-redis ports: - 6379:6379 networks: - kong-net kong: image: kong:3.5.0 container_name: kong depends_on: - kong-db - kong-redis environment: KONG_DATABASE: postgres KONG_PG_HOST: kong-db KONG_PG_USER: kong KONG_PG_PASSWORD: kong_pass KONG_PG_DATABASE: kong KONG_REDIS_HOST: kong-redis KONG_REDIS_PORT: 6379 KONG_PROXY_LISTEN: 0.0.0.0:8000 KONG_ADMIN_LISTEN: 0.0.0.0:8001 KONG_ADMIN_ACCESS_LOG: /dev/stdout KONG_ADMIN_ERROR_LOG: /dev/stderr KONG_PROXY_ACCESS_LOG: /dev/stdout KONG_PROXY_ERROR_LOG: /dev/stderr ports: - 8000:8000 - 8001:8001 - 8443:8443 networks: - kong-net端口规划上有几个细节要留意。8000是Kong代理入口所有下游业务流量都从这里进来8001是Admin API端口所有配置操作都走这里。生产环境里8001一定不要暴露公网Admin API没有任何内置鉴权暴露出去等于把网关的控制权交给全网。我在docker-compose演示里映射出来是为了方便本地调试生产部署要改成只绑定内网地址例如KONG_ADMIN_LISTEN设置为192.168.x.x:8001。PG的密码在demo里写死了生产别这么干。至少用环境变量文件或者Docker Secret管理不然任何一个能读到compose文件的人都知道数据库密码。2.3 初始化顺序与迁移Kong第一次启动之前必须先把数据库表结构建好。Kong把数据库结构变更交给了自己的迁移命令不像大多数应用启动时自动建表。这里给大家看我实际执行的顺序docker compose up -d kong-db kong-redis # 等待数据库真正就绪 docker compose run --rm kong kong migrations bootstrapmigrations bootstrap是初始化命令它建好所有表之后后续启动Kong时就不需要再执行了。如果你把这条命令漏了直接docker compose up -d kongKong容器日志里会持续报数据库连接失败或者找不到表看起来像是网络问题实际上就是结构没初始化。还有一个小坑migrations bootstrap执行完之后有时候还会提示你执行migrations up或者migrations finish。在3.x版本里bootstrap已经完成了绝大部分初始化工作。如果升级Kong版本还会遇到migrations up的需求这个场景后面再说。正常运行环境里只需要记住“数据库起来后先bootstrap再起Kong容器”这个顺序。启动完检查一下curl -s http://localhost:8001/status | jq返回的json里如果database状态是reachable就说明Kong连上了数据库。再curl一下http://localhost:8000如果返回空响应或者404说明代理入口也在正常监听。因为还没有配置任何路由所以404是正常的。3. JWT鉴权配置全流程3.1 Consumer和JWT凭证Kong里有一个核心概念叫Consumer你可以把它理解成“使用API的一方”。这个“一方”可以是一个用户、一个应用、一个服务账号粒度完全由你定义。JWT插件在做鉴权时拿着token里的iss值去匹配Consumer的username匹配成功了才放行。所以Consumer是JWT鉴权的地基。创建Consumer相当简单curl -s -X POST http://localhost:8001/consumers \ -d usernameservice-a创建完Consumer之后绑定JWT凭证curl -s -X POST http://localhost:8001/consumers/service-a/jwt这条命令返回的响应里会有一个key和一个secret。这两个字段非常重要key就是JWT payload里iss字段应该填的值secret则是HS256签名用的密钥。Kong的做法是每个Consumer独立生成一套key和secret你可以理解成每个应用自己拿一把钥匙。返回结果类似{ key: service-a-key, secret: a-strong-secret-value, algorithm: HS256 }这里有个明显的坑Kong要求secret的长度至少为32个字符否则创建时会直接报错。做demo的时候图省事填一个123456然后发现创建不了不要怀疑Kong出bug了先检查secret长度。3.2 服务、路由与JWT插件绑定JWT插件绑定在Service级别还是Route级别取决于业务粒度。如果整个微服务的所有接口都需要鉴权直接绑定到Service省事且一致。如果只有部分接口需要鉴权或者不同接口要用不同的Consumer匹配规则那就要绑定到Route。先创建一个Service指向后端Spring Cloud服务curl -s -X POST http://localhost:8001/services \ -d namedemo-service \ -d urlhttp://demo-backend:8080再创建一条路由把/demo路径下的请求都路由到上面这个Servicecurl -s -X POST http://localhost:8001/services/demo-service/routes \ -d namedemo-route \ -d paths[]/demo \ -d strip_pathfalsestrip_path参数说一下如果设成trueKong转发给后端时会自动去掉/demo这个前缀。比如客户端请求/demo/info转发给后端的就是/info。如果设成false后端收到的还是完整路径/demo/info。Spring Boot服务里如果context-path配置了通常把strip_path设成false或者和后端约定一致即可不然路由会直接404。这个参数是后端联调时最容易踩的坑之一。接下来给这个Service绑定JWT插件curl -s -X POST http://localhost:8001/services/demo-service/plugins \ -d namejwt \ -d config.key_claim_nameiss \ -d config.secret_is_base64false \ -d config.claims_to_verifyexpconfig.key_claim_nameiss表示Kong从JWT payload的iss字段中提取值然后去匹配Consumer的username。这是一个约定如果认证服务签发token时用的不是iss而是别的字段这里就要改成对应字段名。但我强烈建议保持默认因为JWT规范里iss本身就是签发者标识语义最清晰。config.claims_to_verifyexp会让Kong在验签之外同时校验过期时间。这个配置直接决定你的token是否会过期。如果不配JWT的exp形同虚设过期token也能用。我从一开始就一直开启这个校验算是对token基本卫生的要求。配置完成后访问/demo路径下没有带token的请求应该返回401。Kong会告诉你“Unauthorized”整个链路就通了。3.3 认证服务签发Token的配套脚本Kong只负责验证tokentoken的签发需要你的认证服务自己实现。很多Spring Cloud项目都会做一个独立的认证服务用Spring Security加JWT来发token。这里给一个Python脚本作为demo参考用PyJWT库生成一个合法token验证Kong的鉴权是否生效import jwt import time key service-a-key secret a-strong-secret-value payload { iss: key, sub: user-1001, exp: int(time.time()) 3600 } token jwt.encode(payload, secret, algorithmHS256) print(token)这个脚本的关键是payload里的iss必须严格等于创建JWT凭证时返回的key不能随便填。很多人会在这里卡住签发出来的token在Kong那边始终401。我见过不少同事把iss填成自己应用的名字然后怎么配都不通原因就在这。拿到token之后测试TOKEN$(python gen_token.py) curl -s http://localhost:8000/demo/info \ -H Authorization: Bearer ${TOKEN}如果一切正常这个请求就会转发到Spring Boot后端并在响应头里看到Kong注入的X-Consumer-*信息。后端的Controller只需要这样读取用户身份GetMapping(/demo/info) public MapString, String info(HttpServletRequest request) { String username request.getHeader(X-Consumer-Username); return Map.of(user, username null ? anonymous : username); }这就是网关层统一鉴权的价值后端不引入Spring Security的过滤器链不配置JwtAuthenticationConverter只要读一个Header就能识别用户身份代码量直接从几十行降到三五行。3.4 ACL插件做接口级权限管理JWT解决了“你是谁”的问题但还差“你能访问哪些接口”的问题。比如/demo/info所有登录用户都能看/demo/admin只有管理员能访问。Kong里做这个事用的是ACL插件它管的是“访问控制列表”。ACL插件要和认证插件配合使用。Kong处理请求的顺序是先执行认证插件比如JWT确认Consumer身份然后再执行ACL插件看这个Consumer在不在允许名单里。我需要先创建一个ACL group然后把Consumer加进group# 创建ACL group curl -s -X POST http://localhost:8001/consumers/service-a/acls \ -d groupadmin-group # 绑定ACL插件到路由 curl -s -X POST http://localhost:8001/routes/demo-route/plugins \ -d nameacl \ -d config.allow[]admin-group \ -d config.hide_groups_headertrueconfig.hide_groups_headertrue的含义是不要把X-Consumer-Groups头透传给上游避免后端拿到用户组列表后自己做不够可靠的判断。放行的逻辑全部收敛在网关层后端只要知道“这个请求能到我这来说明它已经被允许了”。3.5 JWT安全实践补充demo里用的是HS256对称签名认证服务和Kong共享同一个secret简单直接。但生产环境如果有多个服务需要验证tokenHS256意味着密钥要在每个验证方之间传播泄露面很大。所以生产我建议直接用RS256。Kong的JWT插件也支持RS256配置方式是在创建Consumer的JWT凭证时指定algorithm为RS256并且把公钥通过rsa_public_key字段填进去。私钥只保留在认证服务手里Kong只拿着公钥做验签安全边界清晰得多。另外一个生产必做的实践是防止时钟漂移导致token提前过期或延迟过期。JWT的exp、nbf字段都和系统时间有关。Kong容器如果和认证服务不在同一个时钟源下可能出现一边认为token没过期另一边认为已经过期了。docker-compose部署时尽量用同一个宿主机时间容器内部的时间要和宿主机同步通常默认就是同步的但如果你改了容器时区或者宿主机时间不对就会遇到诡异的401问题。4. 接口级别限流配置与验证4.1 限流维度怎么选Kong的Rate Limiting插件是我用得最多的插件。它的配置模型里有三个核心参数值得好好理解limit_by、policy和限流窗口。先看limit_by支持哪些维度维度含义适用场景consumer按Consumer计数登录用户维度每个用户独立配额ip按客户端IP计数防爬虫、防匿名用户刷接口credential按认证凭证计数每个应用凭证独立配额header按指定Header计数多租户场景按租户ID计数我的经验是业务接口限流优先用consumer维度因为同一个IP背后可能有多个用户按IP限会把正常用户误伤。但开放接口、未登录场景只能用IP维度兜底。两者在Kong里用一个插件实例只能配置一个limit_by不能同时按IP和consumer双重限制。如果需求是说“每个IP每分钟最多10次同时每个登录用户每分钟最多100次”那就需要给同一个Route挂两个Rate Limiting插件实例一个配limit_byip另一个配limit_byconsumer。这个做法官方是支持的因为每个插件实例的计数器是独立的。4.2 时间窗口参数如何配置Rate Limiting插件的时间窗口支持second、minute、hour、day、month、year六个级别。比如我配置minute30表示每分钟最多30次。Kong的实现是固定时间窗口计数器窗口边界按自然时间切分不是滑动窗口。curl -s -X POST http://localhost:8001/routes/demo-route/plugins \ -d namerate-limiting \ -d config.minute20 \ -d config.limit_byconsumer \ -d config.policyredis \ -d config.redis_hostkong-redis \ -d config.redis_port6379这里policyredis很关键。如果不配这个参数默认policy是local数据只存在单个Kong节点进程内。如果你部署了两个或更多Kong副本local模式下两个节点各自计数限流总量会是设定值的倍数完全达不到限流目的。配了redis之后所有节点共享同一个计数器数据一致性强很多。生产环境不止一个Kong节点务必用redis模式。顺便说一下Redis计数本身也不是强一致的。Kong对Redis的计数操作可能在极端场景下出现几秒的误差。如果业务对限流准确性要求极高需要接受这个事实绝大多数场景下分钟级窗口里误差几秒完全不影响业务判断。4.3 把限流精确到单个接口“接口级别限流”的字面意思很好理解但Kong的插件是绑在Service或Route上的要达到单个接口的维度关键要做好Route的拆分。我的做法是一个Spring Cloud服务创建多个Route每个Route对应一个具体接口路径然后在每个Route上挂独立的Rate Limiting插件。比如有一个服务叫demo-service下面有两个接口/demo/login允许每分钟20次/demo/info允许每分钟100次。那就建两条Route# 登录接口 curl -s -X POST http://localhost:8001/services/demo-service/routes \ -d namelogin-route \ -d paths[]/demo/login \ -d strip_pathfalse # 信息接口 curl -s -X POST http://localhost:8001/services/demo-service/routes \ -d nameinfo-route \ -d paths[]/demo/info \ -d strip_pathfalse然后分别挂插件互不干扰curl -s -X POST http://localhost:8001/routes/login-route/plugins \ -d namerate-limiting \ -d config.minute20 \ -d config.limit_byconsumer \ -d config.policyredis curl -s -X POST http://localhost:8001/routes/info-route/plugins \ -d namerate-limiting \ -d config.minute100 \ -d config.limit_byconsumer \ -d config.policyredis这里有个常见误解有人觉得只要路径前缀是/demo请求会自动匹配到demo-service上挂的限流插件。其实路由匹配规则是Kong选中最具体的Route。如果只创建了demo-service和一条/demo路由所有/demo前缀请求都会走同一个Route自然没法区分/login和/info。所以接口级限流的本质是路由级限流先把路由拆开再谈限流参数差异。如果同一个接口还希望按HTTP方法GET、POST分别限流还可以在创建Route时指定methods参数curl -s -X POST http://localhost:8001/services/demo-service/routes \ -d namelogin-post-route \ -d paths[]/demo/login \ -d methods[]POST \ -d strip_pathfalse这样一个路径可以生成多条Route分别挂不同策略。Kong在匹配时会综合Path和Method选中最精确的一条。4.4 限流响应处理和客户端容错Kong的Rate Limiting插件在触发限流后返回503不是很多人以为的429。这个行为一开始让不少人困惑因为Nginx的504是网关超时、503是服务不可用而429才是“请求太多”。Kong硬编码了503想改成429需要额外做处理比较绕。我在生产里没有改这个行为因为这个状态码已经能明确告诉客户端“别重试了你被限流了”。限流触发时Kong还会在响应头里告诉客户端配额情况响应头含义RateLimit-Limit窗口内配额总量RateLimit-Remaining窗口剩余配额RateLimit-Reset配额重置时间Unix时间戳客户端和前端可以根据这些响应头做友好的提示比如“操作过于频繁请稍后再试”。Spring Cloud的Feign客户端如果不做特殊配置默认会把503当作可重试的错误但限流场景下重试只会加剧问题。所以我建议在Feign配置里对503单独处理不重试直接抛出异常或者走降级逻辑。4.5 换一个更稳妥的限流策略按IP和Consumer双重限制我经常收到的一个问题是“你们到底怎么防刷”。我的标准答案是按IP做一个较宽松的总量限制按Consumer做一个更严格的业务限制。比如一个非登录接口可以先设IP维度每个来源每分钟60次再从业务角度对每个租户配置不同的配额后者就依赖Header或者Consumer维度。实现双重限制是在同一个Route上挂两个Rate Limiting插件实例。第一次跑通时我还专门验证了一下确认两个实例互不干扰一个实例触发限流时另一个实例依然正常工作。这个特性让Kong在复杂限流场景下表达力很强。如果哪天需求变成“同一IP同一接口每分钟最多3次登录尝试”这种组合策略就能直接接上。5. 一键跑通完整Demo5.1 Demo目录结构与后端示例前面讲的都是点状配置这里把它收拢成一个完整的Demo工程。整个目录大概长这样demo-kong-gateway/ ├── docker-compose.yml ├── init/ │ ├── init-kong.sh │ └── gen_token.py ├── backend/ │ ├── Dockerfile │ ├── pom.xml │ └── src/main/java/com/demo/ApiApplication.java └── README.md后端是一个极简的Spring Boot应用提供两个接口目的就是验证Kong的鉴权、限流和用户透传效果。核心Controller如下RestController public class ApiController { GetMapping(/demo/info) public MapString, String info(HttpServletRequest request) { String username request.getHeader(X-Consumer-Username); return Map.of( path, /demo/info, username, username null ? anonymous : username ); } GetMapping(/demo/login) public MapString, String login(HttpServletRequest request) { return Map.of( path, /demo/login, message, login endpoint ); } }后端镜像也用最简单的方式打Dockerfile里基于openjdk:17把jar包拷进去启动即可。5.2 初始化脚本把配置固化成可重复执行的文件用Admin API手工curl配置对象一次两次还没问题配置一多就想骂人。我习惯把所有初始化配置写成一个Shell脚本放到版本管理里。这样同事拉下来demo代码跑一个脚本就能复现整套网关配置不用从零手动敲命令。#!/usr/bin/env bash set -euo pipefail KONG_ADMIN${KONG_ADMIN:-http://localhost:8001} DEMO_SERVICE_URL${DEMO_SERVICE_URL:-http://demo-backend:8080} echo 创建 Service curl -s -X POST ${KONG_ADMIN}/services \ -d namedemo-service \ -d url${DEMO_SERVICE_URL} || true echo 创建两条 Route curl -s -X POST ${KONG_ADMIN}/services/demo-service/routes \ -d namelogin-route \ -d paths[]/demo/login \ -d strip_pathfalse || true curl -s -X POST ${KONG_ADMIN}/services/demo-service/routes \ -d nameinfo-route \ -d paths[]/demo/info \ -d strip_pathfalse || true echo 创建 Consumer 和 JWT 凭证 curl -s -X POST ${KONG_ADMIN}/consumers \ -d usernameservice-a || true curl -s -X POST ${KONG_ADMIN}/consumers/service-a/jwt \ -d algorithmHS256 \ -d keyservice-a-key \ -d secreta-strong-secret-value || true echo 绑定 JWT 插件到 Service curl -s -X POST ${KONG_ADMIN}/services/demo-service/plugins \ -d namejwt \ -d config.key_claim_nameiss \ -d config.secret_is_base64false \ -d config.claims_to_verifyexp || true echo 为两个 Route 分别挂限流插件 curl -s -X POST ${KONG_ADMIN}/routes/login-route/plugins \ -d namerate-limiting \ -d config.minute20 \ -d config.limit_byconsumer \ -d config.policyredis \ -d config.redis_hostkong-redis \ -d config.redis_port6379 || true curl -s -X POST ${KONG_ADMIN}/routes/info-route/plugins \ -d namerate-limiting \ -d config.minute100 \ -d config.limit_byconsumer \ -d config.policyredis \ -d config.redis_hostkong-redis \ -d config.redis_port6379 || true echo 初始化完成注意脚本里每个创建操作都加了|| true这是因为Kong的Admin API在创建重复对象时会返回409而幂等脚本的通用做法是先清理再创建或者允许重名失败。为了保持“可重复运行”我会在实际工程里先跑一个清理函数delete_if_exist() { local name$1 local uri$2 local id id$(curl -s ${KONG_ADMIN}${uri} | jq -r .data[] | select(.name \${name}\) | .id | head -1) if [ -n $id ] [ $id ! null ]; then curl -s -X DELETE ${KONG_ADMIN}${uri}/${id} fi }凡是可能重复执行的对象都用这个函数先删一遍这样不管是第一次跑还是第二十次跑结果都一样。生产环境里Kong的配置一定会漂移脚本化、幂等化是防止漂移的基本功。5.3 完整请求链路验收环境起来之后按顺序执行# 1. 启动数据库和Redis docker compose up -d kong-db kong-redis # 2. 初始化Kong数据库表结构 docker compose run --rm kong kong migrations bootstrap # 3. 启动Kong和后端服务 docker compose up -d kong demo-backend # 4. 执行网关初始化脚本 bash init/init-kong.sh # 5. 生成JWT token TOKEN$(python init/gen_token.py) # 6. 正常请求 curl -s http://localhost:8000/demo/info \ -H Authorization: Bearer ${TOKEN} # 7. 不带token请求应该返回401 curl -s http://localhost:8000/demo/info # 8. 连续请求/login接口测试限流第21次开始应该返回503 for i in $(seq 1 25); do code$(curl -s -o /dev/null -w %{http_code} http://localhost:8000/demo/login \ -H Authorization: Bearer ${TOKEN}) echo request $i - $code done第三步里docker compose up -d demo-backend需要注意demo-backend要等jar包构建完才能起来。我实际编译Spring Boot项目时会先在宿主机执行mvn package再docker compose build backend。如果你的机器没装Java也可以临时用一个简单的HTTP服务代替反正后端返回什么不重要重要的是Kong的鉴权和限流行为已经能完整验证。5.4 生产部署中的几个额外部署参数如果你准备把这套方案用到生产环境除了demo里的配置还有几个环境变量必须考虑。KONG_PROXY_LISTEN和KONG_ADMIN_LISTEN在生产环境里要绑定正确的网络接口KONG_LOG_LEVEL建议设置成warn开发环境可以info不然Kong的access log会非常刷屏。另外建议设置KONG_NGINX_WORKER_PROCESSES为auto让Nginx worker进程数根据CPU核数自动调整。后台Redis的高可用也是生产必聊的话题。docker-compose演示用了单节点Redis生产至少要做成主从或者Redis Sentinel模式否则Redis挂了整个限流功能会直接失效。Kong这边对应需要配置的有KONG_REDIS_HOST、KONG_REDIS_PORT、KONG_REDIS_SSL相关参数具体看你的Redis部署形态。6. 常见问题排查与避坑大全6.1 接口一直401这是配置JWT之后最常遇到的问题。排查顺序先看这几点第一确认JWT插件绑定的对象就是当前请求匹配的Route或Service第二确认token的iss等于创建Consumer JWT凭证时的key而不是Consumer的username第三确认secret一致HS256下任何一方改了密钥另一方没改验签必然失败第四确认claims_to_verify里如果配了exptoken没过期。还有一个隐蔽点Admin API返回的key和secret在json里如果创建Consumer和创建JWT凭证时用了不同的分隔符导致漏看或复制错就会一直401。用jq解析响应最稳妥curl -s -X POST http://localhost:8001/consumers/service-a/jwt | jq .key, .secret6.2 限流一直没有生效限流不生效通常有三个原因。一是插件挂在了错误的Route上同一个Service有多条Route请求实际匹配的是另一条二是policylocal且Kong副本不止一个各节点独立计数总量超了也触发不了三是Redis连接配置有问题插件没有正常写Redis但错误被静默吞掉了。验证Redis是否真的写入了计数可以在限流窗口内执行redis-cli keys看一下有没有kong_rate_limiting开头的key。如果key存在说明Redis链路是通的问题在匹配或阈值配置如果key不存在插件根本没有走Redis模式需要检查插件配置。6.3 限流触发返回503而不是429这是设计行为不是Bug。我通常不会跟它较劲但如果你一定要改成429可以考虑在Rate Limiting插件前面叠加一个Serverless插件在限流触发时直接返回自定义响应。过程比较绕而且需要写Lua代码一般情况下没这个必要。我更建议直接在前端或者客户端判断Response Headers里的Ratelimit-Remaining和Ratelimit-Reset给出友好的提示。比如Ratelimit-Remaining为0的时候禁用按钮并提示“操作太频繁xx秒后再试”。6.4 JWT续签和密钥轮换问题Kong只管验证不管签发和续签。JWT过期之后客户端必须重新走认证服务获取新的token。最常见的续签方案是引入refresh token机制Kong这一侧不需要任何变化认证服务签发access token时同时发一个refresh tokenaccess token快过期时客户端拿refresh token换新的access token。要注意refresh token一般只保存在认证服务Kong不参与。密钥轮换时HS256下因为双方对称轮换后旧token立即失效RS256下可以更平滑Kong可以配置多个公钥给签发方私钥更灵活的切换周期。6.5 Docker Compose启动顺序和端口冲突docker compose里如果Kong起得很早PostgreSQL还没就绪Kong会反复重试连接数据库。compose的depends_on不能真正等数据库Ready所以migrations bootstrap之前要手动确认数据库状态。我习惯在脚本里加一个等待循环until docker exec kong-db pg_isready -U kong; do echo waiting for postgres... sleep 2 done端口冲突是另一个高频问题本机已经占了8000、8001、5432端口compose启动直接报Address already in use。我建议在demo里把宿主机端口映射改成不那么容易冲突的比如18000:8000、18001:8001、15432:5432用完即走不会污染本机常用端口。6.6 配置改了但没生效用Admin API修改插件配置后Kong是异步应用还是立即生效取决于版本。3.x版本里插件配置变更基本上是秒级生效但如果发现没生效先看有没有绑定到正确的Route。还有一个我踩过的坑同一个Route上挂了两个同名的插件实例修改时改的是第一个实例而实际生效的是第二个。排查的时候用GET把插件列表拉出来看一下curl -s http://localhost:8001/routes/login-route/plugins | jq .data[] | {id, name, config}确认每个插件实例的id、name和配置再决定删除哪个、修改哪个。我个人在实际操作中的体会是Kong这套方案最适合的落地点是“团队已经有多套微服务但每套都在重复实现鉴权和限流”的阶段。把这两个横切能力下沉到网关之后业务代码清爽了规则调整也变快了最重要的是各个服务之间的安全策略终于有了一致性。但网关配置一定要纳入版本管理别在生产环境直接拿Admin API改完就走否则早晚会出一次配置漂移的故障。最后给一个建议第一次跑通demo之后用一个隔离环境把所有接口路径、限流阈值、Consumer映射全部梳理成文档再固化成初始化脚本这比什么都重要。