使用Go语言创建3个微服务和1个API网关 (2022版)
我们会一起开发3个微服务和1个处理HTTP请求的API网关。HTTP请求会通过gRPC转发给这些微服务。此外还会同时处理JWT验证。
本文由两部分组成,第二部分请点击这里。
我们要创建什么应用呢?我们会一起构建一套小型电商系统,项目使用Go语言进行开发。
[图片上传失败...(image-a2f9dd-1658928453062)]
应用架构本系列文章分成两个部分:
- API网关: 处理HTTP请求
- Auth服务: 提供注册、登录及通过JWT生成Token等功能
- Product服务: 提供添加商品、减库存和查找商品等服务
- Order服务: 该微服务中我们只提供了创建订单功能
每个微服务都是独立的项目。
因本系列文章阅读较为耗时,所以尽可能地让这些微服务保持简洁。我们不会涉及Docker或超时问题。
学习完本部分后,读者可以使用API网关和其中一个微服务新建用户。但要求对Go和gRPC有一定的基础。
下面开干!
创建数据库$ psql postgres
$ CREATE DATABASE auth_svc;
$ CREATE DATABASE order_svc;
$ CREATE DATABASE product_svc;
$ \l
$ \q
\l
[图片上传失败...(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=.
.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
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
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)
}
认证中间件
我们需要拦截掉对商品和订单微服务的未认证请求。也就是说,对于某些路由我们只允许登录用户来访问受保护的微服务。
auth.protovalidate
如果验证令牌正确,就让请求通过。如不正确,则抛出未认证的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)
}
查找单个商品的路由
idproduct.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.protoDecreaseStock
商品微服务客户端
同样我们需要定义与商品微服务通讯的客户端。
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_svcauth_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
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的文章。
后记
阅读英文原文的请注意可能会有如下错误
- 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=.
- grpc with mustEmbedUnimplemented*** method
方法一
type Server struct {
H db.Handler
Jwt utils.JwtWrapper
pb.UnimplementedAuthServiceServer
}
方法二
--go-grpc_out=require_ unimplemented_ servers=false