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.
This commit is contained in:
@@ -0,0 +1,228 @@
|
||||
# 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 内完成全部逻辑)。
|
||||
|
||||
推荐:**先并存**,稳定后再决定是否简化。
|
||||
Reference in New Issue
Block a user