本篇次要介绍Go开发minio存储文件服务的过程. 篇幅有点长.
要实现的性能, 如下:
鉴权(jwt、casbin)
正文文档(swagger)
MinioSDK(minio)
集成部署(jenkins, docker)
代码↓:
Github \
前端 https://github.com/guangnaoke…
Go https://github.com/guangnaoke…
Gitee \
前端 https://gitee.com/Xiao_Yi_Zho…
Go https://gitee.com/Xiao_Yi_Zho…
都是些比较简单的性能. 那么… 开整!
装置
GO装置库及插件
go get -u github.com/gin-gonic/gin
go get github.com/casbin/casbin/v2
go get github.com/golang-jwt/jwt
go get github.com/minio/minio-go/v7
go get github.com/swaggo/gin-swagger
go get github.com/swaggo/swag
go get gopkg.in/yaml.v3
go get -u gorm.io/gorm
go get -u gorm.io/driver/sqlite
docker装置Minio
docker run \
-d \
-p 9000:9000 \
-p 9001:9001 \
--name minio \
--restart=always \
-v /www/minio/data:/data \
-e "MINIO_ROOT_USER=YOURNAME" \
-e "MINIO_ROOT_PASSWORD=YOURPASSWORD" \
minio/minio:latest server /data --console-address ":9001"
MINIO_ROOT_USER, MINIO_ROOT_PASSWORD 账号密码改成本人的
浏览器输出地址: http://127.0.0.1(替换地址):9001 看是否能关上minio管制端
账号密码就是方才设置的. 当初能够建个bucket试试传输文件之类的.
查看SDK列表页
SDK列表 https://docs.min.io/docs/gola…
例子 https://github.com/minio/mini…
须要实现的接口不多, 以下:
ListBucketsListObjectsPutObjectRemoveObjectPresignedGetObject
删除桶之类的还是管理员去操作, 免得误删. 前端只须要上传和查看性能.
docker装置Mysql
docker run \
-p 3306:3306 \
--name mysql \
--privileged=true \
--restart=always \
-v /usr/mysql/local/conf:/etc/mysql/conf.d \
-v /usr/mysql/local/logs:/logs \
-v /usr/mysql/local/data:/var/lib/mysql \
-v /usr/mysql/local/mysql-files:/var/lib/mysql-files \
-e MYSQL_ROOT_PASSWORD=yourpassword \
-e TZ=Asia/Shanghai \
-d docker.io/mysql:latest
–privileged=true 【容器内的root领有真正的权限, 否则只是普通用户权限】\
-v 【挂载目录, 配置文件, 日志】\
MYSQL_ROOT_PASSWORD 改成本人的明码
查看容器看是否运行胜利
通过Navicat之类的工具看是否能失常连贯数据库.
初始化配置信息
根目录创立conf文件夹.
创立conf.yaml, 把配置信息写入conf.yaml文件.
# Mysql服务配置
mysql:
driverName: mysql
host: 127.0.0.1
port: 3306
database: you_database
username: admin
password: admin
charset: utf8mb4
parseTime: True
loc: Local
# MinIO文件存储服务器配置
minio:
endpoint: 127.0.0.1:9000
access: you_access
accessKey: admin
secretKey: admin
初始化全局单例
创立config.go, 这些都是程序初始化时要用到的模型, mysql的配置信息、minio账号配置等.
package conf
type MysqlConf struct {
DriverName string `yaml:"driverName" json:"driver_name"`
Username string `yaml:"username" json:"username"`
Password string `yaml:"password" json:"password"`
Host string `yaml:"host" json:"host"`
Port string `yaml:"port" json:"port"`
Database string `yaml:"database" json:"database"`
Charset string `yaml:"charset" json:"charset"`
ParseTime string `yaml:"parseTime" json:"parse_time"`
Loc string `yaml:"loc" json:"loc"`
}
type MinioConf struct {
Endpoint string `yaml:"endpoint" json:"endpoint"`
Access string `yaml:"access" json:"access"`
AccessKey string `yaml:"accessKey" json:"accessKey"`
SecretKey string `yaml:"secretKey" json:"secretKey"`
}
type ServerConf struct {
MysqlInfo MysqlConf `yaml:"mysql" json:"mysql"`
MinioInfo MinioConf `yaml:"minio" json:"minio"`
}
创立global文件夹.
创立singleton.go, 设置全局单例.
package global
import (
"minio_server/conf"
"github.com/minio/minio-go/v7"
"gorm.io/gorm"
)
var (
Settings conf.ServerConf
DB *gorm.DB
MinioClient *minio.Client
)
创立一个initialize文件夹.
创立config.go, 将之前的yaml配置信息设置到全局serverConfig, 上面会用到.
package initialize
import (
"io/ioutil"
"log"
"minio_server/conf"
"minio_server/global"
"os"
"gopkg.in/yaml.v3"
)
func InitConfig() error {
workDor, _ := os.Getwd()
// 读取yaml配置文件
yamlFile, err := ioutil.ReadFile(workDor + "/conf/conf.yaml")
if err != nil {
log.Printf("yamlFile.Get err %v", err)
return err
}
// 配置信息模型
serverConfig := conf.ServerConf{}
// 将yaml文件对应的配置信息写入serverConfig
err = yaml.Unmarshal(yamlFile, &serverConfig)
if err != nil {
log.Fatalf("Unmarshal: %v", err)
return err
}
// 设置全局Settings
global.Settings = serverConfig
return nil
}
创立mysql.go
package initialize
import (
"database/sql"
"fmt"
"log"
"minio_server/global"
"time"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/schema"
)
func InitMysqlDB() error {
mysqlInfo := global.Settings.MysqlInfo
args := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=%s&parseTime=%s&loc=%s",
mysqlInfo.Username,
mysqlInfo.Password,
mysqlInfo.Host,
mysqlInfo.Port,
mysqlInfo.Database,
mysqlInfo.Charset,
mysqlInfo.ParseTime,
mysqlInfo.Loc,
)
sqlDB, err := sql.Open(mysqlInfo.DriverName, args)
if err != nil {
log.Fatalln(err)
return err
}
// 闲暇连接池中连贯的最大数量
sqlDB.SetMaxIdleConns(10)
// 关上数据库连贯的最大数量, 依据需要看着调
sqlDB.SetMaxOpenConns(100)
// 连贯可复用的最大工夫。
sqlDB.SetConnMaxLifetime(time.Hour)
// 注册单例
gormDB, err := gorm.Open(mysql.New(mysql.Config{
Conn: sqlDB,
}), &gorm.Config{
// 禁止主动给表名加 "s"
NamingStrategy: schema.NamingStrategy{SingularTable: true},
})
if err != nil {
global.DB = nil
log.Fatalln(err)
return err
}
// 设置全局DB
global.DB = gormDB
log.Println("Mysql Init Success")
return nil
}
创立minio.go
package initialize
import (
"log"
"minio_server/global"
"github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
)
func InitMinIO() error {
minioInfo := global.Settings.MinioInfo
// 创立minio服务, 传入IP、账号、明码.
minioClient, err := minio.New(minioInfo.Endpoint, &minio.Options{
Creds: credentials.NewStaticV4(minioInfo.AccessKey, minioInfo.SecretKey, ""),
// 敞开TLS, 临时不须要
Secure: false,
})
if err != nil {
global.MinioClient = nil
log.Fatalln(err)
return err
}
// 设置全局MinioClient
global.MinioClient = minioClient
log.Println("Minio Init Success")
return nil
}
创立init.go, 将须要初始化配置的利用对立封装到init文件.
package initialize
func Init() {
errConf := InitConfig()
if errConf != nil {
panic(errConf)
}
errSql := InitMysqlDB()
if errSql != nil {
panic(errSql)
}
errMinio := InitMinIO()
if errMinio != nil {
panic(errMinio)
}
}
SDK封装
接下来写点SDK相干的代码.
创立models文件夹.
创立user.go, 依照字段去mysql数据库外面创立一些要用的账号密码. Level分1-3级, 依据本人需要配置.
package models
import "time"
type User struct {
UserID int16 `sql:"user_id" json:"user_id"` // 用户ID
Access string `sql:"access" json:"access"` // 用户权限
AccessKey string `sql:"access_key" json:"access_key"` // 用户名称
SecretKey string `sql:"secret_key" json:"secret_key"` // 用户明码
Level int `sql:"level" json:"level"` // 用户等级
CreateTime time.Time `sql:"create_time" json:"create_time"` // 创立工夫
UpdateTime time.Time `sql:"update_time" json:"update_time"` // 更新工夫
}
JWT
创立common文件夹.
创立jwt.go
package common
import (
"minio_server/models"
"time"
"github.com/golang-jwt/jwt"
)
var jwtKey = []byte("your_key")
type Claims struct {
UserID int64
Access string
AccessKey string
Level int
jwt.StandardClaims
}
// 颁发token
func ReleaseToken(user models.User) (string, error) {
expirationTime := time.Now().Add(7 * 24 * time.Hour)
claims := &Claims{
UserID: user.UserID, // 用户id
Access: user.Access, // 用户权限
AccessKey: user.AccessKey, // 用户账号
Level: user.Level, // 等级
StandardClaims: jwt.StandardClaims{
ExpiresAt: expirationTime.Unix(), // 过期工夫
IssuedAt: time.Now().Unix(), // 签发工夫
Issuer: "minio", // 签发人
Subject: "token", // 题目
},
}
// 加密
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
tokenString, err := token.SignedString(jwtKey)
if err != nil {
return "", err
}
return tokenString, nil
}
// 解析token
func ParseToken(tokenString string) (*jwt.Token, *Claims, error) {
claims := &Claims{}
token, err := jwt.ParseWithClaims(tokenString, claims, func(t *jwt.Token) (interface{}, error) {
return jwtKey, nil
})
return token, claims, err
}
repositories
创立repositories文件夹, 这里放一些跟数据库打交道的代码.
创立user.go
package repositories
import (
"errors"
"minio_server/common"
"minio_server/global"
"minio_server/models"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
)
type UserRepository interface {
Login(*models.User) (string, error)
}
type UserManageRepository struct {
table string
}
func NewUserManagerRepository(table string, sql *gorm.DB) UserRepository {
return &UserManageRepository{
table: table, // 表名, 有用DB.Table查问, 就保留下来了, 不须要的能够删除
}
}
func (*UserManageRepository) Login(user *models.User) (string, error) {
if global.DB == nil {
return "", errors.New("数据库连贯失败")
}
// 初始化user表, 不是必须
global.DB.AutoMigrate(&models.User{})
var m models.User
// 判断用户是否存在
if err := global.DB.Where("access_key = ?", &user.AccessKey).First(&m).Error; err != nil {
if m.UserID == 0 {
return "", errors.New("用户不存在")
}
return "", err
}
// 数据库的明码没有用hash加密的话, 就不须要通过bcrypt库的办法来比对, 间接比照就好
// bcrypt库的办法同样能够加密后插入数据库
if err := bcrypt.CompareHashAndPassword([]byte(m.SecretKey), []byte(user.SecretKey)); err
!= nil {
return "", errors.New("明码谬误")
}
// 颁发token
token, err := common.ReleaseToken(m)
if err != nil {
return "", err
}
return token, nil
}
response
创立response文件夹, 这里封装解决胜利或者失败返回的JSON状态数据.
创立response.go, 依据本人理论状况调整.
package response
import (
"net/http"
"github.com/gin-gonic/gin"
)
func Info(c *gin.Context, httpStatus int, status int, code int, data interface{}, message string) {
c.JSON(httpStatus, gin.H{
"status": status,
"code": code,
"data": data,
"message": message,
})
}
func Success(c *gin.Context, data interface{}, message string) {
Info(c, http.StatusOK, 1, 200, data, message)
}
func Unauthorized(c *gin.Context, message string) {
Info(c, http.StatusUnauthorized, -1, 401, nil, message)
}
func NotFound(c *gin.Context) {
Info(c, http.StatusNotFound, -1, 404, nil, "申请资源不存在")
}
func Fail(c *gin.Context, data interface{}, message string) {
Info(c, http.StatusBadRequest, -1, 400, nil, message)
}
services
创立services文件夹, 这里放一些解决业务相干的代码.
创立user.go
package services
import (
"encoding/base64"
"minio_server/common"
"minio_server/models"
"minio_server/repositories"
"minio_server/response"
"net/http"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
)
type IUserService interface {
Login(c *gin.Context)
UserInfo(c *gin.Context)
}
type UserService struct {
UserRepository repositories.UserRepository
}
func NewUserService(repository repositories.UserRepository) IUserService {
return &UserService{UserRepository: repository}
}
// Login godoc
// @Summary 登录
// @Tags users
// @Accept json
// @Param bucket body swagger.Login true "账号密码必须填"
// @Success 200 "{"message": "登录胜利", status: 1}"
// @Failure 400 "{"message": "登录失败", status: -1}"
// @Router /api/user/login [post]
func (u *UserService) Login(c *gin.Context) {
var reqInfo models.User
if err := c.ShouldBindBodyWith(&reqInfo, binding.JSON); err != nil {
response.Fail(c, nil, err.Error())
} else {
if len(reqInfo.SecretKey) < 6 {
response.Info(c, http.StatusUnprocessableEntity, -1, 422, nil, "明码必须大于6位数!")
return
}
if len(reqInfo.AccessKey) == 0 {
response.Info(c, http.StatusUnprocessableEntity, -1, 422, nil, "用户名称不能为空!")
return
}
if token, errLogin := u.UserRepository.Login(&reqInfo); errLogin != nil {
response.Info(c, http.StatusPreconditionFailed, -1, 412, nil, errLogin.Error())
} else {
response.Success(c, token, "登陆胜利")
}
}
}
// UserInfo godoc
// @Summary 获取用户信息
// @Description 留神: header设置token时后面带 Bearer + 空格
// @Tags users
// @Accept json
// @security ApiKeyAuth
// @Success 200 "{"message": "获取胜利", status: 1}"
// @Failure 400 "{"message": "获取失败", status: -1}"
// @Router /api/user/info [get]
func (*UserService) UserInfo(c *gin.Context) {
// 用户信息随token一起发送给了前端, 这个办法能够去掉.
// token外面有用户身份, 解密进去
tokenString := c.GetHeader("Authorization")
tokenString = tokenString[7:]
_, claims, err := common.ParseToken(tokenString)
if err != nil {
response.Fail(c, nil, err.Error())
return
}
// base64加密发回去
access := base64.StdEncoding.EncodeToString([]byte(claims.Access))
if len(access) <= 0 {
response.Fail(c, nil, "获取信息失败")
return
}
response.Success(c, access, "胜利获取身份信息")
}
models下创立bucket.go, 存储桶内文件列表, SDK接口会返回一堆信息, 我这里只须要展现三个, 能够依据本人须要调整.
package models
type ListObjects struct {
Name string `json:"name"` // 文件名
Size int `json:"size"` // 大小
LastModified string `json:"last_modified"` // 批改工夫
}
services文件夹下创立bucket.go, 存储桶相干的SDK封装, 这里没有用到数据库, 所以repositories外面不会创立bucket相干的服务, 有需要的能够本人创立, 代码模式跟user一样, 内容不同而已.
package services
import (
"minio_server/global"
"minio_server/models"
"minio_server/response"
"github.com/gin-gonic/gin"
"github.com/minio/minio-go/v7"
)
type IBucketsService interface {
List(c *gin.Context)
Exists(c *gin.Context)
Remove(c *gin.Context)
ListObjects(c *gin.Context)
}
type BucketsService struct{}
func NewBucketsService() IBucketsService {
return &BucketsService{}
}
// List godoc
// @Summary 获取存储桶列表
// @Tags buckets
// @Accept json
// @security ApiKeyAuth
// @Success 200 "{"message": "获取胜利", status: 1}"
// @Failure 400 "{"message": "获取失败", status: -1}"
// @Router /api/buckets/list [get]
func (*BucketsService) List(c *gin.Context) {
buckets, err := global.MinioClient.ListBuckets(c)
if err != nil {
response.Fail(c, nil, err.Error())
}
response.Success(c, buckets, "获取列表胜利")
}
// Exists godoc
// @Summary 获取存储桶详细信息
// @Tags buckets
// @Accept json
// @security ApiKeyAuth
// @security ApiKeyXRole
// @Success 200 "{"message": "获取胜利", status: 1}"
// @Failure 400 "{"message": "获取失败", status: -1}"
// @Router /api/buckets/exists [get]
func (*BucketsService) Exists(c *gin.Context) {
bucketName := c.Query("bucket")
if len(bucketName) <= 0 {
response.Fail(c, nil, "桶名为空")
return
}
ok, _ := global.MinioClient.BucketExists(c, bucketName)
if !ok {
response.Fail(c, nil, "查问不到该桶")
return
}
response.Success(c, nil, "查问胜利")
}
// Remove godoc
// @Summary 删除存储桶
// @Tags buckets
// @Accept json
// @security ApiKeyAuth
// @security ApiKeyXRole
// @Param bucket query string true "存储桶名必传"
// @Success 200 "{"message": "删除胜利", status: 1}"
// @Failure 400 "{"message": "删除失败", status: -1}"
// @Router /api/buckets/remove [post]
func (*BucketsService) Remove(c *gin.Context) {
bucketName := c.Query("bucket")
if len(bucketName) <= 0 {
response.Fail(c, nil, "桶名为空, 无奈删除")
return
}
err := global.MinioClient.RemoveBucket(c, bucketName)
if err != nil {
response.Fail(c, nil, "删除失败")
return
}
response.Success(c, nil, "删除胜利")
}
// ListObjects godoc
// @Summary 获取存储桶内所有文件列表
// @Tags buckets
// @Accept json
// @security ApiKeyAuth
// @Param bucket query string true "存储桶名必传"
// @Success 200 "{"message": "获取胜利", status: 1}"
// @Failure 400 "{"message": "获取失败", status: -1}"
// @Router /api/buckets/listobjects [get]
func (*BucketsService) ListObjects(c *gin.Context) {
bucketName := c.Query("bucket")
if len(bucketName) <= 0 {
response.Fail(c, nil, "桶名为空, 无奈获取列表")
return
}
list := make(chan []models.ListObjects)
go func() {
defer close(list)
var arr []models.ListObjects
opts := minio.ListObjectsOptions{
UseV1: true,
Recursive: true,
}
objects := global.MinioClient.ListObjects(c, bucketName, opts)
for object := range objects {
if object.Err != nil {
return
}
arr = append(arr, models.ListObjects{
Name: object.Key,
Size: int(object.Size),
LastModified: object.LastModified.Format("2006-01-02 15:04:05"),
})
}
list <- arr
}()
data, ok := <-list
if !ok {
response.Fail(c, nil, "指定的存储桶不存在")
return
}
response.Success(c, data, "查问胜利")
}
services文件夹下创立object.go, 与文件相干的SDK封装. 同样没有用到数据库.
package services
import (
"fmt"
"minio_server/global"
"minio_server/response"
"net/url"
"time"
"github.com/gin-gonic/gin"
"github.com/minio/minio-go/v7"
)
type IObjectService interface {
Get(c *gin.Context)
GetObjectUrl(c *gin.Context)
Stat(c *gin.Context)
Remove(c *gin.Context)
Put(c *gin.Context)
}
type ObjectService struct{}
func NewObjectService() IObjectService {
return &ObjectService{}
}
func (*ObjectService) Get(c *gin.Context) {
bucketName := c.Query("bucket")
objectName := c.Query("object")
if bucketName == "" || objectName == "" {
response.Fail(c, nil, "桶名或文件名谬误")
return
}
reader, err := global.MinioClient.GetObject(c, bucketName, objectName, minio.GetObjectOptions{})
if err != nil {
response.Fail(c, nil, err.Error())
return
}
response.Success(c, nil, "获取文件胜利")
defer reader.Close()
}
// GetObjectUrl godoc
// @Summary 获取文件的url
// @Tags objects
// @Accept json
// @security ApiKeyAuth
// @security ApiKeyXRole
// @Param bucket query string true "存储桶名必传"
// @Param object query string true "文件名必传"
// @Success 200 "{"message": "获取胜利", status: 1}"
// @Failure 400 "{"message": "获取失败", status: -1}"
// @Router /api/object/url [get]
func (*ObjectService) GetObjectUrl(c *gin.Context) {
bucketName := c.Query("bucket")
objectName := c.Query("object")
if bucketName == "" || objectName == "" {
response.Fail(c, nil, "桶名或文件名谬误")
return
}
reqParams := make(url.Values)
fileName := fmt.Sprintf("attachment; filename=\"%s\"", objectName)
reqParams.Set("response-content-disposition", fileName)
presignedURL, err := global.MinioClient.PresignedGetObject(c, bucketName, objectName, time.Duration(1000)*time.Second, reqParams)
if err != nil {
response.Fail(c, nil, err.Error())
return
}
// presignedURL 肯定要string
response.Success(c, presignedURL.String(), "获取文件门路胜利")
}
func (*ObjectService) Stat(c *gin.Context) {
bucketName := c.Query("bucket")
objectName := c.Query("object")
if bucketName == "" || objectName == "" {
response.Fail(c, nil, "请查看传参是否正确")
return
}
stat, err := global.MinioClient.StatObject(c, bucketName, objectName, minio.StatObjectOptions{})
if err != nil {
response.Fail(c, nil, err.Error())
return
}
response.Success(c, stat, "获取文件信息胜利")
}
// Remove godoc
// @Summary 删除文件
// @Tags objects
// @Accept json
// @security ApiKeyAuth
// @security ApiKeyXRole
// @Param bucket query string true "存储桶名必传"
// @Param object query string true "文件名必传"
// @Success 200 "{"message": "删除胜利", status: 1}"
// @Failure 400 "{"message": "删除失败", status: -1}"
// @Router /api/object/remove [post]
func (*ObjectService) Remove(c *gin.Context) {
bucketName := c.Query("bucket")
objectName := c.Query("object")
if bucketName == "" || objectName == "" {
response.Fail(c, nil, "请查看传参是否正确")
return
}
opts := minio.RemoveObjectOptions{
GovernanceBypass: true,
}
err := global.MinioClient.RemoveObject(c, bucketName, objectName, opts)
if err != nil {
response.Fail(c, nil, err.Error())
return
}
response.Success(c, nil, "删除胜利")
}
// Upload godoc
// @Summary 上传文件
// @Tags objects
// @Accept json
// @security ApiKeyAuth
// @security ApiKeyXRole
// @Accept multipart/form-data
// @Param file formData file true "文件名必传"
// @Param bucket formData string true "存储桶名必传"
// @Success 200 "{"message": "删除胜利", status: 1}"
// @Failure 400 "{"message": "删除失败", status: -1}"
// @Router /api/object/upload [post]
func (*ObjectService) Put(c *gin.Context) {
file, _ := c.FormFile("file")
bucket := c.PostForm("bucket")
reader, errFile := file.Open()
if errFile != nil {
response.Fail(c, nil, errFile.Error())
return
}
uoloadInfo, err := global.MinioClient.PutObject(c, bucket, file.Filename, reader, file.Size, minio.PutObjectOptions{ContentType: "application/octet-stream"})
if err != nil {
response.Fail(c, nil, err.Error())
return
}
response.Success(c, uoloadInfo, "文件上传胜利")
}
中间件验证
casbin
创立auth_model.conf
[request_definition]
r = sub, obj, act
[policy_definition]
p = sub, obj, act
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
m = r.sub == p.sub && keyMatch(r.obj, p.obj) && regexMatch(r.act, p.act)
创立casbin.sql, 找个mysql客户端连贯上后面创立好的数据库导进去.
-- Root
-- User
INSERT INTO casbin_rule (p_type,v0,v1,v2,v3,v4,v5) VALUES (
'p','root','/api/user/registered','POST','','','');
INSERT INTO casbin_rule (p_type,v0,v1,v2,v3,v4,v5) VALUES (
'p','root','/api/user/login','POST','','','');
INSERT INTO casbin_rule (p_type,v0,v1,v2,v3,v4,v5) VALUES (
'p','root','/api/user/info','GET','','','');
-- Buckets
INSERT INTO casbin_rule (p_type,v0,v1,v2,v3,v4,v5) VALUES (
'p','root','/api/buckets/list','GET','','','');
INSERT INTO casbin_rule (p_type,v0,v1,v2,v3,v4,v5) VALUES (
'p','root','/api/buckets/exists','GET','','','');
INSERT INTO casbin_rule (p_type,v0,v1,v2,v3,v4,v5) VALUES (
'p','root','/api/buckets/remove','POST','','','');
INSERT INTO casbin_rule (p_type,v0,v1,v2,v3,v4,v5) VALUES (
'p','root','/api/buckets/listobjects','GET','','','');
-- Object
INSERT INTO casbin_rule (p_type,v0,v1,v2,v3,v4,v5) VALUES (
'p','root','/api/object/stat','GET','','','');
INSERT INTO casbin_rule (p_type,v0,v1,v2,v3,v4,v5) VALUES (
'p','root','/api/object/remove','POST','','','');
INSERT INTO casbin_rule (p_type,v0,v1,v2,v3,v4,v5) VALUES (
'p','root','/api/object/upload','POST','','','');
INSERT INTO casbin_rule (p_type,v0,v1,v2,v3,v4,v5) VALUES (
'p','root','/api/object/url','GET','','','');
-- No user registered just reads and writes
-- readwrite
INSERT INTO casbin_rule (p_type,v0,v1,v2,v3,v4,v5) VALUES (
'p','readwrite','/api/user/login','POST','','','');
INSERT INTO casbin_rule (p_type,v0,v1,v2,v3,v4,v5) VALUES (
'p','readwrite','/api/user/info','GET','','','');
-- Buckets
INSERT INTO casbin_rule (p_type,v0,v1,v2,v3,v4,v5) VALUES (
'p','readwrite','/api/buckets/list','GET','','','');
INSERT INTO casbin_rule (ptype,v0,v1,v2,v3,v4,v5) VALUES (
'p','readwrite','/api/buckets/exists','GET','','','');
INSERT INTO casbin_rule (ptype,v0,v1,v2,v3,v4,v5) VALUES (
'p','readwrite','/api/buckets/remove','POST','','','');
INSERT INTO casbin_rule (ptype,v0,v1,v2,v3,v4,v5) VALUES (
'p','readwrite','/api/buckets/listobjects','GET','','','');
-- Object
INSERT INTO casbin_rule (ptype,v0,v1,v2,v3,v4,v5) VALUES (
'p','readwrite','/api/object/stat','GET','','','');
INSERT INTO casbin_rule (ptype,v0,v1,v2,v3,v4,v5) VALUES (
'p','readwrite','/api/object/remove','POST','','','');
INSERT INTO casbin_rule (ptype,v0,v1,v2,v3,v4,v5) VALUES (
'p','readwrite','/api/object/upload','POST','','','');
INSERT INTO casbin_rule (ptype,v0,v1,v2,v3,v4,v5) VALUES (
'p','readwrite','/api/object/url','GET','','','');
-- No user registered just reads and writes
-- read
INSERT INTO casbin_rule (ptype,v0,v1,v2,v3,v4,v5) VALUES (
'p','read','/api/user/login','POST','','','');
INSERT INTO casbin_rule (ptype,v0,v1,v2,v3,v4,v5) VALUES (
'p','read','/api/user/info','GET','','','');
-- Buckets
INSERT INTO casbin_rule (ptype,v0,v1,v2,v3,v4,v5) VALUES (
'p','read','/api/buckets/list','GET','','','');
INSERT INTO casbin_rule (ptype,v0,v1,v2,v3,v4,v5) VALUES (
'p','read','/api/buckets/listobjects','GET','','','');
root、readwrite、read三种权限对应各自能拜访的接口.
不相熟casbin的看这个 文档: https://casbin.org/
封装casbin办法
common文件夹下, 创立casbin.go
package common
import (
"log"
"minio_server/global"
"os"
"github.com/casbin/casbin/v2"
gormadapter "github.com/casbin/gorm-adapter/v3"
"github.com/gin-gonic/gin"
)
//权限构造
type CasbinModel struct {
Ptype string `json:"p_type" bson:"p_type"`
RoleName string `json:"rolename" bson:"v0"`
Path string `json:"path" bson:"v1"`
Method string `json:"method" bson:"v2"`
}
//增加权限
func (c *CasbinModel) AddCasbin(cm CasbinModel) bool {
e := Casbin()
isTrue, _ := e.AddPolicy(cm.RoleName, cm.Path, cm.Method)
return isTrue
}
//长久化到数据库
func Casbin() *casbin.Enforcer {
workDor, _ := os.Getwd()
if global.DB == nil {
log.Fatalln("数据库连贯失败")
}
g, _ := gormadapter.NewAdapterByDB(global.DB)
c, _ := casbin.NewEnforcer(workDor+"/conf/auth_model.conf", g)
c.LoadPolicy()
return c
}
var (
casbins = CasbinModel{}
)
func AddCasbin(c *gin.Context) {
rolename := c.PostForm("rolename")
path := c.PostForm("path")
method := c.PostForm("method")
ptype := "p"
casbin := CasbinModel{
Ptype: ptype,
RoleName: rolename,
Path: path,
Method: method,
}
isok := casbins.AddCasbin(casbin)
if isok {
log.Println("Add Cabin Success")
} else {
log.Println("Add Cabin Error")
}
}
将jwt, casbin封装进验证服务.
创立middleware文件夹, 这里放中间件相干的.
创立auth.go
package middleware
import (
"encoding/base64"
"minio_server/common"
"minio_server/global"
"minio_server/models"
"minio_server/response"
"strings"
"github.com/gin-gonic/gin"
)
// token验证
func Auth() gin.HandlerFunc {
return func(c *gin.Context) {
tokenString := c.GetHeader("Authorization")
if tokenString == "" || !strings.HasPrefix(tokenString, "Bearer") {
response.Unauthorized(c, "权限有余")
c.Abort()
return
}
tokenString = tokenString[7:]
token, claims, err := common.ParseToken(tokenString)
if err != nil || !token.Valid {
response.Unauthorized(c, "权限有余")
c.Abort()
return
}
userId := claims.UserID
var user models.User
if errSearch := global.DB.Table("user").First(&user, userId).Error; errSearch != nil {
response.Fail(c, nil, errSearch.Error())
return
}
c.Set("user", user)
c.Next()
}
}
// casbin权限验证
func AuthCheckRole() gin.HandlerFunc {
return func(c *gin.Context) {
// 获取头部x-role 身份
roleString := c.GetHeader("x-role")
if roleString == "" {
response.Unauthorized(c, "有效身份")
c.Abort()
return
}
// base64 解密
role, err := base64.StdEncoding.DecodeString(roleString)
if err != nil {
response.Unauthorized(c, "有效身份")
c.Abort()
return
}
e := common.Casbin()
//查看权限
res, errRes := e.Enforce(string(role), c.Request.URL.Path, c.Request.Method)
if errRes != nil {
response.Fail(c, nil, errRes.Error())
c.Abort()
return
}
if res {
c.Next()
} else {
response.Unauthorized(c, "权限有余")
c.Abort()
return
}
}
}
创立cors.go, 解决跨域的服务, 没有跨域问题的无需创立.
package middleware
import (
"net/http"
"github.com/gin-gonic/gin"
)
// 解决跨域申请,反对options拜访
func Cors() gin.HandlerFunc {
return func(c *gin.Context) {
method := c.Request.Method
c.Header("Access-Control-Allow-Origin", "*") // *代理容许拜访所有域 正式环境慎用
c.Header( // Authorization token验证, x-role 身份验证.
"Access-Control-Allow-Headers",
"Cache-Control,
Content-Language,
Content-Type,
Expires,
Last-Modified,
Pragma,
Authorization,
x-role"
)
c.Header( //服务器反对的所有跨域申请的办法,为了防止浏览次申请的屡次'预检'申请
"Access-Control-Allow-Methods",
"POST, GET, OPTIONS, PUT, PATCH, DELETE"
)
c.Header( // 首部能够作为响应的一部分裸露给内部
"Access-Control-Expose-Headers",
"Content-Length,
Access-Control-Allow-Origin,
Access-Control-Allow-Headers,
Content-Type"
)
c.Header("Access-Control-Allow-Credentials", "true") // 容许客户端携带验证信息
// 放行OPTIONS办法
if method == "OPTIONS" {
c.AbortWithStatus(http.StatusNoContent)
}
c.Next()
}
}
中间件起到验证作用
api申请–>cors验证–>token验证–>role验证–>casbin权限验证–>返回后果
封装router
创立router文件夹
创立router.go
package router
import (
"minio_server/middleware"
"minio_server/repositories"
"minio_server/services"
_ "minio_server/docs"
"github.com/gin-gonic/gin"
ginSwagger "github.com/swaggo/gin-swagger"
"github.com/swaggo/gin-swagger/swaggerFiles"
)
func CollectRoute(r *gin.Engine) *gin.Engine {
// 注册服务
userRepository := repositories.NewUserManagerRepository("user", nil)
userService := services.NewUserService(userRepository)
bucketsService := services.NewBucketsService()
objectService := services.NewObjectService()
// 全局退出cors验证
r.Use(middleware.Cors())
user := r.Group("/api/user")
{
user.POST("/login", userService.Login)
user.GET("/info", middleware.Auth(), userService.UserInfo)
}
bukets := r.Group("/api/buckets")
// 全组退出token验证
bukets.Use(middleware.Auth())
{
bukets.GET("/list", bucketsService.List)
bukets.GET("/exists", middleware.AuthCheckRole(), bucketsService.Exists)
bukets.POST("/remove", middleware.AuthCheckRole(), bucketsService.Remove)
bukets.GET("/listobjects", bucketsService.ListObjects)
}
object := r.Group("/api/object")
object.Use(middleware.Auth())
{
object.GET("/stat", objectService.Stat)
object.POST("/remove", middleware.AuthCheckRole(), objectService.Remove)
object.POST("/upload", middleware.AuthCheckRole(), objectService.Put)
object.GET("/url", middleware.AuthCheckRole(), objectService.GetObjectUrl)
}
// swagger文档
r.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
return r
}
_ “minio_server/docs” 这块引入会报错, 先疏忽或者正文掉, 上面swagger文件建设后就不会报错了.
不相熟Gin的能够看文档 https://gin-gonic.com/zh-cn/
main.go 批改
package main
import (
"minio_server/initialize"
"minio_server/router"
"github.com/fatih/color"
"github.com/gin-gonic/gin"
)
// @title Swagger Example API
// @version 0.0.1
// @description This is a Minio Server
// @securityDefinitions.apikey ApiKeyAuth
// @in header
// @name Authorization
// @securityDefinitions.apikey ApiKeyXRole
// @in header
// @name x-role
// @host localhost:8082
// @BasePath /
func main() {
// color控制台输入字体色彩, 忽视就好
color.Yellow("mysql & minio =====> Init.....")
// 初始化全局单例
initialize.Init()
color.Yellow("Init end!")
color.Red("=========")
color.Blue("gin service started ======>")
r := gin.Default()
r = router.CollectRoute(r)
r.Run(":8082")
}
Swagger正文文档
models文件夹新建swagger文件夹
创立swagger.go, login登录所需的参数
package swagger
type Login struct {
AccessKey string `json:"access_key"`
SecretKey string `json:"secret_key"`
}
后面的代码有很多swagger正文了.
控制台输出命令:
swag init
根目录会多出docs文件夹,router.go文件对于swagger引入的报错修改了.
地址拜访: http://127.0.0.1(替换地址)/swagger/index.html#/
启动程序
看服务是否可能失常跑起来.
go run main.go
集成部署
上一篇jenkins+webhooks有看吗?对, 又到他们出场了.
首先装置下插件
设置全局工具
抉择你golang开发的版本, 如果主动下载无奈实现, 你可能须要手动装置golang到相干目录.
办法:
// 进入映射的主机目录装置, 同样能够映射到容器内.
// 进入容器
docker exec -it 容器id /bin/bash
// cd到相干目录
cd /var/jenkins_home
// 获取安装包
wget https://go.dev/dl/go1.17.8.linux-amd64.tar.gz
// 解压安装包到当前目录
tar -zxvf go1.17.8.linux-amd64.tar.gz
实现后反复下面的全局工具设置, 记得填对安装包的目录.
设置完后开始测试
根目录创立jenkinsfile
pipeline {
agent {
docker {
image 'golang:1.17.8-stretch'
}
}
stages {
stage('Build') {
steps {
sh "chmod +x -R ${env.WORKSPACE}"
sh 'go env -w GOPROXY=https://goproxy.cn,direct'
sh 'go mod tidy'
}
}
stage('Test') {
steps {
sh 'go test ./utils -count=1 -v'
}
}
}
}
创立工作配置webhooks, 跑测试的过程同上一篇一毛一样. 这里就不反复了.
测试通过如上.
接下来开始部署工作.
根目录创立Dockerfile
FROM alpine:3.15
RUN mkdir -p /app
RUN mkdir /app/logs
RUN mkdir /app/conf
WORKDIR /app
ADD ./dist/main /app/main
ADD ./conf /app/conf
ENV GIN_MODE=release PORT=8082
EXPOSE 8082
CMD ["./main"]
创立构建工作
设置方面同上一篇也是大部分一样, 区别在这里
抉择后面设置好的全局工具
代码如下:
# 进入工作区
cd $WORKSPACE
# 设置代理
export GOPROXY=https://goproxy.cn,direct
# 装置依赖
go mod tidy
# 打印依赖
cat ./go.mod
# 测试
go test ./utils -count=1 -v
# 设置参数打包 依照需要调整为最初部署的环境参数
GOOS=linux CGO_ENABLED=0 go build -o ./dist/main
# docker打包镜像
docker build -t minio_server:$VERSION .
# 删除包
rm -rf main
## stop rm 不是必须
docker stop minio_go_server || true
docker rm minio_go_server -f || true
# 跑容器
docker run -d -p 8082:8082 --name minio_go_server minio_server:$VERSION
# 解决<none>:<none>的垃圾镜像 不是必须
docker rmi $(docker images -f "dangling=true" -q) || true
docker images, docker ps, 查看是否有对应的镜像和容器部署胜利.
接下来用工具测试, 或者swagger文档去测试. 用前篇的前端代码测试也可.
http://127.0.0.1(替换地址)/swagger/index.html#/
swagger登录胜利
前端客户端登录胜利
到这里两篇分享曾经完结.
感激浏览, 如果哪里有谬误或者疑难麻烦评论通知我, 我会及时批改的,谢谢!