本篇次要介绍Go开发minio存储文件服务的过程. 篇幅有点长.

要实现的性能, 如下:

鉴权(jwt、casbin)

正文文档(swagger)

MinioSDK(minio)

集成部署(jenkins, docker)

代码↓:

Github \
前端 https://github.com/guangnaoke…
Go https://github.com/guangnaoke…

Gitee \
前端 https://gitee.com/Xiao_Yi_Zho…
Go https://gitee.com/Xiao_Yi_Zho…

都是些比较简单的性能. 那么… 开整!

装置

GO装置库及插件

go get -u github.com/gin-gonic/gin
go get github.com/casbin/casbin/v2
go get github.com/golang-jwt/jwt
go get github.com/minio/minio-go/v7
go get github.com/swaggo/gin-swagger
go get github.com/swaggo/swag
go get gopkg.in/yaml.v3
go get -u gorm.io/gorm
go get -u gorm.io/driver/sqlite

docker装置Minio

docker run \
-d \
-p 9000:9000 \
-p 9001:9001 \
--name minio \
--restart=always \
-v /www/minio/data:/data \
-e "MINIO_ROOT_USER=YOURNAME" \
-e "MINIO_ROOT_PASSWORD=YOURPASSWORD" \
minio/minio:latest server /data --console-address ":9001"

MINIO_ROOT_USER, MINIO_ROOT_PASSWORD 账号密码改成本人的

浏览器输出地址: http://127.0.0.1(替换地址):9001 看是否能关上minio管制端

账号密码就是方才设置的. 当初能够建个bucket试试传输文件之类的.

查看SDK列表页

SDK列表 https://docs.min.io/docs/gola…
例子 https://github.com/minio/mini…

须要实现的接口不多, 以下:

ListBucketsListObjectsPutObjectRemoveObjectPresignedGetObject

删除桶之类的还是管理员去操作, 免得误删. 前端只须要上传和查看性能.

docker装置Mysql

docker run \
-p 3306:3306 \
--name mysql \
--privileged=true \
--restart=always \
-v /usr/mysql/local/conf:/etc/mysql/conf.d \
-v /usr/mysql/local/logs:/logs \
-v /usr/mysql/local/data:/var/lib/mysql \
-v /usr/mysql/local/mysql-files:/var/lib/mysql-files \
-e MYSQL_ROOT_PASSWORD=yourpassword \
-e TZ=Asia/Shanghai \
-d docker.io/mysql:latest

–privileged=true 【容器内的root领有真正的权限, 否则只是普通用户权限】\
-v 【挂载目录, 配置文件, 日志】\
MYSQL_ROOT_PASSWORD 改成本人的明码

查看容器看是否运行胜利

通过Navicat之类的工具看是否能失常连贯数据库.

初始化配置信息

根目录创立conf文件夹.

创立conf.yaml, 把配置信息写入conf.yaml文件.

# Mysql服务配置
mysql:
  driverName: mysql
  host: 127.0.0.1
  port: 3306
  database: you_database
  username: admin
  password: admin
  charset: utf8mb4
  parseTime: True
  loc: Local

# MinIO文件存储服务器配置
minio:
  endpoint: 127.0.0.1:9000
  access: you_access
  accessKey: admin
  secretKey: admin

初始化全局单例

创立config.go, 这些都是程序初始化时要用到的模型, mysql的配置信息、minio账号配置等.

package conf

type MysqlConf struct {
    DriverName string `yaml:"driverName" json:"driver_name"`
    Username   string `yaml:"username" json:"username"`
    Password   string `yaml:"password" json:"password"`
    Host       string `yaml:"host" json:"host"`
    Port       string `yaml:"port" json:"port"`
    Database   string `yaml:"database" json:"database"`
    Charset    string `yaml:"charset" json:"charset"`
    ParseTime  string `yaml:"parseTime" json:"parse_time"`
    Loc        string `yaml:"loc" json:"loc"`
}

type MinioConf struct {
    Endpoint  string `yaml:"endpoint" json:"endpoint"`
    Access    string `yaml:"access" json:"access"`
    AccessKey string `yaml:"accessKey" json:"accessKey"`
    SecretKey string `yaml:"secretKey" json:"secretKey"`
}

type ServerConf struct {
    MysqlInfo MysqlConf `yaml:"mysql" json:"mysql"`
    MinioInfo MinioConf `yaml:"minio" json:"minio"`
}

创立global文件夹.

创立singleton.go, 设置全局单例.

package global

import (
    "minio_server/conf"

    "github.com/minio/minio-go/v7"
    "gorm.io/gorm"
)

var (
    Settings    conf.ServerConf
    DB          *gorm.DB
    MinioClient *minio.Client
)

创立一个initialize文件夹.

创立config.go, 将之前的yaml配置信息设置到全局serverConfig, 上面会用到.

package initialize

import (
    "io/ioutil"
    "log"
    "minio_server/conf"
    "minio_server/global"
    "os"

    "gopkg.in/yaml.v3"
)

