6.7 KiB
6.7 KiB
Envoy ext_authz 适配 user-rpc.ValidateToken(最小实现)
目标
- Envoy 不直接调用业务 proto 方法。
- 新增一个内部服务
authz-adapter,实现 Envoy 标准 gRPC 鉴权接口。 authz-adapter再调用现有user-rpc.ValidateToken完成会话态二次校验。
1) 最小接口定义(Envoy 标准)
authz-adapter 需要实现的是 Envoy 官方服务:
- gRPC Service:
envoy.service.auth.v3.Authorization - RPC Method:
Check(CheckRequest) returns (CheckResponse)
Go 里通常使用包:
github.com/envoyproxy/go-control-plane/envoy/service/auth/v3github.com/envoyproxy/go-control-plane/envoy/type/v3
2) 最小 Go 适配器骨架
package main
import (
"context"
"net"
"strconv"
"strings"
core "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
authv3 "github.com/envoyproxy/go-control-plane/envoy/service/auth/v3"
typev3 "github.com/envoyproxy/go-control-plane/envoy/type/v3"
"google.golang.org/grpc"
userpb "juwan-backend/app/users/rpc/pb"
)
type server struct {
authv3.UnimplementedAuthorizationServer
userRpc userpb.UsercenterClient
}
func (s *server) Check(ctx context.Context, req *authv3.CheckRequest) (*authv3.CheckResponse, error) {
attrs := req.GetAttributes()
httpReq := attrs.GetRequest().GetHttp()
if httpReq == nil {
return deny(403, "missing http attributes"), nil
}
path := httpReq.GetPath()
// 放行公共接口(探针、登录/注册、发送验证码)
if path == "/healthz" ||
path == "/api/v1/auth/login" ||
path == "/api/v1/auth/register" ||
path == "/api/v1/auth/forgot-password" ||
path == "/api/v1/auth/reset-password" ||
path == "/api/v1/auth/forgot-password/send" ||
path == "/api/v1/email/verification-code/send" {
return allow(nil), nil
}
token := extractCookie(httpReq.GetHeaders(), "JToken")
if token == "" {
return deny(401, "missing token cookie"), nil
}
userIDHeader := getHeader(httpReq.GetHeaders(), "x-auth-user-id")
if userIDHeader == "" {
return deny(401, "missing x-auth-user-id header"), nil
}
userID, err := strconv.ParseInt(userIDHeader, 10, 64)
if err != nil {
return deny(401, "invalid x-auth-user-id"), nil
}
// 调用现有业务 RPC 做会话态二次校验
vt, err := s.userRpc.ValidateToken(ctx, &userpb.ValidateTokenReq{
Token: token,
UserId: userID,
})
if err != nil || vt == nil || !vt.Valid {
return deny(401, "invalid token"), nil
}
// 透传给后端 API
headers := []*core.HeaderValueOption{
{
Header: &core.HeaderValue{Key: "x-auth-user-id", Value: strconv.FormatInt(vt.UserId, 10)},
},
{
Header: &core.HeaderValue{Key: "x-auth-role-type", Value: vt.RoleType},
},
}
return allow(headers), nil
}
func allow(headers []*core.HeaderValueOption) *authv3.CheckResponse {
return &authv3.CheckResponse{
Status: &typev3.Status{Code: int32(typev3.Code_OK)},
HttpResponse: &authv3.CheckResponse_OkResponse{
OkResponse: &authv3.OkHttpResponse{Headers: headers},
},
}
}
func deny(code int32, msg string) *authv3.CheckResponse {
return &authv3.CheckResponse{
Status: &typev3.Status{Code: int32(typev3.Code_PERMISSION_DENIED)},
HttpResponse: &authv3.CheckResponse_DeniedResponse{
DeniedResponse: &authv3.DeniedHttpResponse{
Status: &typev3.HttpStatus{Code: typev3.StatusCode(code)},
Body: "{\"code\":" + strconv.Itoa(int(code)) + ",\"message\":\"" + msg + "\"}",
Headers: []*core.HeaderValueOption{
{Header: &core.HeaderValue{Key: "content-type", Value: "application/json"}},
},
},
},
}
}
func extractCookie(headers map[string]string, name string) string {
c := headers["cookie"]
parts := strings.Split(c, ";")
for _, p := range parts {
kv := strings.SplitN(strings.TrimSpace(p), "=", 2)
if len(kv) == 2 && kv[0] == name {
return kv[1]
}
}
return ""
}
func getHeader(headers map[string]string, name string) string {
for k, v := range headers {
if strings.EqualFold(k, name) {
return v
}
}
return ""
}
func main() {
lis, _ := net.Listen("tcp", ":9002")
grpcServer := grpc.NewServer()
// TODO: 创建 user-rpc client 并注入
// userRpcClient := ...
authv3.RegisterAuthorizationServer(grpcServer, &server{userRpc: nil})
_ = grpcServer.Serve(lis)
}
3) Envoy 最小配置片段(插入现有 http_filters)
在 envoy.filters.http.router 之前加入:
- 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
并在 clusters 中加入:
- name: authz_adapter_cluster
connect_timeout: 0.5s
type: STRICT_DNS
lb_policy: ROUND_ROBIN
load_assignment:
cluster_name: authz_adapter_cluster
endpoints:
- lb_endpoints:
- endpoint:
address:
socket_address:
address: authz-adapter-svc.juwan.svc.cluster.local
port_value: 9002
http2_protocol_options: {}
gRPC 上游必须启用
http2_protocol_options: {}。
4) 落地清单
- 新建
authz-adapter服务(Deployment + Service)。 - Adapter 内部连
user-rpc-svc:9001,调用ValidateToken。 - Envoy 加
ext_authzfilter +authz_adapter_cluster。 - 明确失败语义:
- 无 token -> 401
- token 无效/过期 -> 401
- 权限不足 -> 403
jwt_authn先注入x-auth-user-id、x-auth-is-admin,再由 adapter 透传x-auth-user-id、x-auth-role-type。- 灰度建议:先仅对
/api/v1/auth和/api/v1/email验证链路,再逐步扩展到其它业务路径。
实践建议:若保留 K8s
readiness/liveness探针使用/healthz,请确保该路径在ext_authz上也放行,否则会出现探针 403 导致 Pod 重启。
5) 与当前 jwt_authn 的关系
- 可以并存:
- 先
jwt_authn快速验签并注入 claim header - 再
ext_authz做 Redis 会话态、黑名单、细粒度权限
- 先
- 也可以只保留
ext_authz(由 adapter 内完成全部逻辑)。
推荐:先并存,稳定后再决定是否简化。