Files
juwan-backend/docs/ENVOY_EXT_AUTHZ_ADAPTER.md
T
wwweww 659168fe32 feat: add authz-adapter service and Envoy ext_authz integration
- Implemented authz-adapter deployment and service for Envoy gRPC authorization.
- Created PowerShell script to generate JWK for JWT authentication.
- Documented the integration of ext_authz with user-rpc.ValidateToken in ENVOY_EXT_AUTHZ_ADAPTER.md.
- Added comprehensive Envoy Gateway configuration guide with JWT authentication and access control in ENVOY_GATEWAY_GUIDE.md.
2026-02-26 06:08:35 +08:00

229 lines
6.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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/v3`
- `github.com/envoyproxy/go-control-plane/envoy/type/v3`
---
## 2) 最小 Go 适配器骨架
```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()
method := strings.ToUpper(httpReq.GetMethod())
// 放行公共接口(探针、登录/注册、发送验证码)
if path == "/healthz" ||
path == "/api/users/login" ||
path == "/api/users/register" ||
path == "/api/email/verification-code/send" {
return allow(nil), nil
}
token := extractCookie(httpReq.GetHeaders(), "JToken")
if token == "" {
return deny(401, "missing token cookie"), nil
}
userID, err := parseUserIDFromPath(path)
if err != nil {
return deny(401, "invalid user id in path"), 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: strconv.FormatInt(vt.RoleType, 10)},
},
{
Header: &core.HeaderValue{Key: "x-auth-method", Value: method},
},
}
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 parseUserIDFromPath(path string) (int64, error) {
// 仅示例:请按你的真实路由解析,或改为从 token claim 取 userId
seg := strings.Split(strings.Trim(path, "/"), "/")
for i := 0; i < len(seg); i++ {
if seg[i] == "users" && i+1 < len(seg) {
return strconv.ParseInt(seg[i+1], 10, 64)
}
}
return 0, strconv.ErrSyntax
}
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` 之前加入:
```yaml
- 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` 中加入:
```yaml
- 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) 落地清单
1. 新建 `authz-adapter` 服务(Deployment + Service)。
2. Adapter 内部连 `user-rpc-svc:9001`,调用 `ValidateToken`
3. Envoy 加 `ext_authz` filter + `authz_adapter_cluster`
4. 明确失败语义:
- 无 token -> 401
- token 无效/过期 -> 401
- 权限不足 -> 403
5. 透传统一鉴权头:`x-auth-user-id``x-auth-role-type`
6. 灰度建议:先仅对 `/api/users` 开启,再扩展到 `/api/email`
> 实践建议:若保留 K8s `readiness/liveness` 探针使用 `/healthz`,请确保该路径在 `ext_authz` 上也放行,否则会出现探针 403 导致 Pod 重启。
---
## 5) 与当前 `jwt_authn` 的关系
- 可以并存:
-`jwt_authn` 快速验签
-`ext_authz` 做 Redis 会话态、黑名单、细粒度权限
- 也可以只保留 `ext_authz`(由 adapter 内完成全部逻辑)。
推荐:**先并存**,稳定后再决定是否简化。