func InitConfig() error {
    workDor, _ := os.Getwd()
    // 读取yaml配置文件
    yamlFile, err := ioutil.ReadFile(workDor + "/conf/conf.yaml")
    if err != nil {
        log.Printf("yamlFile.Get err %v", err)
        return err
    }
    
    // 配置信息模型
    serverConfig := conf.ServerConf{}
    
    // 将yaml文件对应的配置信息写入serverConfig
    err = yaml.Unmarshal(yamlFile, &serverConfig)
    if err != nil {
        log.Fatalf("Unmarshal: %v", err)
        return err
    }
    
    // 设置全局Settings
    global.Settings = serverConfig

    return nil
}

创立mysql.go

package initialize

import (
    "database/sql"
    "fmt"
    "log"
    "minio_server/global"
    "time"

    "gorm.io/driver/mysql"
    "gorm.io/gorm"
    "gorm.io/gorm/schema"
)

func InitMysqlDB() error {
    mysqlInfo := global.Settings.MysqlInfo

    args := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=%s&parseTime=%s&loc=%s",
        mysqlInfo.Username,
        mysqlInfo.Password,
        mysqlInfo.Host,
        mysqlInfo.Port,
        mysqlInfo.Database,
        mysqlInfo.Charset,
        mysqlInfo.ParseTime,
        mysqlInfo.Loc,
    )

    sqlDB, err := sql.Open(mysqlInfo.DriverName, args)
    if err != nil {
        log.Fatalln(err)

        return err
    }

    // 闲暇连接池中连贯的最大数量
    sqlDB.SetMaxIdleConns(10)
    // 关上数据库连贯的最大数量, 依据需要看着调
    sqlDB.SetMaxOpenConns(100)
    // 连贯可复用的最大工夫。
    sqlDB.SetConnMaxLifetime(time.Hour)

    // 注册单例
    gormDB, err := gorm.Open(mysql.New(mysql.Config{
        Conn: sqlDB,
    }), &gorm.Config{
        // 禁止主动给表名加 "s"
        NamingStrategy: schema.NamingStrategy{SingularTable: true},
    })
    if err != nil {
        global.DB = nil
        log.Fatalln(err)

        return err
    }
    
    // 设置全局DB
    global.DB = gormDB
    log.Println("Mysql Init Success")

    return nil
}

创立minio.go

package initialize

import (
    "log"
    "minio_server/global"

    "github.com/minio/minio-go/v7"
    "github.com/minio/minio-go/v7/pkg/credentials"
)

func InitMinIO() error {
    minioInfo := global.Settings.MinioInfo
    
    // 创立minio服务, 传入IP、账号、明码.
    minioClient, err := minio.New(minioInfo.Endpoint, &minio.Options{
        Creds:  credentials.NewStaticV4(minioInfo.AccessKey, minioInfo.SecretKey, ""),
        // 敞开TLS, 临时不须要
        Secure: false,
    })
    if err != nil {
        global.MinioClient = nil
        log.Fatalln(err)

        return err
    }
    
    // 设置全局MinioClient
    global.MinioClient = minioClient
    log.Println("Minio Init Success")

    return nil
}

创立init.go, 将须要初始化配置的利用对立封装到init文件.

package initialize

func Init() {
    errConf := InitConfig()
    if errConf != nil {
        panic(errConf)
    }

    errSql := InitMysqlDB()
    if errSql != nil {
        panic(errSql)
    }

    errMinio := InitMinIO()
    if errMinio != nil {
        panic(errMinio)
    }
}

SDK封装

接下来写点SDK相干的代码.

创立models文件夹.

创立user.go, 依照字段去mysql数据库外面创立一些要用的账号密码. Level分1-3级, 依据本人需要配置.

package models

import "time"

type User struct {
    UserID     int16   `sql:"user_id" json:"user_id"`         // 用户ID
    Access     string    `sql:"access" json:"access"`           // 用户权限
    AccessKey  string    `sql:"access_key" json:"access_key"`   // 用户名称
    SecretKey  string    `sql:"secret_key" json:"secret_key"`   // 用户明码
    Level      int       `sql:"level" json:"level"`             // 用户等级
    CreateTime time.Time `sql:"create_time" json:"create_time"` // 创立工夫
    UpdateTime time.Time `sql:"update_time" json:"update_time"` // 更新工夫
}

JWT

创立common文件夹.

创立jwt.go

package common

import (
    "minio_server/models"
    "time"

    "github.com/golang-jwt/jwt"
)

var jwtKey = []byte("your_key")

type Claims struct {
    UserID    int64
    Access    string
    AccessKey string
    Level     int
    jwt.StandardClaims
}

// 颁发token
func ReleaseToken(user models.User) (string, error) {
    expirationTime := time.Now().Add(7 * 24 * time.Hour)

    claims := &Claims{
        UserID:    user.UserID, // 用户id
        Access:    user.Access, // 用户权限
        AccessKey: user.AccessKey, // 用户账号
        Level:     user.Level, // 等级
        StandardClaims: jwt.StandardClaims{
            ExpiresAt: expirationTime.Unix(), // 过期工夫
            IssuedAt:  time.Now().Unix(), // 签发工夫
            Issuer:    "minio", // 签发人
            Subject:   "token", // 题目
        },
    }
    
    // 加密
    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    tokenString, err := token.SignedString(jwtKey)
    if err != nil {
        return "", err
    }

    return tokenString, nil
}

