下一代云原生网关Higress:基于Wasm开发JWT认证插件
发布时间 2023-08-11
编者荐语:
Higress 原生提供了 C++ 版本的 JWT 插件,二次开发适配自己业务需求成本比较高,基于该博主的 Go 版本插件二次开发会容易很多。
一、什么是Higress
Higress是基于阿里内部的Envoy Gateway实践沉淀、以开源Istio + Envoy为核心构建的下一代云原生网关,实现了流量网关 + 微服务网关 + 安全网关三合一的高集成能力,深度集成Dubbo、Nacos、Sentinel等微服务技术栈,能够帮助用户极大的降低网关的部署及运维成本且能力不打折;在标准上全面支持Ingress与Gateway API,积极拥抱云原生下的标准API规范;同时,Higress Controller也支持Nginx Ingress平滑迁移,帮助用户零成本快速迁移到Higress。
二、什么是WASM
WASM代表”WebAssembly”,它是一种可移植、低级别的二进制指令格式,旨在作为Web浏览器中的一种新型执行环境而推出。它的设计目标是为Web上的高性能应用提供一种通用的编译目标,使开发人员能够在不同平台和架构上运行高效的代码。 WASM的主要特点包括:
- 性能: WASM是一种二进制格式,可以高效地编码和解码,从而在浏览器中实现比传统JavaScript更快的执行速度。这使得WASM特别适用于需要高性能的Web应用,如游戏、图像处理和模拟器。
- 安全性: 由于WASM是低级别的虚拟机,它提供了一种隔离和受控的执行环境。这意味着它可以在浏览器中安全地运行,防止恶意代码对用户计算机的损害。
- 跨平台: WASM可以在不同的体系结构和操作系统上运行,使开发人员能够编写一次代码,然后在多个平台上运行,而无需进行大量的修改和调整。
- 语言无关性: 尽管WASM可以从多种编程语言中生成,但它与特定的编程语言无关。这使得开发人员能够选择他们熟悉的语言,并将其编译成WASM以在Web上运行。
- 适用范围: 虽然WASM主要针对Web浏览器中的Web应用程序,但它不仅限于此。它还可以在其他领域,如服务器端、嵌入式系统等中发挥作用。
三、最佳实践
言归正传,现在我们基于Wasm为Higress开发一个JWT认证插件,实现在Higress中进行token解析认证,如果Token无效或不存在直接拒绝返回401,Token有效继续访问后端微服务。利用最佳实践的方式带大家熟悉一下基于Wsam开发Higress插件的整个过程,流程图如下:这样做的好处:
- 下游微服务无需重复进行认证,减少了重复的工作,也提高了系统的安全性
- 避免了下游服务重复解析token来获取用户信息的需求。减少了不必要的开销
四、Higress 部署
# 环境为Kubernetes v1.27.3$ kubectl get nodesNAME STATUS ROLES AGE VERSIONkind-control-plane Ready control-plane 9h v1.27.3
# 通过Helm安装$ helm repo add higress.io https://higress.io/helm-charts
"higress.io" already exists with the same configuration, skipping$ helm install higress -n higress-system higress.io/higress --create-namespace --render-subchart-notes --set global.local=true --set higress-console.o11y.enabled=false --set higress-console.domain=console.higress.io --set higress-console.admin.password.value=admin
NAME: higressLAST DEPLOYED: Thu Aug 10 20:37:40 2023NAMESPACE: higress-systemSTATUS: deployedREVISION: 1TEST SUITE: NoneNOTES:Higress successfully installed!
To learn more about the release, try: $ helm status higress -n higress-system $ helm get all higress -n higress-system
1. Use the following URL to access the console: http://console.higress.io/ Since Higress Console is running in local mode, you may need to add the following line into your hosts file before accessing the console: 127.0.0.1 console.higress.io2. Use following commands to get the credential and login: export ADMIN_USERNAME=$(kubectl get secret --namespace higress-system higress-console -o jsonpath="{.data.adminUsername}" | base64 -d) export ADMIN_PASSWORD=$(kubectl get secret --namespace higress-system higress-console -o jsonpath="{.data.adminPassword}" | base64 -d) echo -e "Username: ${ADMIN_USERNAME}\nPassword: ${ADMIN_PASSWORD}" NOTE: If this is an upgrade release, your current password won't be changed.3. If you'd like to change the credential, you can edit this secret with new values: higress-system/higress-console
# 查看密码$ export ADMIN_USERNAME=$(kubectl get secret --namespace higress-system higress-console -o jsonpath="{.data.adminUsername}" | base64 -d)$ export ADMIN_PASSWORD=$(kubectl get secret --namespace higress-system higress-console -o jsonpath="{.data.adminPassword}" | base64 -d)$ echo -e "Username: ${ADMIN_USERNAME}\nPassword: ${ADMIN_PASSWORD}"Username: adminPassword: admin
# 配置Hosts $ cat /etc/hosts 127.0.0.1 demo.kubesre.com console.higress.io
# 转发一下端口本地可以访问$ kubectl port-forward service/higress-gateway -n higress-system 80:80
访问地址:http://console.higress.io/pluginUsername: admin Password: admin出现如上界面说明安装成功!
五、开发环境搭建
以MacOS为例,Windows可以去查阅官方文档进行安装环境: Golang:https://go.dev/doc/installTinyGo:https://tinygo.org/getting-started/install/Wasm-opt:https://github.com/WebAssembly/binaryen.git
# 安装golang$ brew install go$ go versiongo version go1.19.9 darwin/arm64
# TinyGo$ wget https://github.com/tinygo-org/tinygo/releases/download/v0.25.0/tinygo0.25.0.darwin-amd64.tar.gz$ tar -zxf tinygo0.25.0.darwin-amd64.tar.gz$ export PATH=/tmp/tinygo/bin:$PATH$ tinygo versiontinygo version 0.28.0-dev-5c2753e darwin/amd64 (using go version go1.19.9 and LLVM version 15.0.0)
# Wasm-opt$ wget https://github.com/WebAssembly/binaryen/releases/download/version_114/binaryen-version_114-arm64-macos.tar.gz$ tar binaryen-version_114-arm64-macos.tar.gz$ export PATH=/tmp/binaryen/bin:$PATH$ wasm-opt --versionwasm-opt version 114 (version_114)
到此为止,开发环境搭建完毕:
六、JWT认证插件开发
初始化工程:
# 初始化工程$ mkdir jwt-plugin$ cd jwt-plugin$ go mod init jwt-plugingo: creating new go.mod: module jwt-plugin# 设置代理$ go env -w GOPROXY=https://proxy.golang.com.cn,direct# 下载依赖性$ go get github.com/tetratelabs/proxy-wasm-go-sdk$ go get github.com/alibaba/higress/plugins/wasm-go@main$ go get github.com/tidwall/gjson
插件代码:
package main
import ( "encoding/json" "github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper" jwt "github.com/dgrijalva/jwt-go" "github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm" "github.com/tetratelabs/proxy-wasm-go-sdk/proxywasm/types" "github.com/tidwall/gjson")
// 自定义插件配置
func main() { wrapper.SetCtx( "jwt-plugin", // 配置插件名称 wrapper.ParseConfigBy(parseConfig), wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders), )}
type Config struct { TokenSecretKey string // 解析Token SecretKey TokenHeaders string // 定义获取Token请求头名称}
type Res struct { Code int `json:"code"` // 返回状态码 Msg string `json:"msg"` // 返回信息}
func parseConfig(json gjson.Result, config *Config, log wrapper.Log) error { // 解析出配置,更新到config中 config.TokenSecretKey = json.Get("token_secret_key").String() config.TokenHeaders = json.Get("token_headers").String() return nil}
func onHttpRequestHeaders(ctx wrapper.HttpContext, config Config, log wrapper.Log) types.Action { var res Res if config.TokenHeaders == "" || config.TokenSecretKey == "" { res.Code = 401 res.Msg = "参数不足" data, _ := json.Marshal(res) _ = proxywasm.SendHttpResponse(401, nil, data, -1) return types.ActionContinue }
token, err := proxywasm.GetHttpRequestHeader(config.TokenHeaders) // 获取Token if err != nil { res.Code = 401 res.Msg = "认证失败" data, _ := json.Marshal(res) _ = proxywasm.SendHttpResponse(401, nil, data, -1) return types.ActionContinue } valid := ParseTokenValid(token, config.TokenSecretKey) if valid { _ = proxywasm.ResumeHttpRequest() return types.ActionPause } else { res.Code = 401 res.Msg = "认证失败" data, _ := json.Marshal(res) _ = proxywasm.SendHttpResponse(401, nil, data, -1) return types.ActionContinue }}// 认证解析Tokenfunc ParseTokenValid(tokenString, TokenSecretKey string) bool { token, _ := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { // 在这里提供用于验证签名的密钥 return []byte(TokenSecretKey), nil }) return token.Valid}
HTTP 处理挂载点:
代码中通过 wrapper.ProcessRequestHeadersBy将自定义函数 onHttpRequestHeaders用于HTTP 请求头处理阶段处理请求。除此之外,还可以通过下面方式,设置其他阶段的自定义处理函数
HTTP 处理阶段 | 触发时机 | 挂载方法 |
---|---|---|
HTTP 请求头处理阶段 | 网关接收到客户端发送来的请求头数据时 | wrapper.ProcessRequestHeadersBy |
HTTP 请求 Body 处理阶段 | 网关接收到客户端发送来的请求 Body 数据时 | wrapper.ProcessRequestBodyBy |
HTTP 应答头处理阶段 | 网关接收到后端服务响应的应答头数据时 | wrapper.ProcessResponseHeadersBy |
HTTP 应答 Body 处理阶段 | 网关接收到后端服务响应的应答 Body 数据时 | wrapper.ProcessResponseBodyBy |
工具方法:
代码中的 proxywasm.SendHttpResponse是插件 SDK 提供的两个工具方法
分类 | 方法名称 | 用途 | 可以生效的HTTP 处理阶段 |
---|---|---|---|
请求头处理 | GetHttpRequestHeaders | 获取客户端请求的全部请求头 | HTTP 请求头处理阶段 |
| ReplaceHttpRequestHeaders | 替换客户端请求的全部请求头 | HTTP 请求头处理阶段 | |
| GetHttpRequestHeader | 获取客户端请求的指定请求头 | HTTP 请求头处理阶段 | |
| RemoveHttpRequestHeader | 移除客户端请求的指定请求头 | HTTP 请求头处理阶段 | |
| ReplaceHttpRequestHeader | 替换客户端请求的指定请求头 | HTTP 请求头处理阶段 | |
| AddHttpRequestHeader | 新增一个客户端请求头 | HTTP 请求头处理阶段 | | 请求 Body 处理 | GetHttpRequestBody | 获取客户端请求 Body | HTTP 请求 Body 处理阶段 | |
| AppendHttpRequestBody | 将指定的字节串附加到客户端请求 Body 末尾 | HTTP 请求 Body 处理阶段 | |
| PrependHttpRequestBody | 将指定的字节串附加到客户端请求 Body 的开头 | HTTP 请求 Body 处理阶段 | |
| ReplaceHttpRequestBody | 替换客户端请求 Body | HTTP 请求 Body 处理阶段 | | 应答头处理 | GetHttpResponseHeaders | 获取后端响应的全部应答头 | HTTP 应答头处理阶段 | |
| ReplaceHttpResponseHeaders | 替换后端响应的全部应答头 | HTTP 应答头处理阶段 | |
| GetHttpResponseHeader | 获取后端响应的指定应答头 | HTTP 应答头处理阶段 | |
| RemoveHttpResponseHeader | 移除后端响应的指定应答头 | HTTP 应答头处理阶段 | |
| ReplaceHttpResponseHeader | 替换后端响应的指定应答头 | HTTP 应答头处理阶段 | |
| AddHttpResponseHeader | 新增一个后端响应头 | HTTP 应答头处理阶段 | | 应答 Body 处理 | GetHttpResponseBody | 获取客户端请求 Body | HTTP 应答 Body 处理阶段 | |
| AppendHttpResponseBody | 将指定的字节串附加到后端响应 Body 末尾 | HTTP 应答 Body 处理阶段 | |
| PrependHttpResponseBody | 将指定的字节串附加到后端响应 Body 的开头 | HTTP 应答 Body 处理阶段 | |
| ReplaceHttpResponseBody | 替换后端响应 Body | HTTP 应答 Body 处理阶段 | | HTTP 调用 | DispatchHttpCall | 发送一个 HTTP 请求 | - | |
| GetHttpCallResponseHeaders | 获取 DispatchHttpCall 请求响应的应答头 | - | |
| GetHttpCallResponseBody | 获取 DispatchHttpCall 请求响应的应答 Body | - | |
| GetHttpCallResponseTrailers | 获取 DispatchHttpCall 请求响应的应答 Trailer | - | | 直接响应 | SendHttpResponse | 直接返回一个特定的 HTTP 应答 | - | | 流程恢复 | ResumeHttpRequest | 恢复先前被暂停的请求处理流程 | - | |
| ResumeHttpResponse | 恢复先前被暂停的应答处理流程 | - |
七、编译打包插件
# 编译WASM$ tinygo build -o main.wasm -scheduler=none -target=wasi -gc=custom -tags='custommalloc nottinygc_finalizer' main.go
# 查看Dockerfilecat DockerfileFROM scratchCOPY main.wasm plugin.wasm
# 编译Docker Image$ docker build -t registry.cn-shanghai.aliyuncs.com/kubesre01/jwt-plugin:v1 .
[+] Building 0.1s (5/5) FINISHED docker:desktop-linux => [internal] load build definition from Dockerfile 0.0s => => transferring dockerfile: 113B 0.0s => [internal] load .dockerignore 0.0s => => transferring context: 2B 0.0s => [internal] load build context 0.0s => => transferring context: 1.99MB 0.0s => [1/1] COPY main.wasm plugin.wasm 0.0s => exporting to image 0.0s => => exporting layers 0.0s => => writing image sha256:6097b8347fc71e52eadd05753b2bd6877f4f9283a9cd4d844e62259ef714e373 0.0s => => naming to registry.cn-shanghai.aliyuncs.com/kubesre01/jwt-plugin:v1
# 上传镜像到仓库 $ docker push registry.cn-shanghai.aliyuncs.com/kubesre01/jwt-plugin:v1The push refers to repository [registry.cn-shanghai.aliyuncs.com/kubesre01/jwt-plugin]31f0fa5f1adf: Pushedv1: digest: sha256:c3c629576c71d3dbd4d2bd8118c5523346c63cff7853ce995eb5879affd2c752 size: 526
八、部署示例服务
- Auth服务:负责用户登陆授权返回Token
- Demo服务:负责认证通过后访问的微服务
# 部署Auth服务$ cat auth.ymlapiVersion: apps/v1kind: Deploymentmetadata: name: auth labels: app: authspec: replicas: 1 selector: matchLabels: app: auth template: metadata: labels: app: auth spec: containers: - name: auth imagePullPolicy: Always image: registry.cn-shanghai.aliyuncs.com/kubesre01/auth:v1 ports: - containerPort: 8080---apiVersion: v1kind: Servicemetadata: name: auth-svcspec: type: ClusterIP selector: app: auth ports: - port: 8080 targetPort: 8080$ kubectl apply -f auth.ymldeployment.apps/auth unchangedservice/auth-svc unchanged
# 部署demo服务$ cat demo.ymlapiVersion: apps/v1kind: Deploymentmetadata: name: demo labels: app: demospec: replicas: 1 selector: matchLabels: app: demo template: metadata: labels: app: demo spec: containers: - name: demo imagePullPolicy: Always image: registry.cn-shanghai.aliyuncs.com/kubesre01/demo:v1 ports: - containerPort: 8080---apiVersion: v1kind: Servicemetadata: name: demo-svcspec: type: ClusterIP selector: app: demo ports: - port: 8080 targetPort: 8080
$ kubectl apply -f demo.ymldeployment.apps/demo unchangedservice/demo-svc unchanged
九、验证JWT认证插件
安装插件:
配置域名:配置路由:
为demo服务配置策略:
验证: 先通过auth服务接口获取到Token
携带Token进行请求demo服务,正常返回
不携带Token进行请求demo服务,返回401,详情如下
十、总结
本文带大家了解Higress云原生网关与Wasm,并通过最佳实践开发了基于Wasm的Higress JWT认证插件的整个过程,相信大家一定有所收获,接下来文章内容中会分享更多企业级实战案例,请敬请期待!
添加下面微信,拉你进群与大佬一起探讨云原生!