每日一Go-81、云原生-Kind + Linkerd + APISIX + Gin:构建一套生产级 JWT 网关鉴权链路)

在微服务体系里,"用户鉴权到底放在哪一层" ,几乎是每个后端团队都会反复讨论的问题:是每个服务各自校验 JWT?还是统一交给网关?服务网格又该扮演什么角色?这篇文章将通过一套完整可复现的实战流程 ,从零开始搭建 Kind + Linkerd + APISIX + Gin 的微服务环境,把 JWT 鉴权彻底前移到网关层,让后端服务只关心业务本身,最终实现一条清晰、稳定、可扩展的认证链路。

一、前提条件

1、创建kind(k8s in docker)

css 复制代码
kind create cluster --config kind-config.yaml --name codee-jun
cs 复制代码
kubectl get nodes

2、安装 Linkerd

bash 复制代码
# 设置 LINKERD2_VERSION 环境变量,指定要安装的 Linkerd 版本
# 如果未设置,则会安装最新的 edge 版本
export LINKERD2_VERSION=edge-25.10.7
# 下载并运行 Linkerd 安装脚本
# 使用 TLSv1.2 协议确保安全下载
curl --proto '=https' --tlsv1.2 -sSfL https://run.linkerd.io/install-edge | sh
# 将 Linkerd CLI 添加到 PATH 环境变量中
export PATH=$HOME/.linkerd2/bin:$PATH
# 检查 Linkerd 版本
# 验证 CLI 是否正确安装
linkerd version
# 安装 Gateway API 标准资源
# 使用 server-side 应用模式安装 v1.4.0 版本
kubectl apply --server-side -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.4.0/standard-install.yaml
# 运行 Linkerd 预检查
# 验证集群是否满足安装 Linkerd 的条件
linkerd check --pre
# 安装 Linkerd CRDs
# 先安装自定义资源定义
linkerd install --crds | kubectl apply -f -
# 安装 Linkerd 控制平面
# 安装核心组件
linkerd install | kubectl apply -f -
# 检查 Linkerd 部署状态
# 验证控制平面组件是否成功部署
kubectl -n linkerd get deploy
# 运行 Linkerd 健康检查
# 验证整个 Linkerd 安装是否健康
linkerd check
nginx 复制代码
# 安装 Linkerd 可视化组件
# 安装 dashboard 等监控工具
linkerd viz install | kubectl apply -f -
# 启动 Linkerd 仪表盘
# 打开 Web 界面查看服务网格状态
linkerd viz dashboard

3、安装APISIX网关

bash 复制代码
# 添加 APISIX Helm 仓库
helm repo add apisix https://charts.apiseven.com
# 更新 Helm 仓库
helm repo update
# 创建 apisix 命名空间
kubectl create ns apisix
# 检查 apisix 命名空间是否已正确创建
kubectl get ns apisix -o yaml | grep linkerd
helm install apisix apisix/apisix --create-namespace --namespace apisix
# 检查 APISIX 部署状态
# 验证 APISIX 组件是否成功部署
kubectl get pods -n apisix
# 安装 APISIX 路由
helm upgrade apisix apisix/apisix \
  --namespace apisix \
  --set ingress-controller.enabled=true \
  --set ingress-controller.apisix.adminService.namespace=apisix

二、用户服务

  1. 安装comer工具

这里偷懒用之前写的快速生成RESTFul接口的工具来生成代码,之后再做修改。

nginx 复制代码
go install github.com/imoowi/comer@latest
css 复制代码
~ >> comer version                                                                 coding-jun@bogon 
Comer version  v1.3.15 
  1. 创建用户服务
bash 复制代码
cd src/day_81
comer new user-service
cd user-service
swag init
go mod tidy
  1. 编译镜像
bash 复制代码
docker build -t imoowi/golang_per_day:day81-user .
docker push imoowi/golang_per_day:day81-user 

三、发布应用到linkerd

  1. app.yaml
makefile 复制代码
# Kubernetes Deployment 配置
# 定义 user-service 应用的部署
apiVersion: apps/v1
kind: Deployment
metadata:
  name: user-service  # 部署名称
  #namespace: apisix   # 命名空间
spec:
  replicas: 1          # 副本数
  selector:
    matchLabels:
      app: user        # 标签选择器,匹配 app=user 的 Pod
  template:
    metadata:
      labels:
        app: user      # Pod 标签
      annotations:
        linkerd.io/inject: enabled  # 启用 Linkerd 服务网格注入
    spec:
      containers:
      - name: app      # 容器名称
        image: imoowi/golang_per_day:day81-user  # 容器镜像
        imagePullPolicy: IfNotPresent  # 镜像拉取策略
        ports:
        - containerPort: 8000  # 容器暴露的端口
---
# Kubernetes Service 配置
# 为 user-service 提供网络访问
apiVersion: v1
kind: Service
metadata:
  name: user-service  # Service 名称
  #namespace: apisix   # 命名空间
spec:
  selector:
    app: user        # 标签选择器,匹配 app=user 的 Pod
  ports:
  - port: 8000        # Service 暴露的端口
    targetPort: 8000  # 目标 Pod 端口
  1. kubectl apply 部署app
powershell 复制代码
kubectl apply -f app.configmap.yaml
kubectl apply -f app.yaml