// 解析token
func ParseToken(tokenString string) (*jwt.Token, *Claims, error) {
    claims := &Claims{}

    token, err := jwt.ParseWithClaims(tokenString, claims, func(t *jwt.Token) (interface{}, error) {
        return jwtKey, nil
    })
    
    return token, claims, err
}

repositories

创立repositories文件夹, 这里放一些跟数据库打交道的代码.

创立user.go

package repositories

import (
    "errors"
    "minio_server/common"
    "minio_server/global"
    "minio_server/models"

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

type UserRepository interface {
    Login(*models.User) (string, error)
}

type UserManageRepository struct {
    table string
}

func NewUserManagerRepository(table string, sql *gorm.DB) UserRepository {
    return &UserManageRepository{
        table: table, // 表名, 有用DB.Table查问, 就保留下来了, 不须要的能够删除
    }
}

func (*UserManageRepository) Login(user *models.User) (string, error) {

    if global.DB == nil {
        return "", errors.New("数据库连贯失败")
    }
    
    // 初始化user表, 不是必须
    global.DB.AutoMigrate(&models.User{})

    var m models.User
    
    // 判断用户是否存在
    if err := global.DB.Where("access_key = ?", &user.AccessKey).First(&m).Error; err != nil {
        if m.UserID == 0 {
            return "", errors.New("用户不存在")
        }
        return "", err
    }
    
    // 数据库的明码没有用hash加密的话, 就不须要通过bcrypt库的办法来比对, 间接比照就好
    // bcrypt库的办法同样能够加密后插入数据库
    if err := bcrypt.CompareHashAndPassword([]byte(m.SecretKey), []byte(user.SecretKey)); err
    != nil {
        return "", errors.New("明码谬误")
    }
    // 颁发token
    token, err := common.ReleaseToken(m)
    if err != nil {
        return "", err
    }

    return token, nil
}

response

创立response文件夹, 这里封装解决胜利或者失败返回的JSON状态数据.

创立response.go, 依据本人理论状况调整.

package response

import (
    "net/http"

    "github.com/gin-gonic/gin"
)

func Info(c *gin.Context, httpStatus int, status int, code int, data interface{}, message string) {
    c.JSON(httpStatus, gin.H{
        "status":  status,
        "code":    code,
        "data":    data,
        "message": message,
    })
}

func Success(c *gin.Context, data interface{}, message string) {
    Info(c, http.StatusOK, 1, 200, data, message)
}

func Unauthorized(c *gin.Context, message string) {
    Info(c, http.StatusUnauthorized, -1, 401, nil, message)
}

func NotFound(c *gin.Context) {
    Info(c, http.StatusNotFound, -1, 404, nil, "申请资源不存在")
}

func Fail(c *gin.Context, data interface{}, message string) {
    Info(c, http.StatusBadRequest, -1, 400, nil, message)
}

services

创立services文件夹, 这里放一些解决业务相干的代码.

创立user.go

package services

import (
    "encoding/base64"
    "minio_server/common"
    "minio_server/models"
    "minio_server/repositories"
    "minio_server/response"
    "net/http"

    "github.com/gin-gonic/gin"
    "github.com/gin-gonic/gin/binding"
)

type IUserService interface {
    Login(c *gin.Context)
    UserInfo(c *gin.Context)
}

type UserService struct {
    UserRepository repositories.UserRepository
}

func NewUserService(repository repositories.UserRepository) IUserService {
    return &UserService{UserRepository: repository}
}

// Login godoc
// @Summary 登录
// @Tags users
// @Accept json
// @Param bucket body swagger.Login true "账号密码必须填"
// @Success 200 "{"message": "登录胜利", status: 1}"
// @Failure 400 "{"message": "登录失败", status: -1}"
// @Router /api/user/login [post]
func (u *UserService) Login(c *gin.Context) {
    var reqInfo models.User

    if err := c.ShouldBindBodyWith(&reqInfo, binding.JSON); err != nil {
        response.Fail(c, nil, err.Error())
    } else {

        if len(reqInfo.SecretKey) < 6 {
            response.Info(c, http.StatusUnprocessableEntity, -1, 422, nil, "明码必须大于6位数!")
            return
        }

        if len(reqInfo.AccessKey) == 0 {
            response.Info(c, http.StatusUnprocessableEntity, -1, 422, nil, "用户名称不能为空!")
            return
        }

        if token, errLogin := u.UserRepository.Login(&reqInfo); errLogin != nil {
            response.Info(c, http.StatusPreconditionFailed, -1, 412, nil, errLogin.Error())
        } else {
            response.Success(c, token, "登陆胜利")
        }
    }
}

// UserInfo godoc
// @Summary 获取用户信息
// @Description 留神: header设置token时后面带 Bearer + 空格
// @Tags users
// @Accept json
// @security ApiKeyAuth
// @Success 200 "{"message": "获取胜利", status: 1}"
// @Failure 400 "{"message": "获取失败", status: -1}"
// @Router /api/user/info [get]
func (*UserService) UserInfo(c *gin.Context) {

    // 用户信息随token一起发送给了前端, 这个办法能够去掉.
    
    // token外面有用户身份, 解密进去
    tokenString := c.GetHeader("Authorization")
    tokenString = tokenString[7:]
    _, claims, err := common.ParseToken(tokenString)
    if err != nil {
        response.Fail(c, nil, err.Error())
        return
    }
    
    // base64加密发回去
    access := base64.StdEncoding.EncodeToString([]byte(claims.Access))

    if len(access) <= 0 {
        response.Fail(c, nil, "获取信息失败")
        return
    }
    response.Success(c, access, "胜利获取身份信息")
}

models下创立bucket.go, 存储桶内文件列表, SDK接口会返回一堆信息, 我这里只须要展现三个, 能够依据本人须要调整.

package models

type ListObjects struct {
    Name         string `json:"name"` // 文件名
    Size         int    `json:"size"` // 大小
    LastModified string `json:"last_modified"` // 批改工夫
}

services文件夹下创立bucket.go, 存储桶相干的SDK封装, 这里没有用到数据库, 所以repositories外面不会创立bucket相干的服务, 有需要的能够本人创立, 代码模式跟user一样, 内容不同而已.

package services

import (
    "minio_server/global"
    "minio_server/models"
    "minio_server/response"

    "github.com/gin-gonic/gin"
    "github.com/minio/minio-go/v7"
)

type IBucketsService interface {
    List(c *gin.Context)
    Exists(c *gin.Context)
    Remove(c *gin.Context)
    ListObjects(c *gin.Context)
}

type BucketsService struct{}

func NewBucketsService() IBucketsService {
    return &BucketsService{}
}

// List godoc
// @Summary 获取存储桶列表
// @Tags buckets
// @Accept json
// @security ApiKeyAuth
// @Success 200 "{"message": "获取胜利", status: 1}"
// @Failure 400 "{"message": "获取失败", status: -1}"
// @Router /api/buckets/list [get]
func (*BucketsService) List(c *gin.Context) {

    buckets, err := global.MinioClient.ListBuckets(c)
    if err != nil {
        response.Fail(c, nil, err.Error())
    }

    response.Success(c, buckets, "获取列表胜利")
}

// Exists godoc
// @Summary 获取存储桶详细信息
// @Tags buckets
// @Accept json
// @security ApiKeyAuth
// @security ApiKeyXRole
// @Success 200 "{"message": "获取胜利", status: 1}"
// @Failure 400 "{"message": "获取失败", status: -1}"
// @Router /api/buckets/exists [get]
func (*BucketsService) Exists(c *gin.Context) {
    bucketName := c.Query("bucket")

    if len(bucketName) <= 0 {
        response.Fail(c, nil, "桶名为空")
        return
    }

    ok, _ := global.MinioClient.BucketExists(c, bucketName)
    if !ok {
        response.Fail(c, nil, "查问不到该桶")
        return
    }

    response.Success(c, nil, "查问胜利")
}

// Remove godoc
// @Summary 删除存储桶
// @Tags buckets
// @Accept json
// @security ApiKeyAuth
// @security ApiKeyXRole
// @Param bucket query string true "存储桶名必传"
// @Success 200 "{"message": "删除胜利", status: 1}"
// @Failure 400 "{"message": "删除失败", status: -1}"
// @Router /api/buckets/remove [post]
func (*BucketsService) Remove(c *gin.Context) {
    bucketName := c.Query("bucket")

    if len(bucketName) <= 0 {
        response.Fail(c, nil, "桶名为空, 无奈删除")
        return
    }

    err := global.MinioClient.RemoveBucket(c, bucketName)
    if err != nil {
        response.Fail(c, nil, "删除失败")
        return
    }

    response.Success(c, nil, "删除胜利")

}

// ListObjects godoc
// @Summary 获取存储桶内所有文件列表
// @Tags buckets
// @Accept json
// @security ApiKeyAuth
// @Param bucket query string true "存储桶名必传"
// @Success 200 "{"message": "获取胜利", status: 1}"
// @Failure 400 "{"message": "获取失败", status: -1}"
// @Router /api/buckets/listobjects [get]
func (*BucketsService) ListObjects(c *gin.Context) {
    bucketName := c.Query("bucket")

    if len(bucketName) <= 0 {
        response.Fail(c, nil, "桶名为空, 无奈获取列表")
        return
    }

    list := make(chan []models.ListObjects)
    
    go func() {
        defer close(list)

        var arr []models.ListObjects

        opts := minio.ListObjectsOptions{
                UseV1:     true,
                Recursive: true,
        }

        objects := global.MinioClient.ListObjects(c, bucketName, opts)

        for object := range objects {
            if object.Err != nil {
                return
            }

            arr = append(arr, models.ListObjects{
                Name:         object.Key,
                Size:         int(object.Size),
                LastModified: object.LastModified.Format("2006-01-02 15:04:05"),
            })
        }

        list <- arr

    }()

    data, ok := <-list

    if !ok {
        response.Fail(c, nil, "指定的存储桶不存在")
        return
    }

    response.Success(c, data, "查问胜利")
}

services文件夹下创立object.go, 与文件相干的SDK封装. 同样没有用到数据库.

package services

import (
    "fmt"
    "minio_server/global"
    "minio_server/response"
    "net/url"
    "time"

    "github.com/gin-gonic/gin"
    "github.com/minio/minio-go/v7"
)

type IObjectService interface {
    Get(c *gin.Context)
    GetObjectUrl(c *gin.Context)
    Stat(c *gin.Context)
    Remove(c *gin.Context)
    Put(c *gin.Context)
}

type ObjectService struct{}

func NewObjectService() IObjectService {
    return &ObjectService{}
}

func (*ObjectService) Get(c *gin.Context) {
    bucketName := c.Query("bucket")
    objectName := c.Query("object")

    if bucketName == "" || objectName == "" {
        response.Fail(c, nil, "桶名或文件名谬误")
        return
    }

    reader, err := global.MinioClient.GetObject(c, bucketName, objectName, minio.GetObjectOptions{})
    if err != nil {
        response.Fail(c, nil, err.Error())
        return
    }

    response.Success(c, nil, "获取文件胜利")
    defer reader.Close()
}

// GetObjectUrl godoc
// @Summary 获取文件的url
// @Tags objects
// @Accept json
// @security ApiKeyAuth
// @security ApiKeyXRole
// @Param bucket query string true "存储桶名必传"
// @Param object query string true "文件名必传"
// @Success 200 "{"message": "获取胜利", status: 1}"
// @Failure 400 "{"message": "获取失败", status: -1}"
// @Router /api/object/url [get]
func (*ObjectService) GetObjectUrl(c *gin.Context) {
    bucketName := c.Query("bucket")
    objectName := c.Query("object")

    if bucketName == "" || objectName == "" {
        response.Fail(c, nil, "桶名或文件名谬误")
        return
    }

    reqParams := make(url.Values)
    fileName := fmt.Sprintf("attachment; filename=\"%s\"", objectName)
    reqParams.Set("response-content-disposition", fileName)

    presignedURL, err := global.MinioClient.PresignedGetObject(c, bucketName, objectName, time.Duration(1000)*time.Second, reqParams)
    if err != nil {
        response.Fail(c, nil, err.Error())
        return
    }

    // presignedURL 肯定要string
    response.Success(c, presignedURL.String(), "获取文件门路胜利")
}

func (*ObjectService) Stat(c *gin.Context) {
    bucketName := c.Query("bucket")
    objectName := c.Query("object")

    if bucketName == "" || objectName == "" {
        response.Fail(c, nil, "请查看传参是否正确")
        return
    }

    stat, err := global.MinioClient.StatObject(c, bucketName, objectName, minio.StatObjectOptions{})
    if err != nil {
        response.Fail(c, nil, err.Error())
        return
    }

    response.Success(c, stat, "获取文件信息胜利")
}

// Remove godoc
// @Summary 删除文件
// @Tags objects
// @Accept json
// @security ApiKeyAuth
// @security ApiKeyXRole
// @Param bucket query string true "存储桶名必传"
// @Param object query string true "文件名必传"
// @Success 200 "{"message": "删除胜利", status: 1}"
// @Failure 400 "{"message": "删除失败", status: -1}"
// @Router /api/object/remove [post]
func (*ObjectService) Remove(c *gin.Context) {
    bucketName := c.Query("bucket")
    objectName := c.Query("object")

    if bucketName == "" || objectName == "" {
        response.Fail(c, nil, "请查看传参是否正确")
        return
    }

    opts := minio.RemoveObjectOptions{
        GovernanceBypass: true,
    }

    err := global.MinioClient.RemoveObject(c, bucketName, objectName, opts)
    if err != nil {
        response.Fail(c, nil, err.Error())
        return
    }

    response.Success(c, nil, "删除胜利")
}

// Upload godoc
// @Summary 上传文件
// @Tags objects
// @Accept json
// @security ApiKeyAuth
// @security ApiKeyXRole
// @Accept multipart/form-data
// @Param file formData file true "文件名必传"
// @Param bucket formData string true "存储桶名必传"
// @Success 200 "{"message": "删除胜利", status: 1}"
// @Failure 400 "{"message": "删除失败", status: -1}"
// @Router /api/object/upload [post]
func (*ObjectService) Put(c *gin.Context) {
    file, _ := c.FormFile("file")
    bucket := c.PostForm("bucket")

    reader, errFile := file.Open()
    if errFile != nil {
        response.Fail(c, nil, errFile.Error())
        return
    }
    uoloadInfo, err := global.MinioClient.PutObject(c, bucket, file.Filename, reader, file.Size, minio.PutObjectOptions{ContentType: "application/octet-stream"})
    if err != nil {
        response.Fail(c, nil, err.Error())
        return
    }

    response.Success(c, uoloadInfo, "文件上传胜利")
}

中间件验证

casbin

创立auth_model.conf

[request_definition]
r = sub, obj, act

[policy_definition]
p = sub, obj, act

[policy_effect]
e = some(where (p.eft == allow))

[matchers]
m = r.sub == p.sub && keyMatch(r.obj, p.obj) && regexMatch(r.act, p.act)

创立casbin.sql, 找个mysql客户端连贯上后面创立好的数据库导进去.

-- Root
-- User
INSERT INTO casbin_rule (p_type,v0,v1,v2,v3,v4,v5) VALUES (
'p','root','/api/user/registered','POST','','','');
INSERT INTO casbin_rule (p_type,v0,v1,v2,v3,v4,v5) VALUES (
'p','root','/api/user/login','POST','','','');
INSERT INTO casbin_rule (p_type,v0,v1,v2,v3,v4,v5) VALUES (
'p','root','/api/user/info','GET','','','');
-- Buckets
INSERT INTO casbin_rule (p_type,v0,v1,v2,v3,v4,v5) VALUES (
'p','root','/api/buckets/list','GET','','','');
INSERT INTO casbin_rule (p_type,v0,v1,v2,v3,v4,v5) VALUES (
'p','root','/api/buckets/exists','GET','','','');
INSERT INTO casbin_rule (p_type,v0,v1,v2,v3,v4,v5) VALUES (
'p','root','/api/buckets/remove','POST','','','');
INSERT INTO casbin_rule (p_type,v0,v1,v2,v3,v4,v5) VALUES (
'p','root','/api/buckets/listobjects','GET','','','');
-- Object
INSERT INTO casbin_rule (p_type,v0,v1,v2,v3,v4,v5) VALUES (
'p','root','/api/object/stat','GET','','','');
INSERT INTO casbin_rule (p_type,v0,v1,v2,v3,v4,v5) VALUES (
'p','root','/api/object/remove','POST','','','');
INSERT INTO casbin_rule (p_type,v0,v1,v2,v3,v4,v5) VALUES (
'p','root','/api/object/upload','POST','','','');
INSERT INTO casbin_rule (p_type,v0,v1,v2,v3,v4,v5) VALUES (
'p','root','/api/object/url','GET','','','');

-- No user registered just reads and writes
-- readwrite
INSERT INTO casbin_rule (p_type,v0,v1,v2,v3,v4,v5) VALUES (
'p','readwrite','/api/user/login','POST','','','');
INSERT INTO casbin_rule (p_type,v0,v1,v2,v3,v4,v5) VALUES (
'p','readwrite','/api/user/info','GET','','','');
-- Buckets
INSERT INTO casbin_rule (p_type,v0,v1,v2,v3,v4,v5) VALUES (
'p','readwrite','/api/buckets/list','GET','','','');
INSERT INTO casbin_rule (ptype,v0,v1,v2,v3,v4,v5) VALUES (
'p','readwrite','/api/buckets/exists','GET','','','');
INSERT INTO casbin_rule (ptype,v0,v1,v2,v3,v4,v5) VALUES (
'p','readwrite','/api/buckets/remove','POST','','','');
INSERT INTO casbin_rule (ptype,v0,v1,v2,v3,v4,v5) VALUES (
'p','readwrite','/api/buckets/listobjects','GET','','','');
-- Object
INSERT INTO casbin_rule (ptype,v0,v1,v2,v3,v4,v5) VALUES (
'p','readwrite','/api/object/stat','GET','','','');
INSERT INTO casbin_rule (ptype,v0,v1,v2,v3,v4,v5) VALUES (
'p','readwrite','/api/object/remove','POST','','','');
INSERT INTO casbin_rule (ptype,v0,v1,v2,v3,v4,v5) VALUES (
'p','readwrite','/api/object/upload','POST','','','');
INSERT INTO casbin_rule (ptype,v0,v1,v2,v3,v4,v5) VALUES (
'p','readwrite','/api/object/url','GET','','','');

-- No user registered just reads and writes
-- read
INSERT INTO casbin_rule (ptype,v0,v1,v2,v3,v4,v5) VALUES (
'p','read','/api/user/login','POST','','','');
INSERT INTO casbin_rule (ptype,v0,v1,v2,v3,v4,v5) VALUES (
'p','read','/api/user/info','GET','','','');
-- Buckets
INSERT INTO casbin_rule (ptype,v0,v1,v2,v3,v4,v5) VALUES (
'p','read','/api/buckets/list','GET','','','');
INSERT INTO casbin_rule (ptype,v0,v1,v2,v3,v4,v5) VALUES (
'p','read','/api/buckets/listobjects','GET','','','');

root、readwrite、read三种权限对应各自能拜访的接口.

不相熟casbin的看这个 文档: https://casbin.org/

封装casbin办法

common文件夹下, 创立casbin.go

package common

import (
    "log"
    "minio_server/global"
    "os"

    "github.com/casbin/casbin/v2"
    gormadapter "github.com/casbin/gorm-adapter/v3"
    "github.com/gin-gonic/gin"
)

//权限构造
type CasbinModel struct {
    Ptype    string `json:"p_type" bson:"p_type"`
    RoleName string `json:"rolename" bson:"v0"`
    Path     string `json:"path" bson:"v1"`
    Method   string `json:"method" bson:"v2"`
}

//增加权限
func (c *CasbinModel) AddCasbin(cm CasbinModel) bool {
    e := Casbin()
    isTrue, _ := e.AddPolicy(cm.RoleName, cm.Path, cm.Method)
    return isTrue
}

//长久化到数据库
func Casbin() *casbin.Enforcer {
    workDor, _ := os.Getwd()
    if global.DB == nil {
        log.Fatalln("数据库连贯失败")
    }

    g, _ := gormadapter.NewAdapterByDB(global.DB)
    c, _ := casbin.NewEnforcer(workDor+"/conf/auth_model.conf", g)
    c.LoadPolicy()
    return c
}

var (
    casbins = CasbinModel{}
)

func AddCasbin(c *gin.Context) {
    rolename := c.PostForm("rolename")
    path := c.PostForm("path")
    method := c.PostForm("method")
    ptype := "p"
    casbin := CasbinModel{
        Ptype:    ptype,
        RoleName: rolename,
        Path:     path,
        Method:   method,
    }
    isok := casbins.AddCasbin(casbin)
    if isok {
        log.Println("Add Cabin Success")
    } else {
        log.Println("Add Cabin Error")
    }
}

将jwt, casbin封装进验证服务.

创立middleware文件夹, 这里放中间件相干的.

创立auth.go

package middleware

import (
    "encoding/base64"
    "minio_server/common"
    "minio_server/global"
    "minio_server/models"
    "minio_server/response"
    "strings"

    "github.com/gin-gonic/gin"
)

// token验证
func Auth() gin.HandlerFunc {
    return func(c *gin.Context) {
        tokenString := c.GetHeader("Authorization")

        if tokenString == "" || !strings.HasPrefix(tokenString, "Bearer") {
            response.Unauthorized(c, "权限有余")
            c.Abort()
            return
        }

        tokenString = tokenString[7:]
        token, claims, err := common.ParseToken(tokenString)
        if err != nil || !token.Valid {
            response.Unauthorized(c, "权限有余")
            c.Abort()
            return
        }

        userId := claims.UserID
        var user models.User

        if errSearch := global.DB.Table("user").First(&user, userId).Error; errSearch != nil {
            response.Fail(c, nil, errSearch.Error())
            return
        }

        c.Set("user", user)
        c.Next()
    }
}

// casbin权限验证
func AuthCheckRole() gin.HandlerFunc {
    return func(c *gin.Context) {
        // 获取头部x-role 身份
        roleString := c.GetHeader("x-role")
        if roleString == "" {
            response.Unauthorized(c, "有效身份")
            c.Abort()
            return
        }

        // base64 解密
        role, err := base64.StdEncoding.DecodeString(roleString)
        if err != nil {
            response.Unauthorized(c, "有效身份")
            c.Abort()
            return
        }

        e := common.Casbin()

        //查看权限
        res, errRes := e.Enforce(string(role), c.Request.URL.Path, c.Request.Method)
        if errRes != nil {
            response.Fail(c, nil, errRes.Error())
            c.Abort()
            return
        }
        if res {
            c.Next()
        } else {
            response.Unauthorized(c, "权限有余")
            c.Abort()
            return
        }
    }
}

创立cors.go, 解决跨域的服务, 没有跨域问题的无需创立.

package middleware

import (
    "net/http"

    "github.com/gin-gonic/gin"
)

// 解决跨域申请,反对options拜访
func Cors() gin.HandlerFunc {
    return func(c *gin.Context) {
        method := c.Request.Method
        
        c.Header("Access-Control-Allow-Origin", "*") // *代理容许拜访所有域 正式环境慎用
        
        c.Header( // Authorization token验证, x-role 身份验证.
            "Access-Control-Allow-Headers",
            "Cache-Control,
             Content-Language,
             Content-Type,
             Expires,
             Last-Modified,
             Pragma,
             Authorization,
             x-role"
        )
        c.Header( //服务器反对的所有跨域申请的办法,为了防止浏览次申请的屡次'预检'申请
            "Access-Control-Allow-Methods",
            "POST, GET, OPTIONS, PUT, PATCH, DELETE"
        )
        
        c.Header( // 首部能够作为响应的一部分裸露给内部
            "Access-Control-Expose-Headers",
            "Content-Length,
            Access-Control-Allow-Origin,
            Access-Control-Allow-Headers,
            Content-Type"
        )
        
        c.Header("Access-Control-Allow-Credentials", "true") // 容许客户端携带验证信息

        // 放行OPTIONS办法
        if method == "OPTIONS" {
            c.AbortWithStatus(http.StatusNoContent)
        }

        c.Next()
    }
}

中间件起到验证作用

api申请–>cors验证–>token验证–>role验证–>casbin权限验证–>返回后果

封装router

创立router文件夹

创立router.go

package router

import (
    "minio_server/middleware"
    "minio_server/repositories"
    "minio_server/services"

    _ "minio_server/docs"

    "github.com/gin-gonic/gin"
    ginSwagger "github.com/swaggo/gin-swagger"
    "github.com/swaggo/gin-swagger/swaggerFiles"
)

func CollectRoute(r *gin.Engine) *gin.Engine {

    // 注册服务
    userRepository := repositories.NewUserManagerRepository("user", nil)
    userService := services.NewUserService(userRepository)

    bucketsService := services.NewBucketsService()
    objectService := services.NewObjectService()

    // 全局退出cors验证
    r.Use(middleware.Cors())

    user := r.Group("/api/user")
    {
        user.POST("/login", userService.Login)
        user.GET("/info", middleware.Auth(), userService.UserInfo)
    }

    bukets := r.Group("/api/buckets")
    // 全组退出token验证
    bukets.Use(middleware.Auth())
    {
        bukets.GET("/list", bucketsService.List)
        bukets.GET("/exists", middleware.AuthCheckRole(), bucketsService.Exists)
        bukets.POST("/remove", middleware.AuthCheckRole(), bucketsService.Remove)
        bukets.GET("/listobjects", bucketsService.ListObjects)
    }

    object := r.Group("/api/object")
    object.Use(middleware.Auth())
    {
        object.GET("/stat", objectService.Stat)
        object.POST("/remove", middleware.AuthCheckRole(), objectService.Remove)
        object.POST("/upload", middleware.AuthCheckRole(), objectService.Put)
        object.GET("/url", middleware.AuthCheckRole(), objectService.GetObjectUrl)
    }

    // swagger文档
    r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))

    return r
}

