# Gin 项目实战

### 1、项目设置

#### 使用 Docker-compose 创建 MongoDB 和 Redis 数据库

* docker-compose.yml

```yaml
version: '3'
services:
  mongodb:
    image: mongo
    container_name: mongodb
    restart: always
    env_file:
      - ./app.env

    ports:
      - '6000:27017'
    volumes:
      - mongodb:/data/db

  redis:
    image: redis:alpine
    container_name: redis
    ports:
      - '6379:6379'
    volumes:
      - redisDB:/data
volumes:
  mongodb:
  redisDB:
```

* app.env

```go
PORT=8000
MONGO_INITDB_ROOT_USERNAME=root
MONGO_INITDB_ROOT_PASSWORD=password123
MONGODB_LOCAL_URI=mongodb://root:password123@localhost:6000
REDIS_URL=localhost:6379
```

* 启动

```bash
docker-compose up -d
```

#### 如何将 Golang App 连接到 Redis 和 MongoDB

```go
// 依赖包
go get github.com/spf13/viper
go get github.com/go-redis/redis/v8 
go get go.mongodb.org/mongo-driver/mongo 
go get github.com/gin-gonic/gin
go get golang.org/x/crypto/bcrypt
go get github.com/golang-jwt/jwt
go get github.com/go-redis/redis/v8 
go get github.com/gin-contrib/cors
```

```go
// 创建一个文件夹
mkdir api-golang-mongodb-redis-mysql-gin-project
cd api-golang-mongodb-redis-mysql-gin-project
```

```go
// 初始化一个新的 Golang 项目。
go mod init github.com/example/golang-test
```

* config/default.go

> 定义一个结构，该结构将包含 app.env 文件中的所有变量并提供适当的值类型。

```go
package config

import "github.com/spf13/viper"

type Config struct {
    DBUri    string `mapstructure:"MONGODB_LOCAL_URI"`
    RedisUri string `mapstructure:"REDIS_URL"`
    Port     string `mapstructure:"PORT"`
}

// 👈 Struct Config
func LoadConfig(path string) (config Config, err error) {
    viper.AddConfigPath(path)
    viper.SetConfigType("env")
    viper.SetConfigName("app")

    viper.AutomaticEnv()

    err = viper.ReadInConfig()
    if err != nil {
        return
    }

    err = viper.Unmarshal(&config)
    return
}
```

```go
package main

// 👈 Require the packages
import (
    "context"
    "fmt"
    "log"
    "net/http"

    "github.com/example/golang-test/config"
    "github.com/gin-gonic/gin"
    "github.com/go-redis/redis/v8"
    "go.mongodb.org/mongo-driver/mongo"
    "go.mongodb.org/mongo-driver/mongo/options"
    "go.mongodb.org/mongo-driver/mongo/readpref"
)

// 👈 创建所需的变量，我们稍后会重新分配这些变量
var (
    server      *gin.Engine
    ctx         context.Context
    mongoclient *mongo.Client
    redisclient *redis.Client
)

// 每个 go 文件会先运行 init 函数
func init() {

    // 加载 .env 变量
    config, err := config.LoadConfig(".")
    if err != nil {
        log.Fatal("Could not load environment variables", err)
    }

    // 创建上下文
    ctx = context.TODO()

    // 连接 MongoDB
    mongoconn := options.Client().ApplyURI(config.DBUri)
    mongoclient, err := mongo.Connect(ctx, mongoconn)

    if err != nil {
        panic(err)
    }

    if err := mongoclient.Ping(ctx, readpref.Primary()); err != nil {
        panic(err)
    }

    fmt.Println("MongoDB successfully connected...")

    // 连接 Redis
    redisclient = redis.NewClient(&redis.Options{
        Addr: config.RedisUri,
    })

    if _, err := redisclient.Ping(ctx).Result(); err != nil {
        panic(err)
    }

    err = redisclient.Set(ctx, "test", "Welcome to Golang with Redis and MongoDB", 0).Err()
    if err != nil {
        panic(err)
    }

    fmt.Println("Redis client connected successfully...")

    // 创建Gin引擎实例
    server = gin.Default()
}

func main() {
    config, err := config.LoadConfig(".")

    if err != nil {
        log.Fatal("Could not load config", err)
    }

    // 延迟关闭 mongoclient
    defer mongoclient.Disconnect(ctx)

    value, err := redisclient.Get(ctx, "test").Result()

    if err == redis.Nil {
        fmt.Println("key: test does not exist")
    } else if err != nil {
        panic(err)
    }

    // 定义 API 前缀
    router := server.Group("/api")

    // 测试 redis 连接是否正常
    router.GET("/healthchecker", func(ctx *gin.Context) {
        ctx.JSON(http.StatusOK, gin.H{"status": "success", "message": value})
    })

    log.Fatal(server.Run(":" + config.Port))
}
```

