一看就会的集成部署!太简单了(那顺便再撸个存储文件服务吧!|一看就会的集成部署!太简单了?那顺便再撸个存储文件服务吧! (二))

本篇主要介绍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...
需要实现的接口不多, 以下:
  • 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"` Usernamestring `yaml:"username" json:"username"` Passwordstring `yaml:"password" json:"password"` Hoststring `yaml:"host" json:"host"` Portstring `yaml:"port" json:"port"` Databasestring `yaml:"database" json:"database"` Charsetstring `yaml:"charset" json:"charset"` ParseTimestring `yaml:"parseTime" json:"parse_time"` Locstring `yaml:"loc" json:"loc"` }type MinioConf struct { Endpointstring `yaml:"endpoint" json:"endpoint"` Accessstring `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 ( Settingsconf.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 = serverConfigreturn 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.MysqlInfoargs := 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 { UserIDint16`sql:"user_id" json:"user_id"`// 用户ID Accessstring`sql:"access" json:"access"`// 用户权限 AccessKeystring`sql:"access_key" json:"access_key"`// 用户名称 SecretKeystring`sql:"secret_key" json:"secret_key"`// 用户密码 Levelint`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 { UserIDint64 Accessstring AccessKey string Levelint 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 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.Userif 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 { Namestring `json:"name"` // 文件名 Sizeint`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.ListObjectsopts := 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 := <-listif !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 -- 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 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 { Ptypestring `json:"p_type" bson:"p_type"` RoleName string `json:"rolename" bson:"v0"` Pathstring `json:"path" bson:"v1"` Methodstring `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.Userif 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.Methodc.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# 处理:的垃圾镜像 不是必须 docker rmi $(docker images -f "dangling=true" -q) || true

docker images, docker ps, 查看是否有对应的镜像和容器部署成功.
一看就会的集成部署!太简单了(那顺便再撸个存储文件服务吧!|一看就会的集成部署!太简单了?那顺便再撸个存储文件服务吧! (二))
文章图片

一看就会的集成部署!太简单了(那顺便再撸个存储文件服务吧!|一看就会的集成部署!太简单了?那顺便再撸个存储文件服务吧! (二))
文章图片

接下来用工具测试, 或者swagger文档去测试. 用前篇的前端代码测试也可.
http://127.0.0.1(替换地址)/swagger/index.html#/
一看就会的集成部署!太简单了(那顺便再撸个存储文件服务吧!|一看就会的集成部署!太简单了?那顺便再撸个存储文件服务吧! (二))
文章图片

swagger登录成功
一看就会的集成部署!太简单了(那顺便再撸个存储文件服务吧!|一看就会的集成部署!太简单了?那顺便再撸个存储文件服务吧! (二))
文章图片

前端客户端登录成功
到这里两篇分享已经结束.
感谢阅读, 如果哪里有错误或者疑问麻烦评论告诉我, 我会及时修改的,谢谢!

    推荐阅读