四、配置 ApisixRoute

  1. 找到apisix的admin-key
cs 复制代码
kubectl get cm apisix -n apisix -o yaml | grep key
  1. 添加route

2.1 curl模式

bash 复制代码
kubectl run curl  --image=curlimages/curl --restart=Never -it --rm -- sh
shell 复制代码
$ curl -s -H "X-API-KEY: edd1c9f034335f136f87ad84b625c8f1" http://apisix-admin:9180/apisix/admin/routes
cpp 复制代码
$ curl -s -X POST -H "X-API-KEY: edd1c9f034335f136f87ad84b625c8f1" -H \
"Content-Type: application/json" \
http://apisix-admin:9180/apisix/admin/routes -d \
'{
    "uri": "/api/user/*",
    "upstream": {
        "type": "roundrobin",
        "nodes": {
            "user-service.default.svc.cluster.local:8000": 1
        }
    }
}'

2.2 ui模式

apache 复制代码
kubectl port-forward svc/apisix-dashboard 9080:80  

浏览器访问 http://127.0.0.1:9080/,添加路由,如图

  1. 修改 nodePort: 30080 ,保存退出,k8s自动重启服务
nginx 复制代码
kubectl edit svc apisix-gateway  
  1. 访问浏览器

http://localhost/api/user/common/captcha

看见上面的输出,说明apisix到linkerd的路径打通了。

五、修改user-service jwt的验证方式

  1. 修改中间件
js 复制代码
JWTAuthMiddleware()
   👇
ApisixMiddleware()
go 复制代码
//user-service/internal/router/auth.router.go
func AuthRouters(e *gin.Engine) {
    api := e.Group(`/api/user`)
    {
        api.POST(`/auth-login`, middlewares.VcodeMiddleware(), controllers.AuthLogin)
        api.GET(`/auth-logout`, middlewares.ApisixMiddleware(), controllers.AuthLogout)
        api.GET(`/auth-info`, middlewares.ApisixMiddleware(), controllers.AuthInfo)
    }
}
  1. 中间件绝对信任上游的uid
go 复制代码
/*
generated by comer,https://github.com/imoowi/comer
Copyright © 2023 jun<simpleyuan@gmail.com>
*/
package middlewares
import (
    "net/http"
    "user-service/internal/services"
    "github.com/gin-gonic/gin"
    "github.com/imoowi/comer/utils/response"
    "github.com/spf13/cast"
)
func ApisixMiddleware() func(c *gin.Context) {
    return func(c *gin.Context) {
        authHeader := c.Request.Header.Get("Authorization")
        if authHeader == "" {
            response.Error(c, "请求头中auth为空", http.StatusUnauthorized)
            return
        }
        //判断用户是否主动注销过
        isLogouted := services.User.IsLogouted(c, authHeader)
        if isLogouted {
            response.Error(c, "token 失效", http.StatusUnauthorized)
            return
        }
        //检测apisix过来的用户id有没有
        userId := c.GetHeader("X-User-Id")
        if userId == "" {
            response.Error(c, "X-User-Id  is empty", http.StatusUnauthorized)
            return
        }
        //绝对信任上游传递的用户id
        c.Set("uid", cast.ToUint(userId))
        c.Next()
    }
}

六、配置Apisix的jwt插件

所有操作都在Postman上执行

  1. 环境变量
  1. 添加consumer
  1. 添加路由

请求body是

swift 复制代码
{
    "id": "api-user-auth",
    "uri": "/api/user*",
    "plugins": {
        "jwt-auth": {
            "key": "user-service-admin",
            "secret": "a-string-secret-at-least-256-bits-long",
            "algorithm": "HS256",
            "ignore": {
                "uris": [
                    "/api/user/login",
                    "/api/user/common/captcha"
                ]
            },
            "payload": [
                "user_id"
            ]
        },
        "serverless-pre-function": {
            "phase": "rewrite",
            "functions": [
                "return function(conf, ctx)\n  local jwt = require('resty.jwt')\n  local auth = ngx.req.get_headers()['Authorization']\n  if not auth then return end\n  local token = string.gsub(auth, 'Bearer ', '')\n  local obj = jwt:load_jwt(token)\n  if not obj.valid then return end\n  if obj.payload.user_id then ngx.req.set_header('X-User-Id', obj.payload.user_id) end\nend"
            ]
        }
    },
    "upstream": {
        "type": "roundrobin",
        "nodes": {
            "user-service:8000": 1
        }
    }
}

七、测试一下

  1. 获取验证码

显示验证码的脚步填写在Post-response里,这样每次请求的验证码就会显示到图片上

xml 复制代码
const res = pm.response.json();
// console.log('res=',res)
pm.visualizer.set(`
    <div>
        <h3>验证码</h3>
        <img src="{{img}}" />
        <p>captcha_id: {{id}}</p>
    </div>
`, {
    img: res.captcha_code,
    id: res.captcha_id
});
  1. 登录

登录之后需要把token设置到环境变量里

javascript 复制代码
const res = pm.response.json();
console.log('res=',res)
pm.environment.set("token", res.token);
  1. 获取用户信息

取数据之前,会把token放在header里

这样整个流程就跑通了。

源码地址*

评论区回复666给


如果您喜欢这篇文章,请您(点赞、分享、亮爱心),万分感谢!