#### 测试 Golang API

```bash
docker-compose up -d && go run main.go
```

```bash
>curl localhost:8000/api/healthchecker
{"message":"Welcome to Golang with Redis and MongoDB","status":"success"}
```

#### 当前项目结构

```bash
wang@1:~$ tree api-golang-mongodb-redis-mysql-gin-project/
api-golang-mongodb-redis-mysql-gin-project/
├── app.env
├── config
│   └── default.go
├── docker-compose.yml
├── go.mod
├── go.sum
└── main.go
```

### 2、JWT 身份验证和授权

使用此 Golang JSON Web Token 身份验证 API，用户将能够执行以下操作：

* 注册一个新帐户
* 使用注册的凭据登录
* 过期时刷新访问令牌
* 仅在经过身份验证后获取配置文件信息

| 资源    | HTTP 方法 | 路由                 | 描述        |
| ----- | ------- | ------------------ | --------- |
| users | GET     | /api/users/me      | 返回登录用户的信息 |
| auth  | POST    | /api/auth/register | 注册一个新用户   |
| auth  | POST    | /api/auth/login    | 登录注册用户    |
| auth  | GET     | /api/auth/refresh  | 刷新过期的访问令牌 |
| auth  | GET     | /api/auth/logout   | 注销用户      |

#### 1、 如何生成公钥和私钥

<http://travistidwell.com/jsencrypt/demo/>

* app.env 添加

