在 Go 中实现 gRPC 授权
在 gRPC 服务中,每次调用都对应一个完整的方法名,例如 /library.v1.BookService/DeleteBook。这让方法级授权变得很简单:决定哪些角色可以调用哪些方法,在处理函数运行前用服务端拦截器检查即可。本教程在 Go 中用 Casbin 实现这一点,同时覆盖一元调用和流式调用。
1. 定义服务
示例是一个小型图书馆服务。保存为 proto/library/v1/library.proto:
proto/library/v1/library.proto
syntax = "proto3";
package library.v1;
option go_package = "example.com/library/gen/library/v1;libraryv1";
service BookService {
rpc GetBook(GetBookRequest) returns (Book);
rpc CreateBook(CreateBookRequest) returns (Book);
rpc DeleteBook(DeleteBookRequest) returns (DeleteBookResponse);
}
message Book {
string id = 1;
string title = 2;
}
message GetBookRequest {
string id = 1;
}
message CreateBookRequest {
string title = 1;
}
message DeleteBookRequest {
string id = 1;
}
message DeleteBookResponse {}
用 protoc(或 buf generate)生成 Go 代码:
go mod init example.com/library
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
protoc -Iproto --go_out=. --go_opt=module=example.com/library --go-grpc_out=. --go-grpc_opt=module=example.com/library proto/library/v1/library.proto
go get github.com/casbin/casbin/v3 google.golang.org/grpc
2. 编写模型
保存为 model.conf:
[request_definition]
r = sub, method
[policy_definition]
p = sub, method
[role_definition]
g = _, _
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
m = g(r.sub, p.sub) && keyMatch(r.method, p.method)
r = sub, method:每个请求由调用方和完整的 gRPC 方法名组成。g = _, _:调用方可以被分配到角色,角色也可以被分配到其他角色。keyMatch允许规则以*结尾,这样一行就能覆盖一个服务的所有方法。
3. 编写策略
保存为 policy.csv:
p, reader, /library.v1.BookService/GetBook
p, librarian, /library.v1.BookService/CreateBook
p, admin, /library.v1.BookService/*
g, librarian, reader
g, admin, librarian
g, alice, admin
g, bob, librarian
g, carol, reader
reader 可以调用 GetBook,librarian 还可以调用 CreateBook,admin 可以调用 BookService 的所有方法,包括以后新增的方法。角色之间的 g 行构成层级,所以每个角色只需列出本层新增的权限。
4. 编写拦截器
server/authz.go
package main
import (
"context"
"github.com/casbin/casbin/v3"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
)
// authorize asks Casbin whether the caller may invoke fullMethod, such as /library.v1.BookService/DeleteBook.
func authorize(ctx context.Context, e *casbin.Enforcer, fullMethod string) error {
// Replace this with the identity from your authentication layer,
// such as a verified JWT in the "authorization" metadata or the client's mTLS certificate.
md, _ := metadata.FromIncomingContext(ctx)
users := md.Get("x-user")
if len(users) == 0 {
return status.Error(codes.Unauthenticated, "missing user")
}
allowed, err := e.Enforce(users[0], fullMethod)
if err != nil {
return status.Error(codes.Internal, err.Error())
}
if !allowed {
return status.Errorf(codes.PermissionDenied, "%s may not call %s", users[0], fullMethod)
}
return nil
}
func UnaryAuthz(e *casbin.Enforcer) grpc.UnaryServerInterceptor {
return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) {
if err := authorize(ctx, e, info.FullMethod); err != nil {
return nil, err
}
return handler(ctx, req)
}
}
func StreamAuthz(e *casbin.Enforcer) grpc.StreamServerInterceptor {
return func(srv any, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
if err := authorize(ss.Context(), e, info.FullMethod); err != nil {
return err
}
return handler(srv, ss)
}
}
未认证的调用方得到 Unauthenticated,已认证但没有权限的调用方得到 PermissionDenied,这正是 gRPC 客户端和网关所期望的。
备注
示例用 x-user 元数据保持简短。 Casbin 只负责授权。实际服务中,应校验 authorization 元数据中的令牌;使用 mTLS 时,用 peer.FromContext 从客户端证书中获取身份。如果已经有认证拦截器做了这件事,把它串在授权拦截器之前。
5. 在服务器上注册
server/main.go
package main
import (
"context"
"log"
"net"
"github.com/casbin/casbin/v3"
"google.golang.org/grpc"
libraryv1 "example.com/library/gen/library/v1"
)
type bookService struct {
libraryv1.UnimplementedBookServiceServer
}
func (bookService) GetBook(ctx context.Context, req *libraryv1.GetBookRequest) (*libraryv1.Book, error) {
return &libraryv1.Book{Id: req.GetId(), Title: "The Go Programming Language"}, nil
}
func (bookService) CreateBook(ctx context.Context, req *libraryv1.CreateBookRequest) (*libraryv1.Book, error) {
return &libraryv1.Book{Id: "2", Title: req.GetTitle()}, nil
}
func (bookService) DeleteBook(ctx context.Context, req *libraryv1.DeleteBookRequest) (*libraryv1.DeleteBookResponse, error) {
return &libraryv1.DeleteBookResponse{}, nil
}
func main() {
e, err := casbin.NewEnforcer("model.conf", "policy.csv")
if err != nil {
log.Fatal(err)
}
s := grpc.NewServer(
grpc.ChainUnaryInterceptor(UnaryAuthz(e)),
grpc.ChainStreamInterceptor(StreamAuthz(e)),
)
libraryv1.RegisterBookServiceServer(s, bookService{})
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatal(err)
}
log.Fatal(s.Serve(lis))
}
grpc.ChainUnaryInterceptor 和 grpc.ChainStreamInterceptor 可以接收多个拦截器,所以日志、指标和认证都可以和授权检查放在一起。