一看就会的集成部署!太简单了(那顺便再撸个存储文件服务吧!|一看就会的集成部署!太简单了?那顺便再撸个存储文件服务吧! (二))
本篇主要介绍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登录成功
文章图片
前端客户端登录成功
到这里两篇分享已经结束.
感谢阅读, 如果哪里有错误或者疑问麻烦评论告诉我, 我会及时修改的,谢谢!
推荐阅读
- WPS图文并茂 一步就能搞定_WPS office
- 一看就会的集成部署!太简单了(那顺便再撸个存储文件服务吧!|一看就会的集成部署!太简单了?那顺便再撸个存储文件服务吧! (一))
- C语言进阶|【C语言进阶7——数组和指针的练习(1) - 学习sizeof 和 strlen,看完这一篇就够了】
- 微信打开几秒就自动关闭
- 选游戏手机就请先认准“神U”!
- 只需2步,就能简单搞定Word中的网格线_Word专区
- 新手机开机了就属于激活了吗
- 华为手机充电到80就充不进去了
- centos7系统部署hexo博客新手入门-进阶,看这一篇就够了
- 看完你就知道win10系统怎样样制作详细说明