```go
ACCESS_TOKEN_PRIVATE_KEY=LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlCUEFJQkFBSkJBTzVIKytVM0xrWC91SlRvRHhWN01CUURXSTdGU0l0VXNjbGFFKzlaUUg5Q2VpOGIxcUVmCnJxR0hSVDVWUis4c3UxVWtCUVpZTER3MnN3RTVWbjg5c0ZVQ0F3RUFBUUpCQUw4ZjRBMUlDSWEvQ2ZmdWR3TGMKNzRCdCtwOXg0TEZaZXMwdHdtV3Vha3hub3NaV0w4eVpSTUJpRmI4a25VL0hwb3piTnNxMmN1ZU9wKzVWdGRXNApiTlVDSVFENm9JdWxqcHdrZTFGY1VPaldnaXRQSjNnbFBma3NHVFBhdFYwYnJJVVI5d0loQVBOanJ1enB4ckhsCkUxRmJxeGtUNFZ5bWhCOU1HazU0Wk1jWnVjSmZOcjBUQWlFQWhML3UxOVZPdlVBWVd6Wjc3Y3JxMTdWSFBTcXoKUlhsZjd2TnJpdEg1ZGdjQ0lRRHR5QmFPdUxuNDlIOFIvZ2ZEZ1V1cjg3YWl5UHZ1YStxeEpXMzQrb0tFNXdJZwpQbG1KYXZsbW9jUG4rTkVRdGhLcTZuZFVYRGpXTTlTbktQQTVlUDZSUEs0PQotLS0tLUVORCBSU0EgUFJJVkFURSBLRVktLS0tLQ==

ACCESS_TOKEN_PUBLIC_KEY=LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUZ3d0RRWUpLb1pJaHZjTkFRRUJCUUFEU3dBd1NBSkJBTzVIKytVM0xrWC91SlRvRHhWN01CUURXSTdGU0l0VQpzY2xhRSs5WlFIOUNlaThiMXFFZnJxR0hSVDVWUis4c3UxVWtCUVpZTER3MnN3RTVWbjg5c0ZVQ0F3RUFBUT09Ci0tLS0tRU5EIFBVQkxJQyBLRVktLS0tLQ==
ACCESS_TOKEN_EXPIRED_IN=15m
ACCESS_TOKEN_MAXAGE=15


REFRESH_TOKEN_PRIVATE_KEY=LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlCT1FJQkFBSkJBSWFJcXZXeldCSndnYjR1SEhFQ01RdHFZMTI5b2F5RzVZMGlGcG51a0J1VHpRZVlQWkE4Cmx4OC9lTUh3Rys1MlJGR3VxMmE2N084d2s3TDR5dnY5dVY4Q0F3RUFBUUpBRUZ6aEJqOUk3LzAxR285N01CZUgKSlk5TUJLUEMzVHdQQVdwcSswL3p3UmE2ZkZtbXQ5NXNrN21qT3czRzNEZ3M5T2RTeWdsbTlVdndNWXh6SXFERAplUUloQVA5UStrMTBQbGxNd2ZJbDZtdjdTMFRYOGJDUlRaZVI1ZFZZb3FTeW40YmpBaUVBaHVUa2JtZ1NobFlZCnRyclNWZjN0QWZJcWNVUjZ3aDdMOXR5MVlvalZVRlVDSUhzOENlVHkwOWxrbkVTV0dvV09ZUEZVemhyc3Q2Z08KU3dKa2F2VFdKdndEQWlBdWhnVU8yeEFBaXZNdEdwUHVtb3hDam8zNjBMNXg4d012bWdGcEFYNW9uUUlnQzEvSwpNWG1heWtsaFRDeWtXRnpHMHBMWVdkNGRGdTI5M1M2ZUxJUlNIS009Ci0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0t

REFRESH_TOKEN_PUBLIC_KEY=LS0tLS1CRUdJTiBQVUJMSUMgS0VZLS0tLS0KTUZ3d0RRWUpLb1pJaHZjTkFRRUJCUUFEU3dBd1NBSkJBSWFJcXZXeldCSndnYjR1SEhFQ01RdHFZMTI5b2F5Rwo1WTBpRnBudWtCdVR6UWVZUFpBOGx4OC9lTUh3Rys1MlJGR3VxMmE2N084d2s3TDR5dnY5dVY4Q0F3RUFBUT09Ci0tLS0tRU5EIFBVQkxJQyBLRVktLS0tLQ==

REFRESH_TOKEN_EXPIRED_IN=60m
REFRESH_TOKEN_MAXAGE=60
```

#### 2、添加如下内容

* config/default.go

```go
type Config struct {
    // 添加以下如下
    AccessTokenPrivateKey  string        `mapstructure:"ACCESS_TOKEN_PRIVATE_KEY"`
    AccessTokenPublicKey   string        `mapstructure:"ACCESS_TOKEN_PUBLIC_KEY"`
    RefreshTokenPrivateKey string        `mapstructure:"REFRESH_TOKEN_PRIVATE_KEY"`
    RefreshTokenPublicKey  string        `mapstructure:"REFRESH_TOKEN_PUBLIC_KEY"`
    AccessTokenExpiresIn   time.Duration `mapstructure:"ACCESS_TOKEN_EXPIRED_IN"`
    RefreshTokenExpiresIn  time.Duration `mapstructure:"REFRESH_TOKEN_EXPIRED_IN"`
    AccessTokenMaxAge      int           `mapstructure:"ACCESS_TOKEN_MAXAGE"`
    RefreshTokenMaxAge     int           `mapstructure:"REFRESH_TOKEN_MAXAGE"`
}
```

#### 3、创建用户模型

```bash
mkdir models
```

* models/user.model.go

