增强 HTNN 服务网格功能:基于 Istio 的BasicAuth 与 ACL 插件开发实战
目录
1.引言
什么是HTNN?
为什么开发 BasicAuth 和 ACL 插件?
2.技术背景
技术栈概览
Istio 与服务网格简述
HTNN 框架与插件机制概览
3.插件开发详解:BasicAuth 与 ACL
3.1 BasicAuth插件
功能点
实现细节
3.2 ACL插件
功能点
实现细节
4.实践过程
4.1本地构建自定义HTNN镜像
1.修改源码并添加插件
2.构建Docker镜像
4.2 在 Kubernetes 中部署插件
1.将构建好的数据面和控制面的镜像先导入到Kubernetes集群
2.修改Helm Chart的 values.yaml文件
4.3 测试验证流程
1.引言
什么是HTNN?
HTNN 是一个基于 Istio 构建的服务网格增强项目,通过插件机制来扩展和定制流量控制、安全策略等能力。与 Istio 原生的 Mixer 插件体系不同,HTNN 提供了更加灵活和轻量的插件注册与运行方式,适合快速验证和迭代。
项目地址:https://github.com/mosn/htnn
为什么开发 BasicAuth 和 ACL 插件?
在 HTNN 官方仓库中,虽然提供了如
key-auth
和hmac-auth
等认证插件,但对于传统的用户名密码认证(BasicAuth)以及基于源 IP 的访问控制(ACL)仍未支持。在一些面向公网服务或细粒度控制场景中,这类插件是非常常用且必要的。因此,我基于 HTNN 插件机制,自行开发并集成了:
basicAuth
插件:用于支持 HTTP Basic Authentication,适用于快速接入第三方平台或内部服务接口保护。
acl
插件:实现对来源 IP 的白名单与黑名单策略配置,增强服务边界的访问控制能力。
2.技术背景
技术栈概览
技术 | 作用 |
---|---|
Go | 编写插件逻辑,HTNN 本身使用 Go 开发 |
Protobuf | 定义插件配置结构、实现插件与框架间的序列化通信 |
Docker | 构建包含自定义插件的 HTNN 镜像 |
Kubernetes | 部署测试 HTNN 与插件,验证实际运行效果 |
Linux | 本地开发与测试环境,使用 shell 工具辅助调试 |
Istio | 服务网格的核心组件,HTNN 作为其增强层构建在上方 |
Istio 与服务网格简述
Istio 是当前主流的服务网格框架,提供流量管理、安全控制、可观测性等能力。在微服务架构中,Istio 通过 sidecar(通常是 Envoy)注入方式拦截进出流量,实现服务间的无侵入治理。然而,Istio 自身对于某些认证或访问控制的定制化支持较为复杂或不够灵活,这正是 HTNN 出现的背景。
HTNN 框架与插件机制概览
热插拔机制:插件通过 plugins.RegisterPlugin
接口注册,支持动态加载与配置。
统一请求拦截点:插件可以在请求进入 Envoy 之前对其进行处理(如验证、拦截)。
可配置结构:通过 Protobuf 配置文件或 ConfigMap 传递运行参数,实现运行时灵活控制。
与 Istio 无缝集成:在 Istio 的 service mesh 中以 sidecar 的形式运行,兼容 Istio 原生组件。
3.插件开发详解:BasicAuth 与 ACL
3.1 BasicAuth插件
功能点
1.基于 HTTP Basic Auth 的认证:
验证 HTTP 请求中的 Authorization
头部,确保用户提供了正确的用户名和密码。
如果认证失败,返回 HTTP 401 状态码。
2.支持多用户凭据:
插件支持配置多个用户名和密码对,允许不同用户访问。
3.动态配置:
用户名和密码通过配置文件动态加载,支持灵活调整
4.安全性:
使用 Base64 解码解析 Authorization
头部中的凭据。
如果凭据无效或缺失,拒绝请求。
实现细节
1. 配置结构
配置文件定义在 [config.go] 中:
type Config struct {Credentials map[string]string `json:"credentials,omitempty"`
}
Credentials
字段:
键是用户名,值是对应的密码。
示例配置:
{"credentials": {"user1": "password1","user2": "password2"}
}
2. 插件注册
插件通过 plugins.RegisterPlugin 注册到框架中:
func init() {plugins.RegisterPlugin(basicauth.Name, &plugin{})
}
3. 核心逻辑
核心逻辑实现于 [filter.go
] 中:
func (f *filter) DecodeHeaders(headers api.RequestHeaderMap, endStream bool) api.ResultAction {authHeader, ok := headers.Get("Authorization")if !ok || !strings.HasPrefix(authHeader, "Basic ") {return &api.LocalResponse{Code: 401, Msg: "missing or invalid Authorization header"}}encodedCredentials := strings.TrimPrefix(authHeader, "Basic ")decoded, err := base64.StdEncoding.DecodeString(encodedCredentials)if err != nil {return &api.LocalResponse{Code: 401, Msg: "invalid Authorization header"}}parts := strings.SplitN(string(decoded), ":", 2)if len(parts) != 2 {return &api.LocalResponse{Code: 401, Msg: "invalid Authorization header"}}username, password := parts[0], parts[1]if validPassword, ok := f.config.Credentials[username]; !ok || validPassword != password {return &api.LocalResponse{Code: 401, Msg: "invalid username or password"}}return api.Continue
}
逻辑说明:
检查
Authorization
头部是否存在,且是否以Basic
开头。使用 Base64 解码凭据,并解析出用户名和密码。
验证用户名和密码是否匹配配置中的凭据。
如果验证失败,返回 HTTP 401;否则继续处理请求。
4. 单元测试
单元测试位于 [filter_test.go
]:
func TestBasicAuthFilter(t *testing.T) {tests := []struct {name stringconf stringheaders map[string][]stringexpected int}{{name: "valid credentials",conf: `{"credentials": {"user1": "password1","user2": "password2"}}`,headers: map[string][]string{"Authorization": {"Basic " + base64.StdEncoding.EncodeToString([]byte("user1:password1"))},},expected: 0,},{name: "invalid credentials",conf: `{"credentials": {"user1": "password1","user2": "password2"}}`,headers: map[string][]string{"Authorization": {"Basic " + base64.StdEncoding.EncodeToString([]byte("user1:wrongpassword"))},},expected: 401,},}
}
测试覆盖了以下场景:
有效的用户名和密码。
无效的用户名或密码。
缺失
Authorization
头部
3.2 ACL插件
功能点
1.基于IP地址的访问控制:
支持通过 allow_list
和 deny_list
配置访问规则。
优先检查 deny_list
,如果匹配则拒绝访问。
2.CIDR支持:
支持 CIDR 格式的 IP 地址范围匹配。
3.动态配置:
通过配置文件动态加载访问控制规则。
4.安全性:
如果请求的 IP 地址不在 allow_list
中,默认拒绝访问。
实现细节
1.配置结构
配置文件定义在 [config.go] 中:
type Config struct {AllowList []string `json:"allow_list,omitempty"`DenyList []string `json:"deny_list,omitempty"`
}
AllowList
和 DenyList
字段:
AllowList
:允许访问的 IP 地址或 CIDR 范围。
DenyList
:拒绝访问的 IP 地址或 CIDR 范围。
示例配置:
{"allow_list": ["192.168.1.0/24"],"deny_list": ["10.0.0.1"]
}
2.插件注册
插件通过 plugins.RegisterPlugin 注册到框架中:
func init() {plugins.RegisterPlugin(acl.Name, &plugin{})
}
3.核心逻辑
核心逻辑实现于 [filter.go
] 中:
func (f *filter) DecodeHeaders(headers api.RequestHeaderMap, endStream bool) api.ResultAction {clientIP, ok := headers.Get("X-Forwarded-For")if !ok {return &api.LocalResponse{Code: 403, Msg: "client IP not found"}}for _, denyIP := range f.config.DenyList {if isIPMatch(clientIP, denyIP) {return &api.LocalResponse{Code: 403, Msg: "access denied"}}}for _, allowIP := range f.config.AllowList {if isIPMatch(clientIP, allowIP) {return api.Continue}}return &api.LocalResponse{Code: 403, Msg: "access denied"}
}
逻辑说明:
获取请求头中的
X-Forwarded-For
字段,提取客户端 IP。检查 IP 是否匹配
deny_list
,如果匹配则拒绝访问。检查 IP 是否匹配
allow_list
,如果匹配则允许访问。如果未匹配任何规则,默认拒绝访问。
4.单元测试
单元测试位于 [filter_test.go
]:
func TestACLFilter(t *testing.T) {tests := []struct {name stringconf stringheaders map[string][]stringexpected int}{{name: "deny list match",conf: `{"allow_list": ["192.168.1.0/24"],"deny_list": ["10.0.0.1"]}`,headers: map[string][]string{"X-Forwarded-For": {"10.0.0.1"},},expected: 403,},{name: "allow list match",conf: `{"allow_list": ["192.168.1.0/24"],"deny_list": ["10.0.0.1"]}`,headers: map[string][]string{"X-Forwarded-For": {"192.168.1.50"},},expected: 0,},}
}
测试覆盖了以下场景
匹配
deny_list
。匹配
allow_list
。未匹配任何规则。
4.实践过程
4.1本地构建自定义HTNN镜像
1.修改源码并添加插件
分别在types\plugins(实现配置)和plugins\plugins(实现逻辑)目录下新增basicauth和acl目录。
分别在两个目录下的主插件入口调用RegisterPlugin(...)注册。
2.构建Docker镜像
先检查实现.editorconfig文件的代码格式规范,避免因系统版本不兼容导致换行符等问题影响镜像的构建
dockerfile文件直接复用 htnn\manifests\images\cp\Dockerfile 和htnn\manifests\images\dp\Dockerfile 就行
cd到 htnn\manifests\Makefile 目录下执行 make 命令完成构建:
make build-proxy-imagemake build-controller-image
4.2 在 Kubernetes 中部署插件
1.将构建好的数据面和控制面的镜像先导入到Kubernetes集群
minikube image load your-image-name1:tagminikube image load your-image-name2:tag
2.修改Helm Chart的 values.yaml文件
#文件 htnn\manifests\charts\htnn-controller\values.yaml
hub: ""image: htnn-controllertag: "latest"imagePullPolicy: "Never" # 不使用远程镜像
#文件 manifests\charts\htnn-gateway\values.yaml
gateway:name: istio-ingressgatewayimage: m.daocloud.io/docker.io/envoyproxy/envoy:latestimagePullPolicy: Neverenv:ISTIO_DELTA_XDS: "true"
#默认的manifests\charts\htnn-gateway\values.schema.json文件中没有镜像参数
#需要在json文件中添加该结构参数
3.部署或更新 HTNN 到 Kubernetes
首次部署:
控制面
helm install htnn-controller 本地Path\htnn\manifests\charts\htnn-controller --namespace istio-system --create-namespace -f 本地Path\htnn\manifests\charts\htnn-controller\values.yaml
数据面
helm install htnn-gateway 本地Path\htnn\manifests\charts\htnn-gateway -n istio-system -f 本地Path\htnn\manifests\charts\htnn-gateway\values.yaml
已有部署则使用升级命令 upgrade 替换 install
4.查看 istio-system
命名空间下所有 Pod 的运行状态
PS C:\WINDOWS\system32> kubectl -n istio-system get pods
NAME READY STATUS RESTARTS AGE
istio-ingressgateway-674cd8d4c9-lg692 1/1 Running 3 (3d2h ago) 3d20h
istiod-6b6c464bb7-2df62 1/1 Running 3 (3d2h ago) 3d20h
如果出现异常状态,用以下命令查看原因
kubectl -n istio-system describe pod <pod-name>
4.3 测试验证流程
分别进行后端服务部署、入口流量管理、路由规则定义和安全认证策略配置,配置到kubernetes环境中
部署后端服务
//backend.yaml
---
apiVersion: v1
kind: Service
metadata:name: backendnamespace: istio-systemlabels:app: backend
spec:ports:- port: 80name: httptargetPort: 5678selector:app: backend
---
apiVersion: apps/v1
kind: Deployment
metadata:name: backendnamespace: istio-system
spec:replicas: 1selector:matchLabels:app: backendtemplate:metadata:labels:app: backendspec:containers:- name: backendimage: hashicorp/http-echoargs:- "-text=hello from backend"ports:- containerPort: 5678
定义入口网关
//gateway.yaml
apiVersion: networking.istio.io/v1beta1
kind: Gateway
metadata:name: htnn-gatewaynamespace: istio-system
spec:selector:istio: ingressgatewayservers:- port:number: 80name: httpprotocol: HTTPhosts:- "*"
定义虚拟服务路由规则
//vs-basicauth.yaml
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:name: vs-basicauthnamespace: istio-system
spec:hosts:- "*"gateways:- htnn-gatewayhttp:- match:- uri:prefix: /route:- destination:host: backend.istio-system.svc.cluster.localport:number: 80
定义 basicAuth 插件的策略
//basicauthpolicy.yaml
apiVersion: htnn.mosn.io/v1
kind: FilterPolicy
metadata:name: basicauth-policynamespace: istio-system
spec:targetRef:group: networking.istio.iokind: VirtualServicename: vs-basicauthfilters:basicAuth:config:credentials:admin: "admin123"user: "user123"
先通过 status 检查下策略是否被接受:
$ kubectl -n istio-system get filterpolicies.htnn.mosn.io policy -o yaml···
status:conditions:- lastTransitionTime: "2025-05-15T13:56:42Z"message: The policy has been acceptedobservedGeneration: 1reason: Acceptedstatus: "True"type: Accepted
kind: List
metadata:resourceVersion: ""
让我们在一个终端上执行 port-forward,让本地的客户端可以访问到 k8s 里面的服务:
kubectl port-forward -n istio-system svc/istio-ingressgateway 30474:80
在另一个终端上,我们可以通过 30474 端口访问到 HTNN:
//正确的用户名和密码
curl -v --user admin:admin123 http://localhost:30474/get
HTTP/1.1 200 OK
//不正确的
curl -v --user admin:admin121 http://localhost:30474/get
HTTP/1.1 401 Unauthorized
以上是basicauth插件的测试流程,acl的流程是一样的,先将配置文件写好,再应用策略:
//aclpolicy.yaml
apiVersion: htnn.mosn.io/v1
kind: FilterPolicy
metadata:name: acl-policynamespace: istio-system
spec:targetRef:group: networking.istio.iokind: VirtualServicename: vs-aclfilters:acl:config:allow_list:- "192.168.1.0/24"deny_list:- "10.0.0.1"
让我们在一个终端上执行 port-forward,让本地的客户端可以访问到 k8s 里面的服务:
kubectl port-forward -n istio-system svc/istio-ingressgateway 30474:80
在另一个终端上,我们可以通过 30474 端口访问到 HTNN:
curl -v --header "Host: backend.local" --header "X-Forwarded-For: 192.168.1.100" http://localhost:30474/getHTTP/1.1 200 OKcurl -v --header "Host: backend.local" --header "X-Forwarded-For: 10.0.0.1" http://localhost:30474/getHTTP/1.1 403 Forbidden