gRPC-认证
客户端和服务端之间的调用,可以加入证书,实现调用的安全性
TLS是建立在TCP协议之上的协议,服务于应用层,前身是SSL,实现了将应用层的报文进行加密后再交由TCP进行传输的功能,生产环境可以购买证书或者使用一些平台发放的免费证书
自签证书
-
安装openssl
-
生成私钥文件
openssl genrsa -des3 -out ca.key 2048
genrsa
:这是 OpenSSL 的子命令,用于生成 RSA 私钥。-des3
:表明使用 DES3 对称加密算法对生成的私钥进行加密。在生成私钥时,你会被要求输入一个密码,之后每次使用该私钥时都需要输入此密码。-out ca.key
:指定将生成的私钥保存到名为ca.key
的文件中。2048
:指定生成的 RSA 私钥的位数为 2048 位。位数越大,私钥就越安全,不过生成和使用私钥时的计算量也会相应增加
-
创建证书请求
openssl req -new -key ca.key -out ca.csr
req
:OpenSSL 的子命令,用于处理证书签名请求相关操作。-new
:表明要创建一个新的证书签名请求。-key ca.key
:指定用于签名请求的私钥文件。这里使用之前生成的ca.key
文件,该私钥对应的公钥会被包含在 CSR 中。由于ca.key
是使用 DES3 加密的,执行此命令时会要求输入之前设置的密码。-out ca.csr
:指定将生成的证书签名请求保存到名为ca.csr
的文件中
-
生成证书公钥
openssl x509 -req -days 365 -in ca.csr -signkey ca.key -out ca.crt
x509
:OpenSSL 的子命令,用于处理 X.509 格式的证书。-req
:表明输入的是一个证书签名请求(CSR),需要对其进行签名处理。-days 365
:指定生成的证书的有效期为 365 天。你可以根据需要调整这个数值。-in server.csr
:指定输入的证书签名请求文件,这里使用之前生成的server.csr
文件。-signkey server.key
:指定用于签名的私钥文件,这里使用之前生成的server.key
文件。由于server.key
是使用 DES3 加密的,执行此命令时会要求输入之前设置的密码。-out server.crt
:指定将生成的数字证书保存到名为server.crt
的文件中
- SAN证书
-
老版本的go生成到上面的证书就可以,新版本对安全有了更高要求,使用SNA证书,SAN是 SSL 标准 x509 中定义的一个扩展。使用了 SAN 字段的 SSL 证书,可以扩展此证书支持的域名,使得一个证书可以支持多个不同域名的解析
-
生成SAN证书,需要找到openssl安装目录下的的openssl.cnf文件,复制到项目里,然后修改对应的配置
# openssl.cnf文件 ,找到对应的配置条目修改 copy_extensions = copy # 原先是注释掉的,把注释解开 req_extensions = v3_req # 原先是注释掉的,把注释解开 # 找到 [v3_req] 在下面新加 subjectAltName = @alt_names # 添加 [v3_req] 最后添加[alt_names]标签和字段 [alt_names] DNS.1 = *.xxx.com # 配置要验证的域名,可以配置多个 DNS.2 = xxx
-
修改完cnf文件后生成SNA证书
-
openssl genpkey -algorithm RSA -out server.key # 生成私钥
-
# 生成请求,更改使用openssl.cnf文件路径 # -config 指定使用的 .cnf文件路径,-extensions指定使用的扩展 openssl req -new -nodes -key server.key -out server.csr -days 3650 -config ./openssl.cnf -extensions v3_req
-
# 生成公钥,和自签证书一致 openssl x509 -req -days 365 -in server.csr -signkey server.key -out server.crt
-
# 生成SAN证书 openssl x509 -req -days 365 -in server.csr -out server.pem -CA ca.crt -CAkey ca.key -CAcreateserial -extfile ./openssl.cnf -extensions v3_req
-
生成SNA证书的相关参数
- key: 服务器上的私钥文件,用于对发送给客户端数据的加密,以及对从客户端接收到数据的解密。
- csr: 证书签名请求文件,用于提交给证书颁发机构(CA)对证书签名。
- crt: 由证书颁发机构(CA)签名后的证书,或者是开发者自签名的证书,包含证书持有人的信息,持有人的公钥,以及签署者的签名等信息。
- pem: 是基于Base64编码的证书格式,扩展名包括PEM、CRT和CER
-
单向认证
单向认证也被叫做客户端认证服务器,在这种认证方式里,仅一方(通常是客户端)对另一方(通常是服务器)的身份进行验证,而服务器无需对客户端的身份进行验证
- 客户端发起请求:客户端向服务器发送连接请求。
- 服务器发送证书:服务器接收到请求后,将自己的数字证书发送给客户端。
- 客户端验证证书:客户端接收到证书后,使用权威证书颁发机构(CA)的公钥验证证书的有效性,包括证书是否过期、是否被吊销等。若证书有效,客户端就认为服务器身份是可信的。
- 建立连接:客户端验证通过后,双方建立通信连接
服务端应用证书
package main
import (
"go_rpc_demo/service"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"log"
"net"
)
func main() {
// 读取证书 pem路径和key路径
creds, cred_err := credentials.NewServerTLSFromFile("cert/server.pem", "cert/server.key")
if cred_err != nil {
log.Fatal("证书生成错误", cred_err)
}
// 创建grpc服务实例的时候,应用读取到的证书
rpcServer := grpc.NewServer(grpc.Creds(creds))
// RegisterUserInfoServiceServer是是由 Protobuf 编译器根据.ptoto定义的服务自动生成的函数
// 调用RegisterUserInfoServiceServer ,将第三步的接口的实现注册到rpcServer
service.RegisterUserInfoServiceServer(rpcServer, service.UserService)
// 在tpc协议的8002端口开启监听,返回一个 net.Listener 类型的对象和可能出现的错误
listener, err := net.Listen("tcp", ":8002")
if err != nil {
log.Fatal("启动监听出错", err)
}
// 调用 rpcServer 的 Serve 方法,让 gRPC 服务器开始在 listener 所监听的端口上提供服务。此方法会一直阻塞,直至服务器关闭
err = rpcServer.Serve(listener)
if err != nil {
log.Fatal("启动服务出错")
}
}
客户端应用证书
import (
"context"
"go_rpc_demo/service"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"log"
)
func main() {
// 客户端读取证书,参数:证书路径,要覆盖的服务器域名域名(和openssl.cnf里面的dns.x配置的一样)
creds, creds_err := credentials.NewClientTLSFromFile("cert/server.pem", "*.com")
if creds_err != nil {
log.Fatal("证书错误", creds_err)
}
// 创建一个连接 ,连接到8002端口的grpc服务器
// 第二个参数传入读取到的证书
conn, err := grpc.Dial(":8002", grpc.WithTransportCredentials(creds))
if err != nil {
log.Fatal("连接出错", err)
}
// 执行完关闭连接
defer conn.Close()
// NewUserInfoServiceClient是由 Protobuf 编译器根据服务名字生成的
// 创建客户端,传入对应的连接
client := service.NewUserInfoServiceClient(conn)
// 请求参数
request := &service.UserInfoRequest{UserId: 1}
// 通过客户端调用对应的方法,像调用本地方法一样
infoResponse, err := client.GetUserInfo(context.Background(), request)
if err != nil {
log.Fatal("请求失败", err)
}
// 请求结果
log.Println(infoResponse)
}
双向认证
双向认证又称相互认证,通信双方都需要对对方的身份进行验证,即客户端要验证服务器的身份,同时服务器也要验证客户端的身份。
- 客户端发起请求:客户端向服务器发送连接请求。
- 服务器发送证书:服务器接收到请求后,将自己的数字证书发送给客户端。
- 客户端验证服务器证书:客户端使用 CA 的公钥验证服务器证书的有效性。
- 客户端发送证书:若服务器证书验证通过,客户端将自己的数字证书发送给服务器。
- 服务器验证客户端证书:服务器使用 CA 的公钥验证客户端证书的有效性。
- 建立连接:双方证书都验证通过后,建立安全的通信连接
服务端
import (
"crypto/tls"
"crypto/x509"
"go_rpc_demo/service"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials"
"io/ioutil"
"log"
"net"
)
func main() {
// 从证书相关文件中读取和解析信息,得到证书公钥、秘钥对
cert, err := tls.LoadX509KeyPair("cert/server.pem", "cert/server.key")
if err != nil {
log.Fatal("证书错误", err)
}
// 创建一个新的 空的 certPool
certPool := x509.NewCertPool()
// 读取ca证书
ca, err := ioutil.ReadFile("cert/ca.crt")
if err != nil {
log.Fatal("ca证书错误", err)
}
// 尝试解析传入的PEM证书,如果解析成功会将其添加到pool中
certPool.AppendCertsFromPEM(ca)
// 构建基于 TLS 的 TransportCredentials 选项
creds := credentials.NewTLS(&tls.Config{
// 设置证书链,允许包含一个或多个
Certificates: []tls.Certificate{cert},
// 要求必须校验客户端的证书。可以根据实际情况选用以下参数
ClientAuth: tls.RequireAndVerifyClientCert,
// 设置根证书的集合,校验方式使用 ClientAuth 中设定的模式
ClientCAs: certPool,
})
// 创建grpc服务实例
rpcServer := grpc.NewServer(grpc.Creds(creds))
// RegisterUserInfoServiceServer是是由 Protobuf 编译器根据.ptoto定义的服务自动生成的函数
// 调用RegisterUserInfoServiceServer ,将第三步的接口的实现注册到rpcServer
service.RegisterUserInfoServiceServer(rpcServer, service.UserService)
// 在tpc协议的8002端口开启监听,返回一个 net.Listener 类型的对象和可能出现的错误
listener, err := net.Listen("tcp", ":8002")
if err != nil {
log.Fatal("启动监听出错", err)
}
// 调用 rpcServer 的 Serve 方法,让 gRPC 服务器开始在 listener 所监听的端口上提供服务。此方法会一直阻塞,直至服务器关闭
err = rpcServer.Serve(listener)
if err != nil {
log.Fatal("启动服务出错")
}
}
客户端
双向认证客户端也需要生成对应的公钥和私钥
func main() {
// 从证书文件读取,获得证书公钥、秘钥对
cert, err := tls.LoadX509KeyPair("client/cert/client.pem", "client/cert/client.key")
if err != nil {
log.Fatal("从证书文件读取,获得证书公钥、秘钥对 错误", err)
}
// 创建一个新的、空的 CertPool
certPool := x509.NewCertPool()
ca, err := ioutil.ReadFile("client/cert/ca.crt")
if err != nil {
log.Fatal("加载ca出错", err)
}
// 尝试解析所传入的 PEM 编码的证书。如果解析成功会将其加到 CertPool 中,便于后面的使用
if !certPool.AppendCertsFromPEM(ca) {
log.Fatal("解析ca出错", err)
}
// 构建基于 TLS 的 TransportCredentials 选项
creds := credentials.NewTLS(&tls.Config{
// 设置证书链,允许包含一个或多个
Certificates: []tls.Certificate{cert},
// 要求必须校验客户端的证书。可以根据实际情况选用以下参数
ServerName: "*.com",
RootCAs: certPool,
})
// 创建一个连接 ,连接到8002端口的grpc服务器
// 第二个参数传入读取到的证书
conn, err := grpc.Dial(":8002", grpc.WithTransportCredentials(creds))
if err != nil {
log.Fatal("连接出错", err)
}
// 执行完关闭连接
defer conn.Close()
// NewUserInfoServiceClient是由 Protobuf 编译器根据服务名字生成的
// 创建客户端,传入对应的连接
client := service.NewUserInfoServiceClient(conn)
// 请求参数
request := &service.UserInfoRequest{UserId: 1}
// 通过客户端调用对应的方法,像调用本地方法一样
infoResponse, err := client.GetUserInfo(context.Background(), request)
if err != nil {
log.Fatal("请求失败", err)
}
// 请求结果
log.Println(infoResponse)
}
token 认证
服务端
服务端添加用户名密码的登录验证
func main() {
// 拦截器
var authInterceptor grpc.UnaryServerInterceptor
authInterceptor = func(
ctx context.Context,
req interface{},
info *grpc.UnaryServerInfo,
handler grpc.UnaryHandler,
) (resp interface{}, err error) {
//拦截请求,验证 Token
err = Auth(ctx)
if err != nil {
return
}
// 继续处理请求
return handler(ctx, req)
}
server := grpc.NewServer(grpc.UnaryInterceptor(authInterceptor))
service.RegisterProdServiceServer(server,service.ProductService)
listener, err := net.Listen("tcp", ":8002")
if err != nil {
log.Fatal("服务监听端口失败", err)
}
err = server.Serve(listener)
if err != nil {
log.Fatal("服务、启动失败", err)
}
fmt.Println("启动成功")
}
func Auth(ctx context.Context) error {
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return fmt.Errorf("missing credentials")
}
var user string
var password string
if val, ok := md["user"]; ok {
user = val[0]
}
if val, ok := md["password"]; ok {
password = val[0]
}
if user != "admin" || password != "admin" {
return status.Errorf(codes.Unauthenticated, "token不合法")
}
return nil
}
客户端
type Authentication struct {
User string
Password string
}
// 客户端需要实现 PerRPCCredentials 接口
func (a *Authentication) GetRequestMetadata(context.Context, ...string) (
map[string]string, error,
) {
return map[string]string{"user": a.User, "password": a.Password}, nil
}
// // GetRequestMetadata 方法返回认证需要的必要信息,RequireTransportSecurity 方法表示是否启用安全链接
func (a *Authentication) RequireTransportSecurity() bool {
return false
}
user := &auth.Authentication{
User: "admin",
Password: "admin",
}
conn, err := grpc.Dial(":8002", grpc.WithTransportCredentials(insecure.NewCredentials()),grpc.WithPerRPCCredentials(user))
风月都好看,人间也浪漫.