_ “minio_server/docs” 这块引入会报错, 先疏忽或者正文掉, 上面swagger文件建设后就不会报错了.

不相熟Gin的能够看文档 https://gin-gonic.com/zh-cn/

main.go 批改

package main

import (
    "minio_server/initialize"
    "minio_server/router"

    "github.com/fatih/color"
    "github.com/gin-gonic/gin"
)

// @title Swagger Example API
// @version 0.0.1
// @description This is a Minio Server
// @securityDefinitions.apikey ApiKeyAuth
// @in header
// @name Authorization
// @securityDefinitions.apikey ApiKeyXRole
// @in header
// @name x-role
// @host localhost:8082
// @BasePath /
func main() {
    // color控制台输入字体色彩, 忽视就好
    color.Yellow("mysql & minio =====> Init.....")
    // 初始化全局单例
    initialize.Init()
    color.Yellow("Init end!")

    color.Red("=========")

    color.Blue("gin service started ======>")
    r := gin.Default()
    r = router.CollectRoute(r)
    r.Run(":8082")
}

Swagger正文文档

models文件夹新建swagger文件夹

创立swagger.go, login登录所需的参数

package swagger

type Login struct {
    AccessKey string `json:"access_key"`
    SecretKey string `json:"secret_key"`
}

后面的代码有很多swagger正文了.

