本篇次要介绍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/gingo get github.com/casbin/casbin/v2go get github.com/golang-jwt/jwtgo get github.com/minio/minio-go/v7go get github.com/swaggo/gin-swaggergo get github.com/swaggo/swaggo get gopkg.in/yaml.v3go get -u gorm.io/gormgo 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...

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

  • ListBuckets 存储桶列表
  • ListObjects 桶内文件列表
  • PutObject 上传文件
  • RemoveObject 删除文件
  • PresignedGetObject 获取URL下载

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

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 conftype 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 globalimport (    "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 initializeimport (    "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 initializeimport (    "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 initializeimport (    "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 initializefunc 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 modelsimport "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 commonimport (    "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}// 颁发tokenfunc 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}// 解析tokenfunc 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 repositoriesimport (    "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 responseimport (    "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 servicesimport (    "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 modelstype ListObjects struct {    Name         string `json:"name"` // 文件名    Size         int    `json:"size"` // 大小    LastModified string `json:"last_modified"` // 批改工夫}

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

package servicesimport (    "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 servicesimport (    "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-- UserINSERT 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','','','');-- BucketsINSERT 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','','','');-- ObjectINSERT 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-- readwriteINSERT 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','','','');-- BucketsINSERT 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','','','');-- ObjectINSERT 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-- readINSERT 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','','','');-- BucketsINSERT 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 commonimport (    "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 middlewareimport (    "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 middlewareimport (    "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 routerimport (    "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 mainimport (    "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 swaggertype 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.15RUN mkdir -p /appRUN mkdir /app/logsRUN mkdir /app/confWORKDIR /appADD ./dist/main /app/mainADD ./conf /app/confENV GIN_MODE=release PORT=8082EXPOSE 8082CMD ["./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 || truedocker 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登录胜利

前端客户端登录胜利

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

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