本篇次要介绍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登录胜利
前端客户端登录胜利
到这里两篇分享曾经完结.
感激浏览, 如果哪里有谬误或者疑难麻烦评论通知我, 我会及时批改的,谢谢!