控制台输出命令:

swag init

根目录会多出docs文件夹,router.go文件对于swagger引入的报错修改了.

地址拜访: http://127.0.0.1(替换地址)/swagger/index.html#/

启动程序

看服务是否可能失常跑起来.

go run main.go

集成部署

上一篇jenkins+webhooks有看吗?对, 又到他们出场了.

首先装置下插件

设置全局工具

抉择你golang开发的版本, 如果主动下载无奈实现, 你可能须要手动装置golang到相干目录.

办法:

// 进入映射的主机目录装置, 同样能够映射到容器内.
// 进入容器
docker exec -it 容器id /bin/bash
// cd到相干目录
cd /var/jenkins_home
// 获取安装包
wget https://go.dev/dl/go1.17.8.linux-amd64.tar.gz
// 解压安装包到当前目录
tar -zxvf go1.17.8.linux-amd64.tar.gz

实现后反复下面的全局工具设置, 记得填对安装包的目录.

设置完后开始测试

根目录创立jenkinsfile

pipeline {
  agent {
    docker {
      image 'golang:1.17.8-stretch'
    }
  }
  stages {
    stage('Build') {
      steps {
        sh "chmod +x -R ${env.WORKSPACE}"
        sh 'go env -w GOPROXY=https://goproxy.cn,direct'
        sh 'go mod tidy'
      }
    }
    stage('Test') {
      steps {
        sh 'go test ./utils -count=1 -v'
      }
    }
  }
}

