Tạo Dịch Vụ Xác Thực Cho Ứng Dụng Nhỏ Sử Dụng Go và gRPC-Gateway(V2)

Giới thiệu

Ứng dụng nhỏ có thể sử dụng khả năng đăng nhập được cung cấp bởi WeChat để dễ dàng lấy thông tin nhận dạng người dùng từ WeChat, giúp xây dựng hệ thống người dùng bên trong ứng dụng nhanh chóng.

Mô tả Quy Trình Kinh Doanh

  • Tài liệu hướng dẫn tích hợp của WeChat

Cấu Hình Dự Án Ban Đầu

Môi Trường Phát Triển

Môi trường phát triển cục bộ:

go version

go version go1.14.14 darwin/amd64

protoc --version

libprotoc 3.15.7

protoc-gen-go --version

protoc-gen-go v1.26.0

protoc-gen-go-grpc --version

protoc-gen-go-grpc 1.1.0

protoc-gen-grpc-gateway --version

Kết Cấu Mã Nguồn Ban Đầu

Sử dụng go mod init service để khởi tạo dự án Go, tại đây tôi sử dụng tên module là service.

├── auth // Dịch vụ xác thực
│   ├── api
│   │   ├── gen
│   │   │   ├── v1 // Lưu mã nguồn đã tạo, v1 là phiên bản API đầu tiên
│   │   │   │   ├── auth.proto
│   │   │   │   └── auth.yaml
│   │   ├── auth
│   │   │   └── auth.go // Thực hiện dịch vụ cụ thể
│   ├── wechat 
│   └── main.go // Server gRPC cho xác thực
├── gateway // gRPC-Gateway, proxy ngược đến các server gRPC
│   └── main.go
├── gen.sh // Lệnh tạo mã từ `auth.proto`
└── go.mod

Định Nghĩa Lĩnh Vực (auth.proto)

syntax = "proto3";
package auth.v1;
option go_package="service/auth/api/gen/v1;authpb";

// Khách hàng gửi một code
message LoginRequest {
    string code = 1;
}

// Máy chủ trả về trạng thái đăng nhập tùy chỉnh (token)
message LoginResponse {
    string access_token = 1;
    int32 expires_in = 2; // Theo quy định của OAuth2
}

service AuthService {
    rpc Login (LoginRequest) returns (LoginResponse);
}

Sử Dụng gRPC-Gateway Để Phơi Bày RESTful JSON API

Định Nghĩa auth.yaml

type: google.api.Service
config_version: 3

http:
  rules:
  - selector: auth.v1.AuthService.Login
    post: /v1/auth/login
    body: "*"

Tạo Mã Từ Cấu Hình

Sử Dụng gen.sh Để Tạo Mã Liên Quan Đến gRPC-Gateway

PROTO_PATH=./auth/api
GO_OUT_PATH=./auth/api/gen/v1

protoc -I=$PROTO_PATH --go_out=paths=source_relative:$GO_OUT_PATH auth.proto
protoc -I=$PROTO_PATH --go-grpc_out=paths=source_relative:$GO_OUT_PATH auth.proto
protoc -I=$PROTO_PATH --grpc-gateway_out=paths=source_relative,grpc_api_configuration=$PROTO_PATH/auth.yaml:$GO_OUT_PATH auth.proto

Chạy lệnh:

sh gen.sh

Sau khi thành công, sẽ tạo ra các tệp auth.pb.go, auth_grpc.pb.go, auth.pb.gw.go với cấu trúc như sau:

├── auth
│   ├── api
│   │   ├── gen
│   │   │   ├── v1
│   │   │   │   ├── auth.pb.go // Mã protobuf liên quan đến Go
│   │   │   │   ├── auth_grpc.pb.go // Mã server gRPC liên quan đến Go
│   │   │   │   ├── auth.pb.gw.go // Mã gRPC-Gateway liên quan đến Go
│   │   │   │   ├── auth.proto
│   │   │   │   └── auth.yaml
│   │   ├── auth
│   │   │   └── auth.go
│   ├── wechat 
│   └── main.go
├── gateway
│   └── main.go
├── gen.sh
└── go.mod

