美文网首页
[Golang]使用gRPC、API网关和权限校验创建Go微服务

[Golang]使用gRPC、API网关和权限校验创建Go微服务

作者: AlanHou | 来源:发表于2022-07-27 21:29 被阅读0次

    使用Go语言创建3个微服务和1个API网关 (2022版)

    [Golang]使用gRPC、API网关和权限校验创建Go微服务 Part 1/2

    我们会一起开发3个微服务和1个处理HTTP请求的API网关。HTTP请求会通过gRPC转发给这些微服务。此外还会同时处理JWT验证。

    本文由两部分组成,第二部分请点击这里

    我们要创建什么应用呢?

    我们会一起构建一套小型电商系统,项目使用Go语言进行开发。

    [图片上传失败...(image-a2f9dd-1658928453062)]

    应用架构

    本系列文章分成两个部分:

    1. API网关: 处理HTTP请求
    2. Auth服务: 提供注册登录及通过JWT生成Token等功能
    3. Product服务: 提供添加商品减库存查找商品等服务
    4. Order服务: 该微服务中我们只提供了创建订单功能

    每个微服务都是独立的项目。

    因本系列文章阅读较为耗时,所以尽可能地让这些微服务保持简洁。我们不会涉及Docker或超时问题。

    学习完本部分后,读者可以使用API网关和其中一个微服务新建用户。但要求对Go和gRPC有一定的基础。

    下面开干!

    创建数据库

    $ psql postgres
    $ CREATE DATABASE auth_svc;
    $ CREATE DATABASE order_svc;
    $ CREATE DATABASE product_svc;
    $ \l
    $ \q
    

    执行\l的结果如下图。可以看到我们创建了3个数据库。

    [图片上传失败...(image-e678b1-1658928453062)]

    对PostgreSQL不熟悉的用户也不必担心,使用MySQL完全没有问题,考虑到国内大部分开发者更习惯使用MySQL,后续代码将使用MySQL,其实使用GORM对开发而言两者的差别并不大。

    $ mysql -uroot -p
    $ CREATE DATABASE auth_svc;
    $ CREATE DATABASE order_svc;
    $ CREATE DATABASE product_svc;
    $ SHOW DATABASES;
    $ exit
    

    创建项目

    首先我们需要创建项目。推荐创建一个存储Go项目的工作目录。入口目录读者可自行选定。

    注:本文中有大量的创建目录和切换目录等命令,Windows的用户可使用Git Bash或直接在IDE中创建对应的目录

    $ mkdir go-grpc-project
    $ cd go-grpc-project
    $ mkdir go-grpc-api-gateway go-grpc-auth-svc go-grpc-order-svc go-grpc-product-svc
    

    先开发API网关。

    $ cd go-grpc-api-gateway
    

    API网关

    从API网关讲起可能会略显无趣,但在完成后就可以将微服务结合API网关直接进行测试了。

    初始化项目

    $ go mod init go-grpc-api-gateway
    

    安装模块

    $ go get github.com/gin-gonic/gin
    $ go get github.com/spf13/viper
    $ go get google.golang.org/grpc
    

    项目结构

    我们需要设置整个项目。我个人偏好在一开始就创建好所有目录和文件。有挺多目录文件,请坐稳了。

    文件夹

    $ mkdir -p cmd pkg/config/envs pkg/auth/pb pkg/auth/routes pkg/order/pb pkg/order/routes pkg/product/pb pkg/product/routes
    

    文件

    $ touch Makefile cmd/main.go pkg/config/envs/dev.env pkg/config/config.go
    $ touch pkg/auth/pb/auth.proto pkg/auth/routes/login.go pkg/auth/routes/register.go pkg/auth/client.go pkg/auth/middleware.go pkg/auth/routes.go
    $ touch pkg/product/pb/product.proto pkg/product/routes/create_product.go pkg/product/routes/find_one.go pkg/product/client.go pkg/product/routes.go
    $ touch pkg/order/pb/order.proto pkg/order/routes/create_order.go pkg/order/client.go pkg/order/routes.go
    

    项目结构如下图所示:

    [图片上传失败...(image-8d2f3f-1658928453062)]

    以上使用的是VS Code,如果希望显示同样的图标,请安装Material Icon Theme插件。下面就开始写代码吧。

    Protobuf文件

    首先我们需要分别为3个微服务添加Protobuf文件。

    认证微服务的Proto

    第一个protobuf文件用于构建认证微服务。可以看到我们会定义三个端点

    • Register
    • Login
    • Validate (JSON Web Token)

    pkg/auth/pb/auth.proto中加入如下代码:

    syntax = "proto3";
    
    package auth;
    
    option go_package = "./pkg/auth/pb";
    
    service AuthService {
      rpc Register(RegisterRequest) returns (RegisterResponse) {}
      rpc Login(LoginRequest) returns (LoginResponse) {}
      rpc Validate(ValidateRequest) returns (ValidateResponse) {}
    }
    
    // Register
    
    message RegisterRequest {
      string email = 1;
      string password = 2;
    }
    
    message RegisterResponse {
      int64 status = 1;
      string error = 2;
    }
    
    // Login
    
    message LoginRequest {
      string email = 1;
      string password = 2;
    }
    
    message LoginResponse {
      int64 status = 1;
      string error = 2;
      string token = 3;
    }
    
    // Validate
    
    message ValidateRequest { string token = 1; }
    
    message ValidateResponse {
      int64 status = 1;
      string error = 2;
      int64 userId = 3;
    }
    

    订单微服务的Proto

    订单微服务仅处理一项任务,创建订单。因此我们需要商品ID (稍后进行获取)、数量和用户ID。

    pkg/order/pb/order.proto中添加如下代码:

    syntax = "proto3";
    
    package order;
    
    option go_package = "./pkg/order/pb";
    
    service OrderService {
      rpc CreateOrder(CreateOrderRequest) returns (CreateOrderResponse) {}
    }
    
    message CreateOrderRequest {
      int64 productId = 1;
      int64 quantity = 2;
      int64 userId = 3;
    }
    
    message CreateOrderResponse {
      int64 status = 1;
      string error = 2;
      int64 id = 3;
    }
    
    

    商品微服务的Proto

    与订单微服务通讯的准备已就绪,下面来对商品微服务做同样的操作。

    Proto文件

    这次会包含三个端点:

    • 创建商品
    • 查找单个商品
    • 扣除商品库存

    pkg/product/pb/product.proto中添加代码如下:

    syntax = "proto3";
    
    package product;
    
    option go_package = "./pkg/product/pb";
    
    service ProductService {
      rpc CreateProduct(CreateProductRequest) returns (CreateProductResponse) {}
      rpc FindOne(FindOneRequest) returns (FindOneResponse) {}
      rpc DecreaseStock(DecreaseStockRequest) returns (DecreaseStockResponse) {}
    }
    
    // CreateProduct
    
    message CreateProductRequest {
      string name = 1;
      string sku = 2;
      int64 stock = 3;
      int64 price = 4;
    }
    
    message CreateProductResponse {
      int64 status = 1;
      string error = 2;
      int64 id = 3;
    }
    
    // FindOne
    
    message FindOneData {
      int64 id = 1;
      string name = 2;
      string sku = 3;
      int64 stock = 4;
      int64 price = 5;
    }
    
    message FindOneRequest { int64 id = 1; }
    
    message FindOneResponse {
      int64 status = 1;
      string error = 2;
      FindOneData data = 3;
    }
    
    // DecreaseStock
    
    message DecreaseStockRequest {
      int64 id = 1;
      int64 orderId = 2;
    }
    
    message DecreaseStockResponse {
      int64 status = 1;
      string error = 2;
    }
    

    Makefile

    下来我们来编写Makefile。这里添加两条命令来执行其它命令。听起来很怪,可以认为是一种快捷方式。

    这样我们不用再敲下冗长的protoc pkg/**…,只要键入make proto即可。

    下面就对Makefile添加代码:

    proto:
        protoc pkg/**/pb/*.proto --go_out=. --go-grpc_out=.
    
    server:
        go run cmd/main.go
    

    现在我们就可以通过刚刚创建的proto文件来生成protobuf文件了。

    $ make proto
    

    控制台中的输出应该也非常简单:

    $ make proto
    protoc pkg/**/pb/*.proto --go_out=. --go-grpc_out=.
    

    生成的protobuf文件会和.proto文件放在一起,如下图:

    [图片上传失败...(image-52d7e0-1658928453062)]

    环境变量

    我们需要定义一些环境变量。

    pkg/config/envs/dev.env中加入如下代码:

    PORT=:3000
    AUTH_SVC_URL=localhost:50051
    PRODUCT_SVC_URL=localhost:50052
    ORDER_SVC_URL=localhost:50053
    

    配置

    在这一文件中,我们将环境文件中的数据拉给到API网关。

    pkg/config/config.go中加入如下代码:

    package config
    
    import "github.com/spf13/viper"
    
    type Config struct {
        Port          string `mapstructure:"PORT"`
        AuthSvcUrl    string `mapstructure:"AUTH_SVC_URL"`
        ProductSvcUrl string `mapstructure:"PRODUCT_SVC_URL"`
        OrderSvcUrl   string `mapstructure:"ORDER_SVC_URL"`
    }
    
    func LoadConfig() (c Config, err error) {
        viper.AddConfigPath("./pkg/config/envs")
        viper.SetConfigName("dev")
        viper.SetConfigType("env")
    
        viper.AutomaticEnv()
    
        err = viper.ReadInConfig()
    
        if err != nil {
            return
        }
    
        err = viper.Unmarshal(&c)
    
        return
    }
    
    

    API网关的主要配置已完成。下面就需要编写三个微服务的客户端代码,服务端代码稍后编写。

    Auth微服务的端点

    现在我们来编写对Auth微服务的实现,该服务现在还不存在。但因为我们已定义了protobuf文件,所以知道各个微服务的请求和响应。

    注册路由

    如果用户希望注册账号,需要向API网关发送一条请求,我们接收到请求再转发给认证微服务。该微服务会返回响应。当然我们还没有编写这个微服务,但已经知道它接收的数据及返回的响应内容。

    因此这里我们创建一个结构体RegisterRequestBody,用于绑定HTTP请求体,然后绑定所要做的gRPC请求的请求体。

    pkg/auth/routes/register.go中添加如下代码:

    **注意! **在大部分Go文件中,需要将项目名称替换成你自己的。此处模块名位于第5行,请自行替换。

    哈哈,其实大可不必担心,因为我在这里使用了本地路径~

    package routes
    
    import (
        "context"
        "go-grpc-api-gateway/pkg/auth/pb"
        "net/http"
    
        "github.com/gin-gonic/gin"
    )
    
    type RegisterRequestBody struct {
        Email    string `json:"email"`
        Password string `json:"password"`
    }
    
    func Register(ctx *gin.Context, c pb.AuthServiceClient) {
        body := RegisterRequestBody{}
    
        if err := ctx.BindJSON(&body); err != nil {
            ctx.AbortWithError(http.StatusBadRequest, err)
            return
        }
    
        res, err := c.Register(context.Background(), &pb.RegisterRequest{
            Email:    body.Email,
            Password: body.Password,
        })
    
        if err != nil {
            ctx.AbortWithError(http.StatusBadGateway, err)
            return
        }
    
        ctx.JSON(int(res.Status), &res)
    }
    
    

    登录路由

    登录路由与注册路由非常类似。首先绑定HTTP请求体,然后发送请求体给认证微服务。

    微服务的响应为JWT令牌, 这个token在后续编写的路由中会使用到。

    pkg/auth/routes/login.go中添加如下代码:

    package routes
    
    import (
        "context"
        "go-grpc-api-gateway/pkg/auth/pb"
        "net/http"
    
        "github.com/gin-gonic/gin"
    )
    
    type LoginRequestBody struct {
        Email    string `json:"email"`
        Password string `json:"password"`
    }
    
    func Login(ctx *gin.Context, c pb.AuthServiceClient) {
        b := LoginRequestBody{}
    
        if err := ctx.BindJSON(&b); err != nil {
            ctx.AbortWithError(http.StatusBadRequest, err)
            return
        }
    
        res, err := c.Login(context.Background(), &pb.LoginRequest{
            Email:    b.Email,
            Password: b.Password,
        })
    
        if err != nil {
            ctx.AbortWithError(http.StatusBadGateway, err)
            return
        }
    
        ctx.JSON(http.StatusCreated, &res)
    }
    
    

    认证微服务客户端

    需要进行拨号来与认证微服务进行通讯。别忘了我们在config.go文件中初始化了环境文件中所存储的微服务URL。下面就来访问这一数据。

    下面编写pkg/auth/client.go

    package auth
    
    import (
        "fmt"
    
        "go-grpc-api-gateway/pkg/auth/pb"
        "go-grpc-api-gateway/pkg/config"
    
        "google.golang.org/grpc"
    )
    
    type ServiceClient struct {
        Client pb.AuthServiceClient
    }
    
    func InitServiceClient(c *config.Config) pb.AuthServiceClient {
        // using WithInsecure() because no SSL running
        cc, err := grpc.Dial(c.AuthSvcUrl, grpc.WithInsecure())
    
        if err != nil {
            fmt.Println("Could not connect:", err)
        }
    
        return pb.NewAuthServiceClient(cc)
    }
    
    

    认证中间件

    我们需要拦截掉对商品和订单微服务的未认证请求。也就是说,对于某些路由我们只允许登录用户来访问受保护的微服务。

    其实很简单,通过HTTP请求头获取到JWT令牌,然后通过认证微服务来验证这个令牌。我们此前已在auth.proto文件中定义了validate端点。

    如果验证令牌正确,就让请求通过。如不正确,则抛出未认证的HTTP错误。

    pkg/auth/middleware.go中编写如下代码:

    package auth
    
    import (
        "context"
        "net/http"
        "strings"
    
        "go-grpc-api-gateway/pkg/auth/pb"
    
        "github.com/gin-gonic/gin"
    )
    
    type AuthMiddlewareConfig struct {
        svc *ServiceClient
    }
    
    func InitAuthMiddleware(svc *ServiceClient) AuthMiddlewareConfig {
        return AuthMiddlewareConfig{svc}
    }
    
    func (c *AuthMiddlewareConfig) AuthRequired(ctx *gin.Context) {
        authorization := ctx.Request.Header.Get("authorization")
    
        if authorization == "" {
            ctx.AbortWithStatus(http.StatusUnauthorized)
            return
        }
    
        token := strings.Split(authorization, "Bearer ")
    
        if len(token) < 2 {
            ctx.AbortWithStatus(http.StatusUnauthorized)
            return
        }
    
        res, err := c.svc.Client.Validate(context.Background(), &pb.ValidateRequest{
            Token: token[1],
        })
    
        if err != nil || res.Status != http.StatusOK {
            ctx.AbortWithStatus(http.StatusUnauthorized)
            return
        }
    
        ctx.Set("userId", res.UserId)
    
        ctx.Next()
    }
    
    

    初始化路由

    要访问刚刚编写的路由,需要先行注册。

    pkg/auth/routes.go中加入如下代码:

    package auth
    
    import (
        "go-grpc-api-gateway/pkg/auth/routes"
        "go-grpc-api-gateway/pkg/config"
    
        "github.com/gin-gonic/gin"
    )
    
    func RegisterRoutes(r *gin.Engine, c *config.Config) *ServiceClient {
        svc := &ServiceClient{
            Client: InitServiceClient(c),
        }
    
        routes := r.Group("/auth")
        routes.POST("/register", svc.Register)
        routes.POST("/login", svc.Login)
    
        return svc
    }
    
    func (svc *ServiceClient) Register(ctx *gin.Context) {
        routes.Register(ctx, svc.Client)
    }
    
    func (svc *ServiceClient) Login(ctx *gin.Context) {
        routes.Login(ctx, svc.Client)
    }
    
    

    订单微服务的端点

    现在需要对订单微服务做同样的操作。

    创建订单路由

    该路由也类似于上面编写的注册和登录路由。获取HTTP请求体,再将数据转发给订单微服务。

    pkg/order/routes/create_order.go中加入如下代码:

    package routes
    
    import (
        "context"
        "net/http"
    
        "go-grpc-api-gateway/pkg/order/pb"
    
        "github.com/gin-gonic/gin"
    )
    
    type CreateOrderRequestBody struct {
        ProductId int64 `json:"productId"`
        Quantity  int64 `json:"quantity"`
    }
    
    func CreateOrder(ctx *gin.Context, c pb.OrderServiceClient) {
        body := CreateOrderRequestBody{}
    
        if err := ctx.BindJSON(&body); err != nil {
            ctx.AbortWithError(http.StatusBadRequest, err)
            return
        }
    
        userId, _ := ctx.Get("userId")
    
        res, err := c.CreateOrder(context.Background(), &pb.CreateOrderRequest{
            ProductId: body.ProductId,
            Quantity:  body.Quantity,
            UserId:    userId.(int64),
        })
    
        if err != nil {
            ctx.AbortWithError(http.StatusBadGateway, err)
            return
        }
    
        ctx.JSON(http.StatusCreated, &res)
    }
    
    

    订单微服务客户端

    订单微服务也需要一个客户端。

    pkg/order/client.go中加入如下代码:

    package order
    
    import (
        "fmt"
    
        "go-grpc-api-gateway/pkg/config"
        "go-grpc-api-gateway/pkg/order/pb"
    
        "google.golang.org/grpc"
    )
    
    type ServiceClient struct {
        Client pb.OrderServiceClient
    }
    
    func InitServiceClient(c *config.Config) pb.OrderServiceClient {
        // using WithInsecure() because no SSL running
        cc, err := grpc.Dial(c.OrderSvcUrl, grpc.WithInsecure())
    
        if err != nil {
            fmt.Println("Could not connect:", err)
        }
    
        return pb.NewOrderServiceClient(cc)
    }
    
    

    初始化路由

    需要先进行注册才能访问刚刚编写的路由。

    pkg/order/routes.go中加入如下代码:

    package order
    
    import (
        "go-grpc-api-gateway/pkg/auth"
        "go-grpc-api-gateway/pkg/config"
        "go-grpc-api-gateway/pkg/order/routes"
    
        "github.com/gin-gonic/gin"
    )
    
    func RegisterRoutes(r *gin.Engine, c *config.Config, authSvc *auth.ServiceClient) {
        a := auth.InitAuthMiddleware(authSvc)
    
        svc := &ServiceClient{
            Client: InitServiceClient(c),
        }
    
        routes := r.Group("/order")
        routes.Use(a.AuthRequired)
        routes.POST("/", svc.CreateOrder)
    }
    
    func (svc *ServiceClient) CreateOrder(ctx *gin.Context) {
        routes.CreateOrder(ctx, svc.Client)
    }
    
    

    商品微服务的端点

    创建商品的路由

    该路由也类似前述的路由。

    pkg/product/routes/create_product.go中添加如下代码:

    package routes
    
    import (
        "context"
        "net/http"
    
        "go-grpc-api-gateway/pkg/product/pb"
    
        "github.com/gin-gonic/gin"
    )
    
    type CreateProductRequestBody struct {
        Name  string `json:"name"`
        Stock int64  `json:"stock"`
        Price int64  `json:"price"`
    }
    
    func CreateProduct(ctx *gin.Context, c pb.ProductServiceClient) {
        body := CreateProductRequestBody{}
    
        if err := ctx.BindJSON(&body); err != nil {
            ctx.AbortWithError(http.StatusBadRequest, err)
            return
        }
    
        res, err := c.CreateProduct(context.Background(), &pb.CreateProductRequest{
            Name:  body.Name,
            Stock: body.Stock,
            Price: body.Price,
        })
    
        if err != nil {
            ctx.AbortWithError(http.StatusBadGateway, err)
            return
        }
    
        ctx.JSON(http.StatusCreated, &res)
    }
    
    

    查找单个商品的路由

    这是我们首次从路由中获取参数。我们很快就会在URL中定义该参数。但这里我们先获取这个参数id,然后将字符串转换为数字,原因是我们在product.proto中定义的是整型。

    pkg/product/routes/find_one.go中添加如下代码:

    package routes
    
    import (
        "context"
        "net/http"
        "strconv"
    
        "go-grpc-api-gateway/pkg/product/pb"
    
        "github.com/gin-gonic/gin"
    )
    
    func FineOne(ctx *gin.Context, c pb.ProductServiceClient) {
        id, _ := strconv.ParseInt(ctx.Param("id"), 10, 32)
    
        res, err := c.FindOne(context.Background(), &pb.FindOneRequest{
            Id: int64(id),
        })
    
        if err != nil {
            ctx.AbortWithError(http.StatusBadGateway, err)
            return
        }
    
        ctx.JSON(http.StatusCreated, &res)
    }
    
    

    我们没有为product.proto中定义的DecreaseStock端点创建路由。这是因为该端点无法在API网关中直接访问。在系列文章第2部分中我们会编写调用订单微服务中该端点的代码。

    商品微服务客户端

    同样我们需要定义与商品微服务通讯的客户端。

    pkg/product/client.go中添加如下代码:

    package product
    
    import (
        "fmt"
    
        "go-grpc-api-gateway/pkg/config"
        "go-grpc-api-gateway/pkg/product/pb"
    
        "google.golang.org/grpc"
    )
    
    type ServiceClient struct {
        Client pb.ProductServiceClient
    }
    
    func InitServiceClient(c *config.Config) pb.ProductServiceClient {
        // using WithInsecure() because no SSL running
        cc, err := grpc.Dial(c.ProductSvcUrl, grpc.WithInsecure())
    
        if err != nil {
            fmt.Println("Could not connect:", err)
        }
    
        return pb.NewProductServiceClient(cc)
    }
    
    

    初始化路由

    同样我们需要注册刚刚创建的路由。

    pkg/product/routes.go中添加如下代码:

    package product
    
    import (
        "go-grpc-api-gateway/pkg/auth"
        "go-grpc-api-gateway/pkg/config"
        "go-grpc-api-gateway/pkg/product/routes"
    
        "github.com/gin-gonic/gin"
    )
    
    func RegisterRoutes(r *gin.Engine, c *config.Config, authSvc *auth.ServiceClient) {
        a := auth.InitAuthMiddleware(authSvc)
    
        svc := &ServiceClient{
            Client: InitServiceClient(c),
        }
    
        routes := r.Group("/product")
        routes.Use(a.AuthRequired)
        routes.POST("/", svc.CreateProduct)
        routes.GET("/:id", svc.FindOne)
    }
    
    func (svc *ServiceClient) FindOne(ctx *gin.Context) {
        routes.FineOne(ctx, svc.Client)
    }
    
    func (svc *ServiceClient) CreateProduct(ctx *gin.Context) {
        routes.CreateProduct(ctx, svc.Client)
    }
    
    

    Main文件

    最后还有一件重要的事,我们需要启动应用。前面已经注册了路由,现在需要在启动应用时调用这些注册的代码。

    cmd/main.go中添加如下代码:

    package main
    
    import (
        "log"
    
        "go-grpc-api-gateway/pkg/auth"
        "go-grpc-api-gateway/pkg/config"
        "go-grpc-api-gateway/pkg/order"
        "go-grpc-api-gateway/pkg/product"
    
        "github.com/gin-gonic/gin"
    )
    
    func main() {
        c, err := config.LoadConfig()
    
        if err != nil {
            log.Fatalln("Failed at config", err)
        }
    
        r := gin.Default()
    
        authSvc := *auth.RegisterRoutes(r, &c)
        product.RegisterRoutes(r, &c, &authSvc)
        order.RegisterRoutes(r, &c, &authSvc)
    
        r.Run(c.Port)
    }
    
    

    API网关至此就大功告成了!

    $ make server
    

    命令地终端中的效果如下:

    [图片上传失败...(image-e544a4-1658928453062)]

    下面我们来开发第一个微服务。

    认证微服务 (go-grpc-auth-svc)

    这是要编写的三个微服务中的第一个。本文已经很长了,所以会尽量保持简洁。像环境变量这样的处理方式非常类似。

    在命令行终端中进入 go-grpc-auth-svc

    初始化项目

    $ go mod init go-grpc-auth-svc
    

    安装模块Installing Modules

    $ go get github.com/spf13/viper
    $ go get google.golang.org/grpc
    $ go get gorm.io/gorm
    $ go get gorm.io/driver/mysql
    $ go get golang.org/x/crypto/bcrypt
    $ go get github.com/golang-jwt/jwt/v4
    

    项目结构

    我们需要先配置项目。认证微服务相较API网关要更为轻量。

    文件夹

    $ mkdir -p cmd pkg/config/envs pkg/db pkg/models pkg/pb pkg/services pkg/utils
    

    文件

    $ touch Makefile cmd/main.go pkg/config/envs/dev.env pkg/config/config.go
    $ touch pkg/pb/auth.proto pkg/db/db.go pkg/models/auth.go pkg/services/auth.go pkg/utils/hash.go pkg/utils/jwt.go
    

    项目结构如下图所示:

    [图片上传失败...(image-42e939-1658928453062)]

    Makefile

    这里我们同样需要Makefile来简化输入的命令。

    Makefile中加入如下代码:

    proto:
        protoc pkg/pb/*.proto --go_out=. --go-grpc_out=.
    
    server:
        go run cmd/main.go
    

    Proto文件

    在微服务端及API网关两端都需要proto文件。

    pkg/pb/auth.proto中添加如下代码:

    syntax = "proto3";
    
    package auth;
    
    option go_package = "./pkg/pb";
    
    service AuthService {
      rpc Register(RegisterRequest) returns (RegisterResponse) {}
      rpc Login(LoginRequest) returns (LoginResponse) {}
      rpc Validate(ValidateRequest) returns (ValidateResponse) {}
    }
    
    // Register
    
    message RegisterRequest {
      string email = 1;
      string password = 2;
    }
    
    message RegisterResponse {
      int64 status = 1;
      string error = 2;
    }
    
    // Login
    
    message LoginRequest {
      string email = 1;
      string password = 2;
    }
    
    message LoginResponse {
      int64 status = 1;
      string error = 2;
      string token = 3;
    }
    
    // Validate
    
    message ValidateRequest { string token = 1; }
    
    message ValidateResponse {
      int64 status = 1;
      string error = 2;
      int64 userId = 3;
    }
    

    生成Protobuf文件

    需要生成protobuf文件。

    $ make proto
    

    环境变量

    这里需要的变量有gRPC服务端端口、数据库的URL和JWT所用到的密钥。

    pkg/config/envs/dev.env中加入如下代码:

    PORT=:50051
    DB_URL=<USER>:<PASSWORD>@tcp(<HOST>:<PORT>)/auth_svc?charset=utf8mb4&parseTime=True&loc=Local
    JWT_SECRET_KEY=r43t18sc
    

    配置

    我们需要为该微服务创建一个config.go文件。

    pkg/config/config.go中加入如下代码:

    package config
    
    import "github.com/spf13/viper"
    
    type Config struct {
        Port         string `mapstructure:"PORT"`
        DBUrl        string `mapstructure:"DB_URL"`
        JWTSecretKey string `mapstructure:"JWT_SECRET_KEY"`
    }
    
    func LoadConfig() (config Config, err error) {
        viper.AddConfigPath("./pkg/config/envs")
        viper.SetConfigName("dev")
        viper.SetConfigType("env")
    
        viper.AutomaticEnv()
    
        err = viper.ReadInConfig()
    
        if err != nil {
            return
        }
    
        err = viper.Unmarshal(&config)
    
        return
    }
    
    

    数据模型

    在这个模型中,我们在auth_svc数据库中创建一张。此前已经在MySQL中创建过数据库auth_svc

    pkg/models/auth.go中添加如下代码:

    package models
    
    type User struct {
        Id       int64  `json:"id" gorm:"primaryKey"`
        Email    string `json:"email"`
        Password string `json:"password"`
    }
    
    

    数据库连接

    下面来连接数据库。

    db.AutoMigrate方法会在启用应用时自动创建数据表。

    pkg/db/db.go中添加如下代码:

    package db
    
    import (
        "log"
    
        "go-grpc-auth-svc/pkg/models"
    
        "gorm.io/driver/mysql"
        "gorm.io/gorm"
    )
    
    type Handler struct {
        DB *gorm.DB
    }
    
    func Init(url string) Handler {
        db, err := gorm.Open(mysql.Open(url), &gorm.Config{})
    
        if err != nil {
            log.Fatalln(err)
        }
    
        db.AutoMigrate(&models.User{})
    
        return Handler{db}
    }
    
    

    Hash帮助函数

    在该文件中,有两个函数,一个用于通过bcrypt对密码加密,另一个用于密码验证。

    pkg/utils/hash.go中加入如下代码:

    package utils
    
    import "golang.org/x/crypto/bcrypt"
    
    func HashPassword(password string) string {
        bytes, _ := bcrypt.GenerateFromPassword([]byte(password), 5)
    
        return string(bytes)
    }
    
    func CheckPasswordHash(password string, hash string) bool {
        err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
    
        return err == nil
    }
    
    

    JWT帮助函数

    这里我们根据dev.env文件中定义的密钥来生成和校验JWT令牌。

    pkg/utils/jwt.go中添加如下代码:

    package utils
    
    import (
        "errors"
        "time"
    
        "go-grpc-auth-svc/pkg/models"
    
        "github.com/golang-jwt/jwt/v4"
    )
    
    type JwtWrapper struct {
        SecretKey       string
        Issuer          string
        ExpirationHours int64
    }
    
    type jwtClaims struct {
        jwt.StandardClaims
        Id    int64
        Email string
    }
    
    func (w *JwtWrapper) GenerateToken(user models.User) (signedToken string, err error) {
        claims := &jwtClaims{
            Id:    user.Id,
            Email: user.Email,
            StandardClaims: jwt.StandardClaims{
                ExpiresAt: time.Now().Local().Add(time.Hour * time.Duration(w.ExpirationHours)).Unix(),
                Issuer:    w.Issuer,
            },
        }
    
        token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    
        signedToken, err = token.SignedString([]byte(w.SecretKey))
    
        if err != nil {
            return "", err
        }
    
        return signedToken, nil
    }
    
    func (w *JwtWrapper) ValidateToken(signedToken string) (claims *jwtClaims, err error) {
        token, err := jwt.ParseWithClaims(
            signedToken,
            &jwtClaims{},
            func(token *jwt.Token) (interface{}, error) {
                return []byte(w.SecretKey), nil
            },
        )
    
        if err != nil {
            return
        }
    
        claims, ok := token.Claims.(*jwtClaims)
    
        if !ok {
            return nil, errors.New("Couldn't parse claims")
        }
    
        if claims.ExpiresAt < time.Now().Local().Unix() {
            return nil, errors.New("JWT is expired")
        }
    
        return claims, nil
    
    }
    
    

    Auth服务

    这里我们编写Auth微服务的业务逻辑。在API网关中创建的认证路由会将请求转发给该文件。

    pkg/services/auth.go中添加如下代码:

    package services
    
    import (
        "context"
        "net/http"
    
        "go-grpc-auth-svc/pkg/db"
        "go-grpc-auth-svc/pkg/models"
        "go-grpc-auth-svc/pkg/pb"
        "go-grpc-auth-svc/pkg/utils"
    )
    
    type Server struct {
        H   db.Handler
        Jwt utils.JwtWrapper
        pb.UnimplementedAuthServiceServer
    }
    
    func (s *Server) Register(ctx context.Context, req *pb.RegisterRequest) (*pb.RegisterResponse, error) {
        var user models.User
    
        if result := s.H.DB.Where(&models.User{Email: req.Email}).First(&user); result.Error == nil {
            return &pb.RegisterResponse{
                Status: http.StatusConflict,
                Error:  "E-Mail already exists",
            }, nil
        }
    
        user.Email = req.Email
        user.Password = utils.HashPassword(req.Password)
    
        s.H.DB.Create(&user)
    
        return &pb.RegisterResponse{
            Status: http.StatusCreated,
        }, nil
    }
    
    func (s *Server) Login(ctx context.Context, req *pb.LoginRequest) (*pb.LoginResponse, error) {
        var user models.User
    
        if result := s.H.DB.Where(&models.User{Email: req.Email}).First(&user); result.Error != nil {
            return &pb.LoginResponse{
                Status: http.StatusNotFound,
                Error:  "User not found",
            }, nil
        }
    
        match := utils.CheckPasswordHash(req.Password, user.Password)
    
        if !match {
            return &pb.LoginResponse{
                Status: http.StatusNotFound,
                Error:  "User not found",
            }, nil
        }
    
        token, _ := s.Jwt.GenerateToken(user)
    
        return &pb.LoginResponse{
            Status: http.StatusOK,
            Token:  token,
        }, nil
    }
    
    func (s *Server) Validate(ctx context.Context, req *pb.ValidateRequest) (*pb.ValidateResponse, error) {
        claims, err := s.Jwt.ValidateToken(req.Token)
    
        if err != nil {
            return &pb.ValidateResponse{
                Status: http.StatusBadRequest,
                Error:  err.Error(),
            }, nil
        }
    
        var user models.User
    
        if result := s.H.DB.Where(&models.User{Email: claims.Email}).First(&user); result.Error != nil {
            return &pb.ValidateResponse{
                Status: http.StatusNotFound,
                Error:  "User not found",
            }, nil
        }
    
        return &pb.ValidateResponse{
            Status: http.StatusOK,
            UserId: user.Id,
        }, nil
    }
    
    

    Main文件

    最后需要启动该微服务。

    我们来编写cmd/main.go

    package main
    
    import (
        "fmt"
        "log"
        "net"
    
        "go-grpc-auth-svc/pkg/config"
        "go-grpc-auth-svc/pkg/db"
        "go-grpc-auth-svc/pkg/pb"
        "go-grpc-auth-svc/pkg/services"
        "go-grpc-auth-svc/pkg/utils"
    
        "google.golang.org/grpc"
    )
    
    func main() {
        c, err := config.LoadConfig()
    
        if err != nil {
            log.Fatalln("Failed at config", err)
        }
    
        h := db.Init(c.DBUrl)
    
        jwt := utils.JwtWrapper{
            SecretKey:       c.JWTSecretKey,
            Issuer:          "go-grpc-auth-svc",
            ExpirationHours: 24 * 365,
        }
    
        lis, err := net.Listen("tcp", c.Port)
    
        if err != nil {
            log.Fatalln("Failed to listing:", err)
        }
    
        fmt.Println("Auth Svc on", c.Port)
    
        s := services.Server{
            H:   h,
            Jwt: jwt,
        }
    
        grpcServer := grpc.NewServer()
    
        pb.RegisterAuthServiceServer(grpcServer, &s)
    
        if err := grpcServer.Serve(lis); err != nil {
            log.Fatalln("Failed to serve:", err)
        }
    }
    
    

    终于告一段落了,第一部分中的API网关和认证微服务就此完成。

    下面运行服务。

    $ make server
    

    终端中的输出如下:

    [图片上传失败...(image-1f070d-1658928453062)]

    测试认证微服务和API网关

    现在我们向API网关发送两个HTTP请求。要确保两个应用都进行了启动。API网关的端口是3000,认证微服务的端口是52001。

    注册新用户

    curl --request POST \
      --url http://localhost:3000/auth/register \
      --header 'Content-Type: application/json' \
      --data '{
     "email": "elon@musk.com",
     "password": "1234567"
    }'
    

    用户登录

    curl --request POST \
      --url http://localhost:3000/auth/login \
      --header 'Content-Type: application/json' \
      --data '{
     "email": "elon@musk.com",
     "password": "1234567"
    }'
    

    这个请求的响应非常重要,因为该请求会返回JWT令牌,在第二部分其它微服务中会使用到。

    响应内容如下:

    {"status":200,"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2OTA0NTg2NDMsImlzcyI6ImdvLWdycGMtYXV0aC1zdmMiLCJJZCI6MSwiRW1haWwiOiJlbG9uQG11c2suY29tIn0.rtmWUnxTR5pFycFbRy1C5S5oVrs2Nkt0sYO4QIsykFg"}
    

    整理自Kevin Vogel的文章。

    后记

    阅读英文原文的请注意可能会有如下错误

    1. protoc-gen-go: plugins are not supported
    --go_out: protoc-gen-go: plugins are not supported; use 'protoc --go-grpc_out=...' to generate gRPC
    

    解决方法是替换掉Makefile中的相应内容

    proto:
        protoc pkg/**/pb/*.proto --go_out=. --go-grpc_out=.
    
    
    1. grpc with mustEmbedUnimplemented*** method
      方法一
    type Server struct {
        H   db.Handler
        Jwt utils.JwtWrapper
        pb.UnimplementedAuthServiceServer
    }
    
    

    方法二

    --go-grpc_out=require_ unimplemented_ servers=false
    

    相关文章

      网友评论

          本文标题:[Golang]使用gRPC、API网关和权限校验创建Go微服务

          本文链接:https://www.haomeiwen.com/subject/ayleurtx.html