创立工作配置webhooks, 跑测试的过程同上一篇一毛一样. 这里就不反复了.

测试通过如上.

接下来开始部署工作.

根目录创立Dockerfile

FROM alpine:3.15

RUN mkdir -p /app

RUN mkdir /app/logs

RUN mkdir /app/conf

WORKDIR /app

ADD ./dist/main /app/main

ADD ./conf /app/conf

ENV GIN_MODE=release PORT=8082

EXPOSE 8082

CMD ["./main"]

创立构建工作

设置方面同上一篇也是大部分一样, 区别在这里

抉择后面设置好的全局工具

代码如下:

# 进入工作区
cd $WORKSPACE

# 设置代理
export GOPROXY=https://goproxy.cn,direct

# 装置依赖
go mod tidy

# 打印依赖
cat ./go.mod

# 测试
go test ./utils -count=1 -v

# 设置参数打包 依照需要调整为最初部署的环境参数
GOOS=linux CGO_ENABLED=0 go build -o ./dist/main

# docker打包镜像
docker build -t minio_server:$VERSION .

# 删除包
rm -rf main

## stop rm 不是必须
docker stop minio_go_server || true

docker rm minio_go_server -f || true

# 跑容器
docker run -d -p 8082:8082 --name minio_go_server minio_server:$VERSION

# 解决<none>:<none>的垃圾镜像 不是必须
docker rmi $(docker images -f "dangling=true" -q) || true

docker images, docker ps, 查看是否有对应的镜像和容器部署胜利.


接下来用工具测试, 或者swagger文档去测试. 用前篇的前端代码测试也可.

http://127.0.0.1(替换地址)/swagger/index.html#/

swagger登录胜利

前端客户端登录胜利

到这里两篇分享曾经完结.

感激浏览, 如果哪里有谬误或者疑难麻烦评论通知我, 我会及时批改的,谢谢!