Appearance
gRPC 安全与 TLS
gRPC 默认要求 TLS 加密传输——这不是可选项,而是框架层面的安全设计。本篇深入 gRPC 的传输安全机制,从 TLS 基础概念、单向 TLS、mTLS 双向认证,到基于 metadata 的 Token/JWT 认证和拦截器授权,最终构建一个完整的安全 gRPC 服务。
一、gRPC 传输安全概述
gRPC 的安全模型分为两层:
┌──────────────────────────────────────────────────┐
│ 应用层安全(认证 & 授权) │
│ - Token / JWT 认证 │
│ - OAuth2 │
│ - RBAC / ABAC 授权 │
│ 实现方式:metadata + 拦截器 │
├──────────────────────────────────────────────────┤
│ 传输层安全(TLS) │
│ - 单向 TLS:客户端验证服务端身份 │
│ - 双向 TLS(mTLS):双方互相验证 │
│ 实现方式:credentials 包 │
└──────────────────────────────────────────────────┘为什么 gRPC 强制 TLS:
- 防止中间人攻击(MITM),保护传输中的数据。
- 内部服务间通信也不应裸奔——零信任网络模型要求所有通信加密。
- gRPC-Go 中如果不用 TLS,必须显式传入
insecure.NewCredentials(),这种「显式不安全」的设计迫使开发者意识到风险。
二、TLS 基础:证书、密钥、CA
1. 核心概念
| 概念 | 说明 |
|---|---|
| 私钥(key) | 用于解密和签名,必须保密 |
| 公钥(cert) | 用于加密和验签,可公开 |
| 证书(cert) | 公钥 + 身份信息 + CA 签名,证明公钥属于某个主体 |
| CA | 证书颁发机构,用自身私钥签发证书 |
| CSR | 证书签名请求,包含公钥和身份信息,提交给 CA 签名 |
| 证书链 | 证书可能由中间 CA 签发,需要追溯到根 CA |
2. TLS 握手流程
单向 TLS:
客户端 服务端
| --- ClientHello (支持的TLS版本、密码套件) ---> |
| <-- ServerHello + 证书 + 公钥 --------------- |
| --- 验证证书 -> 生成对称密钥 -> 加密发送 -----> |
| <-- 用对称密钥加密通信 ----------------------- |
双向 TLS (mTLS):
客户端 服务端
| --- ClientHello ----------------------------> |
| <-- ServerHello + 服务端证书 ----------------- |
| --- 客户端证书 + 用服务端公钥加密的密钥 -----> |
| <-- 验证客户端证书 -> 确认通信 --------------- |三、自签名证书生成
在开发和内部服务间通信中,自签名 CA + 证书是常见做法。下面用 Go 代码生成完整的证书体系。
go
package main
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"fmt"
"math/big"
"os"
"time"
)
// 生成自签名 CA 证书
func generateCA() (*x509.Certificate, interface{}, error) {
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return nil, nil, err
}
template := &x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{
Organization: []string{"Example CA"},
CommonName: "Example Root CA",
},
NotBefore: time.Now(),
NotAfter: time.Now().Add(10 * 365 * 24 * time.Hour),
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
BasicConstraintsValid: true,
IsCA: true,
MaxPathLen: 1,
}
certDER, err := x509.CreateCertificate(rand.Reader, template, template, &priv.PublicKey, priv)
if err != nil {
return nil, nil, err
}
cert, err := x509.ParseCertificate(certDER)
if err != nil {
return nil, nil, err
}
return cert, priv, nil
}
// 用 CA 签发服务端/客户端证书
func generateCert(caCert *x509.Certificate, caPriv interface{},
commonName string, isServer bool,
) (*x509.Certificate, interface{}, error) {
priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return nil, nil, err
}
template := &x509.Certificate{
SerialNumber: big.NewInt(time.Now().Unix()),
Subject: pkix.Name{
Organization: []string{"Example"},
CommonName: commonName,
},
NotBefore: time.Now(),
NotAfter: time.Now().Add(365 * 24 * time.Hour),
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
ExtKeyUsage: []x509.ExtKeyUsage{},
}
if isServer {
template.ExtKeyUsage = append(template.ExtKeyUsage, x509.ExtKeyUsageServerAuth)
template.DNSNames = []string{"localhost", "example.com"}
template.IPAddresses = []net.IP{net.ParseIP("127.0.0.1")}
} else {
template.ExtKeyUsage = append(template.ExtKeyUsage, x509.ExtKeyUsageClientAuth)
}
certDER, err := x509.CreateCertificate(rand.Reader, template, caCert, &priv.PublicKey, caPriv)
if err != nil {
return nil, nil, err
}
cert, err := x509.ParseCertificate(certDER)
if err != nil {
return nil, nil, err
}
return cert, priv, nil
}
// 保存证书和私钥到 PEM 文件
func saveCertPEM(filename string, cert *x509.Certificate) error {
f, err := os.Create(filename)
if err != nil {
return err
}
defer f.Close()
return pem.Encode(f, &pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw})
}
func saveKeyPEM(filename string, priv interface{}) error {
keyBytes, err := x509.MarshalPKCS8PrivateKey(priv)
if err != nil {
return err
}
f, err := os.Create(filename)
if err != nil {
return err
}
defer f.Close()
return pem.Encode(f, &pem.Block{Type: "PRIVATE KEY", Bytes: keyBytes})
}
func main() {
// 1. 生成 CA
caCert, caPriv, err := generateCA()
if err != nil {
fmt.Printf("generate CA: %v\n", err)
return
}
fmt.Printf("CA cert: CN=%s, valid until %s\n",
caCert.Subject.CommonName, caCert.NotAfter.Format("2006-01-02"))
// 2. 用 CA 签发服务端证书
serverCert, serverKey, err := generateCert(caCert, caPriv, "server.example.com", true)
if err != nil {
fmt.Printf("generate server cert: %v\n", err)
return
}
fmt.Printf("Server cert: CN=%s, SAN=%v\n",
serverCert.Subject.CommonName, serverCert.DNSNames)
// 3. 用 CA 签发客户端证书(用于 mTLS)
clientCert, clientKey, err := generateCert(caCert, caPriv, "client.example.com", false)
if err != nil {
fmt.Printf("generate client cert: %v\n", err)
return
}
fmt.Printf("Client cert: CN=%s, ExtKeyUsage=%v\n",
clientCert.Subject.CommonName, clientCert.ExtKeyUsage)
// 4. 保存到文件(实际项目中保存到 certs/ 目录)
_ = saveCertPEM("ca.crt", caCert)
_ = saveKeyPEM("ca.key", caPriv)
_ = saveCertPEM("server.crt", serverCert)
_ = saveKeyPEM("server.key", serverKey)
_ = saveCertPEM("client.crt", clientCert)
_ = saveKeyPEM("client.key", clientKey)
fmt.Println("\nAll certificates generated successfully")
fmt.Println("Files: ca.crt, ca.key, server.crt, server.key, client.crt, client.key")
}也可以用 OpenSSL 命令行生成:
bash
# 生成 CA 私钥和证书
openssl genrsa -out ca.key 2048
openssl req -new -x509 -key ca.key -out ca.crt -days 3650 -subj "/CN=Example CA"
# 生成服务端证书
openssl genrsa -out server.key 2048
openssl req -new -key server.key -out server.csr -subj "/CN=localhost"
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -days 365
# 生成客户端证书(mTLS 用)
openssl genrsa -out client.key 2048
openssl req -new -key client.key -out client.csr -subj "/CN=client"
openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out client.crt -days 365四、服务端 TLS 配置
go
package main
import (
"fmt"
"log"
"net"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
)
// 服务端单向 TLS:客户端验证服务端身份
func newTLSServer(addr string) {
// 方式一:从文件加载证书(最简单)
// creds, err := credentials.NewServerTLSFromFile("server.crt", "server.key")
// if err != nil {
// log.Fatalf("load TLS: %v", err)
// }
// 方式二:使用 tls.Config(更灵活)
// cert, _ := tls.LoadX509KeyPair("server.crt", "server.key")
// tlsConfig := &tls.Config{
// Certificates: []tls.Certificate{cert},
// MinVersion: tls.VersionTLS12, // 强制 TLS 1.2+
// }
// creds := credentials.NewTLS(tlsConfig)
// 完整服务端启动
lis, err := net.Listen("tcp", addr)
if err != nil {
log.Fatalf("listen: %v", err)
}
// server := grpc.NewServer(grpc.Creds(creds))
// userv1.RegisterUserServiceServer(server, &userServiceImpl{})
// log.Println(server.Serve(lis))
fmt.Printf("TLS server would listen on %s\n", addr)
}
func main() {
// 演示 credentials 包的用法
// 实际运行需要证书文件
fmt.Println("Server TLS configuration:")
fmt.Println(" credentials.NewServerTLSFromFile(certFile, keyFile)")
fmt.Println(" credentials.NewTLS(tlsConfig)")
// 用 nil tls.Config 演示选项组装(实际使用需真实配置)
server := grpc.NewServer(
grpc.Creds(credentials.NewTLS(nil)),
)
defer server.Stop()
fmt.Printf("server created: %T\n", server)
newTLSServer(":50051")
}五、客户端 TLS 配置
go
package main
import (
"crypto/tls"
"crypto/x509"
"fmt"
"log"
"os"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
)
// 客户端单向 TLS:验证服务端证书
func newTLSClient(addr string) {
// 方式一:信任系统证书 + 指定 serverName
// creds, err := credentials.NewClientTLSFromFile("server.crt", "example.com")
// if err != nil {
// log.Fatalf("load TLS: %v", err)
// }
// conn, _ := grpc.NewClient(addr, grpc.WithTransportCredentials(creds))
// 方式二:自定义 CA 池(用于自签名证书)
certPool := x509.NewCertPool()
// caCert, _ := os.ReadFile("ca.crt")
// certPool.AppendCertsFromPEM(caCert)
tlsConfig := &tls.Config{
RootCAs: certPool, // 信任的 CA 列表
ServerName: "example.com", // 必须与证书 CN/SAN 匹配
MinVersion: tls.VersionTLS12,
}
creds := credentials.NewTLS(tlsConfig)
conn, err := grpc.NewClient(addr, grpc.WithTransportCredentials(creds))
if err != nil {
log.Fatal(err)
}
defer conn.Close()
fmt.Printf("TLS client connected to %s\n", addr)
}
// 开发环境:跳过证书验证(仅用于测试,绝不用于生产)
func newInsecureTLSClient(addr string) {
tlsConfig := &tls.Config{
InsecureSkipVerify: true, // 危险!跳过证书验证
}
creds := credentials.NewTLS(tlsConfig)
conn, _ := grpc.NewClient(addr, grpc.WithTransportCredentials(creds))
defer conn.Close()
fmt.Printf("Insecure TLS client (testing only) -> %s\n", addr)
}
func main() {
// 演示 CA 池加载
certPool := x509.NewCertPool()
fmt.Printf("Empty cert pool created: %v\n", certPool)
// 实际加载 CA 证书
// if caCert, err := os.ReadFile("ca.crt"); err == nil {
// if certPool.AppendCertsFromPEM(caCert) {
// fmt.Println("CA cert added to pool")
// }
// }
_ = os.ReadFile
newTLSClient("example.com:443")
}六、双向 TLS(mTLS)
mTLS 要求客户端也出示证书,服务端验证客户端身份。这是内部服务间最高安全级别的认证方式。
go
package main
import (
"crypto/tls"
"crypto/x509"
"fmt"
"log"
"net"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
)
// === mTLS 服务端配置 ===
func newMTLSServer(addr string) {
// 加载服务端证书和私钥
// serverCert, err := tls.LoadX509KeyPair("server.crt", "server.key")
// if err != nil {
// log.Fatalf("load server cert: %v", err)
// }
// 加载 CA 证书用于验证客户端
certPool := x509.NewCertPool()
// caCert, _ := os.ReadFile("ca.crt")
// certPool.AppendCertsFromPEM(caCert)
tlsConfig := &tls.Config{
Certificates: []tls.Certificate{}, // serverCert,
ClientAuth: tls.RequireAndVerifyClientCert, // 强制要求并验证客户端证书
ClientCAs: certPool, // 信任的 CA
MinVersion: tls.VersionTLS12,
}
// ClientAuth 选项说明:
// tls.NoClientCert - 不要求客户端证书
// tls.RequestClientCert - 请求但不要求
// tls.RequireAnyClientCert - 要求但不验证
// tls.VerifyClientCertIfGiven - 如果提供了则验证
// tls.RequireAndVerifyClientCert - 强制要求并验证(mTLS 用这个)
creds := credentials.NewTLS(tlsConfig)
lis, _ := net.Listen("tcp", addr)
server := grpc.NewServer(grpc.Creds(creds))
// 注册服务...
_ = server
_ = lis
fmt.Printf("mTLS server on %s (requires client cert)\n", addr)
}
// === mTLS 客户端配置 ===
func newMTLSClient(addr string) {
// 加载客户端证书和私钥
// clientCert, err := tls.LoadX509KeyPair("client.crt", "client.key")
// if err != nil {
// log.Fatalf("load client cert: %v", err)
// }
// 加载 CA 用于验证服务端
certPool := x509.NewCertPool()
// caCert, _ := os.ReadFile("ca.crt")
// certPool.AppendCertsFromPEM(caCert)
tlsConfig := &tls.Config{
Certificates: []tls.Certificate{}, // clientCert, // 客户端证书
RootCAs: certPool, // 信任的 CA
ServerName: "example.com",
MinVersion: tls.VersionTLS12,
}
creds := credentials.NewTLS(tlsConfig)
conn, err := grpc.NewClient(addr, grpc.WithTransportCredentials(creds))
if err != nil {
log.Fatal(err)
}
defer conn.Close()
fmt.Printf("mTLS client -> %s (with client cert)\n", addr)
}
func main() {
// 演示 mTLS 的 TLS 配置结构
fmt.Println("=== mTLS Server Config ===")
fmt.Println(" Certificates: [server cert]")
fmt.Println(" ClientAuth: RequireAndVerifyClientCert")
fmt.Println(" ClientCAs: [trusted CA pool]")
fmt.Println()
fmt.Println("=== mTLS Client Config ===")
fmt.Println(" Certificates: [client cert]")
fmt.Println(" RootCAs: [trusted CA pool]")
fmt.Println(" ServerName: must match server cert CN/SAN")
newMTLSServer(":50051")
newMTLSClient("127.0.0.1:50051")
}七、Token 认证:基于 metadata
TLS 解决了传输加密和身份验证,但应用层还需要认证「谁在调用」。gRPC 通过 metadata 传递认证信息。
go
package main
import (
"context"
"fmt"
"log"
"sync"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
)
// === Token 管理器 ===
type TokenManager struct {
mu sync.RWMutex
tokens map[string]tokenInfo // token -> user info
}
type tokenInfo struct {
UserID string
Role string
ExpiresAt time.Time
}
func NewTokenManager() *TokenManager {
return &TokenManager{tokens: make(map[string]tokenInfo)}
}
// 生成 token
func (tm *TokenManager) Issue(userID, role string) string {
token := fmt.Sprintf("token-%s-%d", userID, time.Now().UnixNano())
tm.mu.Lock()
defer tm.mu.Unlock()
tm.tokens[token] = tokenInfo{
UserID: userID,
Role: role,
ExpiresAt: time.Now().Add(time.Hour),
}
return token
}
// 验证 token
func (tm *TokenManager) Validate(token string) (*tokenInfo, error) {
tm.mu.RLock()
defer tm.mu.RUnlock()
info, ok := tm.tokens[token]
if !ok {
return nil, status.Error(codes.Unauthenticated, "invalid token")
}
if time.Now().After(info.ExpiresAt) {
return nil, status.Error(codes.Unauthenticated, "token expired")
}
return &info, nil
}
// === 客户端:注入 token 到 metadata ===
func withToken(ctx context.Context, token string) context.Context {
return metadata.AppendToOutgoingContext(ctx, "authorization", "Bearer "+token)
}
// 客户端 token 自动注入拦截器
func tokenInjectInterceptor(token string) grpc.UnaryClientInterceptor {
return func(ctx context.Context, method string, req, reply interface{},
cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption,
) error {
ctx = withToken(ctx, token)
return invoker(ctx, method, req, reply, cc, opts...)
}
}
func main() {
tm := NewTokenManager()
// 1. 用户登录获取 token
token := tm.Issue("user-42", "admin")
fmt.Printf("issued token: %s\n", token)
// 2. 模拟客户端调用时注入 token
ctx := withToken(context.Background(), token)
md, _ := metadata.FromOutgoingContext(ctx)
fmt.Printf("outgoing metadata: %v\n", md)
// 3. 服务端验证 token
// 模拟服务端从 incoming metadata 提取 token
incomingMD := metadata.NewIncomingContext(context.Background(),
metadata.Pairs("authorization", "Bearer "+token))
extractedToken := ""
if vals := incomingMD.Get("authorization"); len(vals) > 0 {
extractedToken = vals[0]
}
fmt.Printf("extracted: %s\n", extractedToken)
info, err := tm.Validate(extractedToken)
if err != nil {
log.Fatal(err)
}
fmt.Printf("validated: userID=%s role=%s\n", info.UserID, info.Role)
// 4. 演示过期 token
expiredToken := tm.Issue("user-expired", "user")
// 手动过期
tm.mu.Lock()
t := tm.tokens[expiredToken]
t.ExpiresAt = time.Now().Add(-time.Hour)
tm.tokens[expiredToken] = t
tm.mu.Unlock()
_, err = tm.Validate(expiredToken)
fmt.Printf("expired token validation: %v\n", err)
}八、OAuth2 与 JWT
1. JWT 认证
go
package main
import (
"context"
"fmt"
"log"
"strings"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
)
// 模拟 JWT(实际项目用 github.com/golang-jwt/jwt/v5)
// JWT 结构: header.payload.signature,均 base64url 编码
type JWTClaims struct {
UserID string `json:"user_id"`
Role string `json:"role"`
Exp int64 `json:"exp"`
Iat int64 `json:"iat"`
}
// 模拟 JWT 签发(简化版,实际用 HMAC/RSA 签名)
func signJWT(claims JWTClaims, secret string) string {
// 实际实现:
// header := base64url(headerJSON)
// payload := base64url(claimsJSON)
// signature := HMAC-SHA256(header + "." + payload, secret)
// return header + "." + payload + "." + signature
return fmt.Sprintf("jwt.%s.%s.%s",
claims.UserID, claims.Role, "fake-signature")
}
// 模拟 JWT 验证
func verifyJWT(token string, secret string) (*JWTClaims, error) {
if token == "" {
return nil, status.Error(codes.Unauthenticated, "empty token")
}
parts := strings.Split(token, ".")
if len(parts) != 4 || parts[0] != "jwt" {
return nil, status.Error(codes.Unauthenticated, "malformed token")
}
// 实际验证签名...
claims := &JWTClaims{
UserID: parts[1],
Role: parts[2],
Exp: time.Now().Add(time.Hour).Unix(),
}
if claims.Exp < time.Now().Unix() {
return nil, status.Error(codes.Unauthenticated, "token expired")
}
return claims, nil
}
// JWT 认证拦截器
func jwtAuthInterceptor(secret string, whitelist map[string]bool) grpc.UnaryServerInterceptor {
return func(ctx context.Context, req interface{},
info *grpc.UnaryServerInfo, handler grpc.UnaryHandler,
) (interface{}, error) {
// 白名单跳过
if whitelist[info.FullMethod] {
return handler(ctx, req)
}
// 从 metadata 提取 token
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return nil, status.Error(codes.Unauthenticated, "no metadata")
}
values := md.Get("authorization")
if len(values) == 0 {
return nil, status.Error(codes.Unauthenticated, "no authorization")
}
token := strings.TrimPrefix(values[0], "Bearer ")
claims, err := verifyJWT(token, secret)
if err != nil {
return nil, status.Errorf(codes.Unauthenticated, "invalid token: %v", err)
}
// 将 claims 存入 context
ctx = context.WithValue(ctx, jwtClaimsKey{}, claims)
return handler(ctx, req)
}
}
type jwtClaimsKey struct{}
func ClaimsFromContext(ctx context.Context) (*JWTClaims, bool) {
c, ok := ctx.Value(jwtClaimsKey{}).(*JWTClaims)
return c, ok
}
func main() {
secret := "my-secret-key"
// 签发 JWT
token := signJWT(JWTClaims{
UserID: "user-42",
Role: "admin",
Exp: time.Now().Add(time.Hour).Unix(),
Iat: time.Now().Unix(),
}, secret)
fmt.Printf("JWT token: %s\n", token)
// 验证 JWT
claims, err := verifyJWT(token, secret)
if err != nil {
log.Fatal(err)
}
fmt.Printf("claims: %+v\n", claims)
// 模拟拦截器调用
whitelist := map[string]bool{
"/auth.AuthService/Login": true,
}
interceptor := jwtAuthInterceptor(secret, whitelist)
// 测试白名单方法
_, err = interceptor(
context.Background(), "req",
&grpc.UnaryServerInfo{FullMethod: "/auth.AuthService/Login"},
func(ctx context.Context, req interface{}) (interface{}, error) {
return "login ok", nil
},
)
fmt.Printf("whitelist call: err=%v\n", err)
// 测试需要认证的方法(带 token)
ctx := metadata.NewIncomingContext(context.Background(),
metadata.Pairs("authorization", "Bearer "+token))
_, err = interceptor(ctx, "req",
&grpc.UnaryServerInfo{FullMethod: "/shop.OrderService/GetOrder"},
func(ctx context.Context, req interface{}) (interface{}, error) {
c, _ := ClaimsFromContext(ctx)
return fmt.Sprintf("order for %s", c.UserID), nil
},
)
fmt.Printf("authenticated call: err=%v\n", err)
// 测试无 token 的调用
_, err = interceptor(context.Background(), "req",
&grpc.UnaryServerInfo{FullMethod: "/shop.OrderService/GetOrder"},
func(ctx context.Context, req interface{}) (interface{}, error) { return nil, nil },
)
fmt.Printf("unauthenticated call: err=%v\n", err)
}2. OAuth2 Token
go
package main
import (
"context"
"fmt"
"time"
"golang.org/x/oauth2"
"golang.org/x/oauth2/clientcredentials"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/credentials/oauth"
)
// OAuth2 客户端凭证模式(适合服务间调用)
func newOAuth2Client(addr string) {
// 配置 OAuth2 客户端凭证
config := &clientcredentials.Config{
ClientID: "my-client-id",
ClientSecret: "my-client-secret",
TokenURL: "https://auth.example.com/oauth2/token",
Scopes: []string{"order.read", "order.write"},
}
// 方式一:使用 gRPC 的 oauth 包
// creds := oauth.NewTokenSource(config.TokenSource(context.Background()))
// conn, _ := grpc.NewClient(addr,
// grpc.WithTransportCredentials(credentials.NewTLS(nil)), // TLS 传输层
// grpc.WithPerRPCCredentials(creds), // OAuth2 应用层
// )
// 方式二:使用 PerRPCCredentials 接口自定义
tokenSource := config.TokenSource(context.Background())
// 模拟获取 token
_ = tokenSource
fmt.Println("OAuth2 client configured:")
fmt.Println(" ClientID:", config.ClientID)
fmt.Println(" TokenURL:", config.TokenURL)
fmt.Println(" Scopes:", config.Scopes)
_ = grpc.NewClient
_ = credentials.NewTLS
_ = oauth.NewTokenSource
_ = time.Second
}九、拦截器实现认证与授权
go
package main
import (
"context"
"fmt"
"log"
"strings"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
)
// === 认证拦截器:验证身份 ===
type AuthContext struct {
UserID string
Role string
Scopes []string
}
type authCtxKey struct{}
func AuthInterceptor(secret string) grpc.UnaryServerInterceptor {
whitelist := map[string]bool{
"/auth.AuthService/Login": true,
"/auth.AuthService/Register": true,
}
return func(ctx context.Context, req interface{},
info *grpc.UnaryServerInfo, handler grpc.UnaryHandler,
) (interface{}, error) {
if whitelist[info.FullMethod] {
return handler(ctx, req)
}
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return nil, status.Error(codes.Unauthenticated, "no metadata")
}
tokens := md.Get("authorization")
if len(tokens) == 0 {
return nil, status.Error(codes.Unauthenticated, "missing authorization")
}
token := strings.TrimPrefix(tokens[0], "Bearer ")
authCtx, err := authenticate(token, secret)
if err != nil {
return nil, status.Errorf(codes.Unauthenticated, "auth failed: %v", err)
}
ctx = context.WithValue(ctx, authCtxKey{}, authCtx)
return handler(ctx, req)
}
}
func authenticate(token string, secret string) (*AuthContext, error) {
// 简化:实际验证 JWT 签名
if token == "" {
return nil, fmt.Errorf("empty token")
}
// 模拟解析
if strings.Contains(token, "invalid") {
return nil, fmt.Errorf("invalid signature")
}
return &AuthContext{
UserID: "user-42",
Role: "user",
Scopes: []string{"order:read", "order:write"},
}, nil
}
func GetAuthContext(ctx context.Context) (*AuthContext, bool) {
ac, ok := ctx.Value(authCtxKey{}).(*AuthContext)
return ac, ok
}
// === 授权拦截器:检查权限 ===
// 权限矩阵:方法 -> 允许的角色
var methodRoles = map[string][]string{
"/shop.OrderService/GetOrder": {"user", "admin"},
"/shop.OrderService/CreateOrder": {"user", "admin"},
"/shop.OrderService/DeleteOrder": {"admin"}, // 仅管理员
"/shop.OrderService/ListAllOrders": {"admin"}, // 仅管理员
}
func AuthzInterceptor(ctx context.Context, req interface{},
info *grpc.UnaryServerInfo, handler grpc.UnaryHandler,
) (interface{}, error) {
allowedRoles, requiresAuthz := methodRoles[info.FullMethod]
if !requiresAuthz {
// 不在权限矩阵中的方法,默认允许
return handler(ctx, req)
}
authCtx, ok := GetAuthContext(ctx)
if !ok {
return nil, status.Error(codes.Unauthenticated, "no auth context")
}
// 检查角色
hasRole := false
for _, role := range allowedRoles {
if authCtx.Role == role {
hasRole = true
break
}
}
if !hasRole {
return nil, status.Errorf(codes.PermissionDenied,
"role %q is not allowed for %s (requires: %v)",
authCtx.Role, info.FullMethod, allowedRoles)
}
return handler(ctx, req)
}
// === Scope 级别授权 ===
func requireScope(requiredScope string) grpc.UnaryServerInterceptor {
return func(ctx context.Context, req interface{},
info *grpc.UnaryServerInfo, handler grpc.UnaryHandler,
) (interface{}, error) {
authCtx, ok := GetAuthContext(ctx)
if !ok {
return nil, status.Error(codes.Unauthenticated, "no auth context")
}
hasScope := false
for _, s := range authCtx.Scopes {
if s == requiredScope {
hasScope = true
break
}
}
if !hasScope {
return nil, status.Errorf(codes.PermissionDenied,
"missing required scope: %s", requiredScope)
}
return handler(ctx, req)
}
}
func main() {
secret := "jwt-secret"
// 组装拦截器链
server := grpc.NewServer(grpc.ChainUnaryInterceptor(
AuthInterceptor(secret), // 1. 认证
AuthzInterceptor, // 2. 授权(角色检查)
requireScope("order:read"), // 3. 授权(scope 检查)
))
fmt.Printf("server with auth + authz: %T\n", server)
// 测试:模拟一个 admin 用户访问 DeleteOrder
adminToken := "admin-token"
ctx := metadata.NewIncomingContext(context.Background(),
metadata.Pairs("authorization", "Bearer "+adminToken))
// 修改 authenticate 让 admin-token 返回 admin 角色
// (简化演示,直接手动构造)
authCtx := &AuthContext{UserID: "admin-1", Role: "admin", Scopes: []string{"order:read", "order:write"}}
ctx = context.WithValue(ctx, authCtxKey{}, authCtx)
// 测试角色检查
allowedRoles := methodRoles["/shop.OrderService/DeleteOrder"] // ["admin"]
fmt.Printf("DeleteOrder allowed roles: %v, user role: %s\n", allowedRoles, authCtx.Role)
// 测试普通用户访问管理员方法
userAuthCtx := &AuthContext{UserID: "user-1", Role: "user", Scopes: []string{"order:read"}}
userHasRole := false
for _, r := range allowedRoles {
if userAuthCtx.Role == r {
userHasRole = true
}
}
fmt.Printf("user accessing admin method: allowed=%v\n", userHasRole)
// 测试 scope 检查
requiredScope := "order:write"
userHasScope := false
for _, s := range userAuthCtx.Scopes {
if s == requiredScope {
userHasScope = true
}
}
fmt.Printf("user has scope %q: %v (scopes: %v)\n", requiredScope, userHasScope, userAuthCtx.Scopes)
_ = time.Second
_ = log.Printf
}十、完整示例:带认证和 TLS 的安全 gRPC 服务
go
package main
import (
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"fmt"
"log"
"math/big"
"net"
"os"
"os/signal"
"strings"
"sync"
"syscall"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/credentials"
"google.golang.org/grpc/metadata"
"google.golang.org/grpc/status"
)
// === 1. 内存证书生成(用于演示,无需文件) ===
type certPair struct {
certPEM []byte
keyPEM []byte
cert *x509.Certificate
key interface{}
}
func generateCertInMemory(commonName string, isCA bool, isServer bool) *certPair {
priv, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
template := &x509.Certificate{
SerialNumber: big.NewInt(time.Now().UnixNano()),
Subject: pkix.Name{CommonName: commonName, Organization: []string{"Demo"}},
NotBefore: time.Now(),
NotAfter: time.Now().Add(24 * time.Hour),
KeyUsage: x509.KeyUsageDigitalSignature,
}
if isCA {
template.KeyUsage |= x509.KeyUsageCertSign
template.BasicConstraintsValid = true
template.IsCA = true
}
if isServer {
template.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}
template.DNSNames = []string{"localhost"}
} else if !isCA {
template.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}
}
var parent *x509.Certificate
var signer interface{} = priv
// 自签名
parent = template
certDER, _ := x509.CreateCertificate(rand.Reader, template, parent, &priv.PublicKey, signer)
cert, _ := x509.ParseCertificate(certDER)
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})
keyBytes, _ := x509.MarshalPKCS8PrivateKey(priv)
keyPEM := pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: keyBytes})
return &certPair{certPEM: certPEM, keyPEM: keyPEM, cert: cert, key: priv}
}
// === 2. 用户存储 ===
type User struct {
ID string `json:"id"`
Name string `json:"name"`
Role string `json:"role"`
}
type UserStore struct {
mu sync.RWMutex
users map[string]*User
tokens map[string]string // token -> userID
}
func NewUserStore() *UserStore {
return &UserStore{
users: map[string]*User{
"u1": {ID: "u1", Name: "alice", Role: "admin"},
"u2": {ID: "u2", Name: "bob", Role: "user"},
},
tokens: make(map[string]string),
}
}
func (s *UserStore) Login(userID string) (string, error) {
s.mu.Lock()
defer s.mu.Unlock()
if _, ok := s.users[userID]; !ok {
return "", fmt.Errorf("user not found")
}
token := fmt.Sprintf("tok-%s-%d", userID, time.Now().UnixNano())
s.tokens[token] = userID
return token, nil
}
func (s *UserStore) ValidateToken(token string) (*User, error) {
s.mu.RLock()
defer s.mu.RUnlock()
userID, ok := s.tokens[token]
if !ok {
return nil, fmt.Errorf("invalid token")
}
user, ok := s.users[userID]
if !ok {
return nil, fmt.Errorf("user not found")
}
return user, nil
}
func (s *UserStore) Get(userID string) (*User, error) {
s.mu.RLock()
defer s.mu.RUnlock()
user, ok := s.users[userID]
if !ok {
return nil, fmt.Errorf("user %s not found", userID)
}
return user, nil
}
// === 3. 认证拦截器 ===
type authCtxKey struct{}
func AuthInterceptor(store *UserStore) grpc.UnaryServerInterceptor {
whitelist := map[string]bool{
"/auth.AuthService/Login": true,
}
return func(ctx context.Context, req interface{},
info *grpc.UnaryServerInfo, handler grpc.UnaryHandler,
) (interface{}, error) {
if whitelist[info.FullMethod] {
return handler(ctx, req)
}
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return nil, status.Error(codes.Unauthenticated, "no metadata")
}
tokens := md.Get("authorization")
if len(tokens) == 0 {
return nil, status.Error(codes.Unauthenticated, "missing token")
}
token := strings.TrimPrefix(tokens[0], "Bearer ")
user, err := store.ValidateToken(token)
if err != nil {
return nil, status.Error(codes.Unauthenticated, err.Error())
}
ctx = context.WithValue(ctx, authCtxKey{}, user)
return handler(ctx, req)
}
}
// === 4. 授权拦截器 ===
func AuthzInterceptor(ctx context.Context, req interface{},
info *grpc.UnaryServerInfo, handler grpc.UnaryHandler,
) (interface{}, error) {
// 仅管理员可访问的方法
adminOnly := map[string]bool{
"/user.UserService/DeleteUser": true,
}
if !adminOnly[info.FullMethod] {
return handler(ctx, req)
}
user, ok := ctx.Value(authCtxKey{}).(*User)
if !ok {
return nil, status.Error(codes.Unauthenticated, "no auth context")
}
if user.Role != "admin" {
return nil, status.Errorf(codes.PermissionDenied,
"admin role required, you are %s", user.Role)
}
return handler(ctx, req)
}
// === 5. 构建 TLS 配置 ===
func buildServerTLSConfig(cp *certPair) *tls.Config {
cert, _ := tls.X509KeyPair(cp.certPEM, cp.keyPEM)
return &tls.Config{
Certificates: []tls.Certificate{cert},
MinVersion: tls.VersionTLS12,
}
}
func buildClientTLSConfig(cp *certPair) *tls.Config {
certPool := x509.NewCertPool()
certPool.AddCert(cp.cert)
return &tls.Config{
RootCAs: certPool,
ServerName: "localhost",
MinVersion: tls.VersionTLS12,
}
}
// === 6. 服务端 ===
func runSecureServer(addr string, store *UserStore, serverCert *certPair) (*grpc.Server, net.Listener) {
lis, err := net.Listen("tcp", addr)
if err != nil {
log.Fatalf("listen: %v", err)
}
creds := credentials.NewTLS(buildServerTLSConfig(serverCert))
server := grpc.NewServer(
grpc.Creds(creds),
grpc.ChainUnaryInterceptor(
AuthInterceptor(store),
AuthzInterceptor,
),
)
go func() {
log.Printf("secure gRPC server on %s (TLS + Auth)", lis.Addr())
if err := server.Serve(lis); err != nil {
log.Printf("serve: %v", err)
}
}()
return server, lis
}
// === 7. 客户端 ===
func runSecureClient(addr string, caCert *certPair, store *UserStore) {
creds := credentials.NewTLS(buildClientTLSConfig(caCert))
conn, err := grpc.NewClient(addr, grpc.WithTransportCredentials(creds))
if err != nil {
log.Printf("dial: %v", err)
return
}
defer conn.Close()
// 模拟客户端调用流程
// 1. 登录获取 token
token, err := store.Login("u1")
if err != nil {
log.Printf("login: %v", err)
return
}
fmt.Printf("client login: token=%s\n", token)
// 2. 带 token 调用(通过 metadata 注入)
ctx := metadata.AppendToOutgoingContext(
context.Background(), "authorization", "Bearer "+token)
// 模拟一次 RPC 调用(实际用 protoc 生成的 client)
// resp, err := userv1.NewUserServiceClient(conn).GetUser(ctx, &userv1.GetUserRequest{Id: "u1"})
user, err := store.Get("u1")
if err != nil {
log.Printf("get user: %v", err)
return
}
fmt.Printf("client got user: %+v\n", user)
_ = ctx
}
// === main ===
func main() {
// 1. 生成证书
fmt.Println("=== Generating certificates ===")
caCert := generateCertInMemory("Demo CA", true, false)
serverCert := generateCertInMemory("localhost", false, true)
fmt.Printf("CA: CN=%s\n", caCert.cert.Subject.CommonName)
fmt.Printf("Server: CN=%s, SAN=%v\n", serverCert.cert.Subject.CommonName, serverCert.cert.DNSNames)
// 2. 初始化存储
store := NewUserStore()
// 3. 启动安全服务端
fmt.Println("\n=== Starting secure server ===")
server, lis := runSecureServer(":50055", store, serverCert)
// 4. 客户端调用
fmt.Println("\n=== Client calling ===")
runSecureClient("127.0.0.1:50055", caCert, store)
// 5. 等待退出
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
fmt.Println("\n=== Shutting down ===")
server.GracefulStop()
_ = lis.Close()
fmt.Println("stopped")
}这个完整示例演示了一个安全 gRPC 服务的全貌:内存生成自签名证书 + TLS 传输加密 + Token 认证拦截器 + 角色授权拦截器。实际生产中只需将内存证书替换为从文件/Secret 加载的正式证书,将内存 token 替换为 JWT/OAuth2 即可。
十一、小结
本篇深入 gRPC 安全与 TLS:
- 传输安全概述:gRPC 安全分两层——传输层 TLS 和应用层认证/授权。
- TLS 基础:私钥、公钥、证书、CA、CSR 的概念,TLS 握手流程。
- 自签名证书:用 Go
crypto/x509包或 OpenSSL 生成 CA + 服务端/客户端证书。 - 服务端 TLS:
credentials.NewServerTLSFromFile或NewTLS(tls.Config)配置证书。 - 客户端 TLS:信任 CA 池 + ServerName 校验,开发环境可跳过验证(绝不用于生产)。
- mTLS 双向认证:服务端
ClientAuth: RequireAndVerifyClientCert,客户端提供Certificates,是内部服务最高安全级别。 - Token 认证:通过 metadata 传递
authorization: Bearer <token>,拦截器提取并验证。 - JWT/OAuth2:JWT 拦截器验证签名和过期时间,OAuth2 用
PerRPCCredentials自动注入 token。 - 认证拦截器:白名单跳过、token 验证、claims 注入 context。
- 授权拦截器:角色检查(RBAC)和 scope 检查,
PermissionDenied状态码拒绝。 - 完整示例:内存证书 + TLS + Token 认证 + 角色授权的端到端安全服务。
gRPC 系列教程到此结束。从 Protobuf 编码原理到服务端/客户端深度实现,从拦截器中间件到错误处理,从 REST 集成到安全 TLS,你已经具备了构建生产级 gRPC 系统的完整知识体系。