Tổ chức lại gói:

go mod tidy

Thực Hiện Ban Đầu Auth gRPC Service Server

Thực Hiện Giao Diện AuthServiceServer

Xem mã auth_grpc.pb.go để tìm định nghĩa AuthServiceServer:

……
type AuthServiceServer interface {
	Login(context.Context, *LoginRequest) (*LoginResponse, error)
	mustEmbedUnimplementedAuthServiceServer()
}
……

Thực hiện trong auth/auth/auth.go:

type Service struct {
	Logger         *zap.Logger
	OpenIDResolver OpenIDResolver
	authpb.UnimplementedAuthServiceServer
}

type OpenIDResolver interface {
	Resolve(code string) (string, error)
}

func (s *Service) Login(c context.Context, req *authpb.LoginRequest) (*authpb.LoginResponse, error) {
	s.Logger.Info("received code",
		zap.String("code", req.Code))
	openID, err := s.OpenIDResolver.Resolve(req.Code)
	if err != nil {
		return nil, status.Errorf(codes.Unavailable,
			"cannot resolve openid: %v", err)
	}
	return &authpb.LoginResponse{
		AccessToken: "token for open id " + openID,
		ExpiresIn:   7200,
	}, nil
}

Thực Hiện Giao Diện OpenIDResolver

Sử dụng thư viện bên thứ ba từ cộng đồng:

go get -u github.com/medivhzhan/weapp/v2

Thực hiện trong auth/wechat/wechat.go:

type Service struct {
	AppID     string
	AppSecret string
}

func (s *Service) Resolve(code string) (string, error) {
	resp, err := weapp.Login(s.AppID, s.AppSecret, code)
	if err != nil {
		return "", fmt.Errorf("weapp.Login: %v", err)
	}
	if err = resp.GetResponseError(); err != nil {
		return "", fmt.Errorf("weapp response error: %v", err)
	}
	return resp.OpenID, nil
}

Cấu Hình gRPC Server Cho Dịch Vụ Auth

Trong auth/main.go:

func main() {
	logger, err := zap.NewDevelopment()
	if err != nil {
		log.Fatalf("cannot create logger: %v", err)
	}
	lis, err := net.Listen("tcp", ":8081")
	if err != nil {
		logger.Fatal("cannot listen", zap.Error(err))
	}
	s := grpc.NewServer()
	authpb.RegisterAuthServiceServer(s, &auth.Service{
		OpenIDResolver: &wechat.Service{
			AppID:     "your-app-id",
			AppSecret: "your-app-secret",
		},
		Logger: logger,
	})
	err = s.Serve(lis)
	if err != nil {
	    logger.Fatal("cannot serve", zap.Error(err))   
	}
}

Thực Hiện Ban Đầu API Gateway

Trong gateway/main.go:

c := context.Background()
c, cancel := context.WithCancel(c)
defer cancel()

mux := runtime.NewServeMux(runtime.WithMarshalerOption(
	runtime.MIMEWildcard,
	&runtime.JSONPb{
		MarshalOptions: protojson.MarshalOptions{
			UseEnumNumbers: true,
			UseProtoNames:  true,
		},
		UnmarshalOptions: protojson.UnmarshalOptions{
			DiscardUnknown: true,
		},
	},
))
err := authpb.RegisterAuthServiceHandlerFromEndpoint(
	c,
	mux,
	"localhost:8081",
	[]grpc.DialOption{grpc.WithInsecure()},
)
if err != nil {
	log.Fatalf("cannot register auth service: %v", err)
}

err = http.ListenAndServe(":8080", mux)
if err != nil {
	log.Fatalf("cannot listen and serve: %v", err)
}

Thử nghiệm

wx.request({
  url: "http://localhost:8080/v1/auth/login",
  method: "POST",
  data: { code: res.code },
  success: console.log,
  fail: console.error,
})

Thẻ: Go gRPC-Gateway grpc protobuf WeChat

Đăng vào ngày 2 tháng 6 lúc 16:09