```go
package models

import (
    "time"

    "go.mongodb.org/mongo-driver/bson/primitive"
)

// 1、创建一个 SignUpInput 结构来指定注册新用户所需的字段
// MongoDB 将使用该bson标签，因为 MongoDB 将数据存储为 BSON 文档
type SignUpInput struct {
    Name            string    `json:"name" bson:"name" binding:"required"`
    Email           string    `json:"email" bson:"email" binding:"required"`
    Password        string    `json:"password" bson:"password" binding:"required,min=8"`
    PasswordConfirm string    `json:"passwordConfirm" bson:"passwordConfirm,omitempty" binding:"required"`
    Role            string    `json:"role" bson:"role"`
    Verified        bool      `json:"verified" bson:"verified"`
    CreatedAt       time.Time `json:"created_at" bson:"created_at"`
    UpdatedAt       time.Time `json:"updated_at" bson:"updated_at"`
}

// 2、用户登录逻辑，只需要用户提供电子邮件和密码
type SignInInput struct {
    Email    string `json:"email" bson:"email" binding:"required"`
    Password string `json:"password" bson:"password" binding:"required"`
}

// 3、创建一个 DBResponse 结构来定义 MongoDB 将返回的字段
type UserDBResponse struct {
    ID              primitive.ObjectID `json:"id" bson:"_id"`
    Name            string             `json:"name" bson:"name"`
    Email           string             `json:"email" bson:"email"`
    Password        string             `json:"password" bson:"password"`
    PasswordConfirm string             `json:"passwordConfirm,omitempty" bson:"passwordConfirm,omitempty"`
    Role            string             `json:"role" bson:"role"`
    Verified        bool               `json:"verified" bson:"verified"`
    CreatedAt       time.Time          `json:"created_at" bson:"created_at"`
    UpdatedAt       time.Time          `json:"updated_at" bson:"updated_at"`
}

// 4、创建一个结构来指定应该包含在 JSON 响应中的字段和一个过滤掉敏感字段的函数。
type UserResponse struct {
    ID        primitive.ObjectID `json:"id,omitempty" bson:"_id,omitempty"`
    Name      string             `json:"name,omitempty" bson:"name,omitempty"`
    Email     string             `json:"email,omitempty" bson:"email,omitempty"`
    Role      string             `json:"role,omitempty" bson:"role,omitempty"`
    CreatedAt time.Time          `json:"created_at" bson:"created_at"`
    UpdatedAt time.Time          `json:"updated_at" bson:"updated_at"`
}

func FilteredResponse(user *DBResponse) UserResponse {
    return UserResponse{
        ID:        user.ID,
        Email:     user.Email,
        Name:      user.Name,
        Role:      user.Role,
        CreatedAt: user.CreatedAt,
        UpdatedAt: user.UpdatedAt,
    }
}
```

#### 5、创建身份接口和用户接口

> 在典型的 Golang RESTful API 中，有控制器和服务。控制器不能直接访问数据库，而是调用服务来改变或查询数据库。

* 1、身份认证接口

> 让我们定义一个 AuthService 指定 SignUpUser和SignInUser方法的接口

* services/auth.service.go

```go
package services

import "github.com/example/golang-test/models"


type AuthService interface {
    // 注册登录接口， 并返回指定格式 json 数据
    SignUpUser(*models.SignUpInput) (*models.UserDBResponse, error)
    SignInUser(*models.SignInInput) (*models.UserDBResponse, error)
}
```

* 2、用户认证接口

> 定义一个UserService具有 FindUserById 和 FindUserByEmail 方法的接口，方法在后面

* services/user.service.go

```go
package services

import "github.com/example/golang-test/models"

type UserService interface {
    FindUserById(string) (*models.UserDBResponse, error)
    FindUserByEmail(string) (*models.UserDBResponse, error)
}
```

#### 6、加密和解密用户密码

> 数据库存储的密码不可能是明文， 所以加密，这里定义两个函数进行加密和解密

* utils/password.go

```go
package utils

import (
    "fmt"

    "golang.org/x/crypto/bcrypt"
)

// 哈希加密密码
func HashPassword(password string) (string, error) {
    hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
    if err != nil {
        return "", fmt.Errorf("could not hash password %w", err)
    }
    return string(hashedPassword), nil
}

// 哈希解密密码
func VerifyPassword(hashedPassword string, candidatePassword string) error {
    return bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(candidatePassword))
}
```

#### 7、创建与数据库交互的服务

> 定义一个 SignUpUser 将由控制器调用以在数据库中创建新用户的服务。

* **认证接口实现**

> 我在结构中包含了 MongoDB Collection 结构和 context 包的 Context 接口，AuthServiceImpl以帮助我们对 MongoDB 数据库执行基本的 CRUD 操作。

> 然后我使用NewAuthService构造函数来实例化 MongoDB 集合结构和 Context 接口。此外，NewAuthService构造函数实现了AuthService我们上面定义的接口。

略......

#### 身份验证中间件
