蔡升荣 發表於 2023-4-12 13:33:00

快速搭建一个go语言web后端服务脚手架

<p>快速搭建一个go语言web后端服务脚手架<br>
源码:https://github.com/weloe/go-web-demo</p>
<p>web框架使用gin,数据操作使用gorm,访问控制使用casbin</p>
<p>首先添加一下自定义的middleware</p>
<p>recover_control.go ,统一处理panic error返回的信息</p>
<pre><code class="language-golang">package middleware

import (
        "fmt"
        "github.com/gin-gonic/gin"
        "go-web-demo/component"
        "log"
        "net/http"
)

func Recover(c *gin.Context) {
        defer func() {
                if r := recover(); r != nil {
                        // print err msg
                        log.Printf("panic: %v\n", r)
                        // debug.PrintStack()
                        // response same struct
                        c.JSON(http.StatusBadRequest, component.RestResponse{Code: -1, Message: fmt.Sprintf("%v", r)})
                }
        }()

        c.Next()
}

</code></pre>
<p>access_control.go 使用casbin进行访问控制的中间件</p>
<pre><code class="language-golang">package middleware

import (
        "fmt"
        "github.com/casbin/casbin/v2"
        gormadapter "github.com/casbin/gorm-adapter/v3"
        "github.com/gin-gonic/gin"
        _ "github.com/go-sql-driver/mysql"
        "go-web-demo/component"
        "log"
        "net/http"
)

// DefaultAuthorize determines if current subject has been authorized to take an action on an object.
func DefaultAuthorize(obj string, act string) gin.HandlerFunc {
        return func(c *gin.Context) {

                // Get current user/subject
                token := c.Request.Header.Get("token")
                if token == "" {
                        c.AbortWithStatusJSON(http.StatusUnauthorized, component.RestResponse{Message: "token is nil"})
                        return
                }
                username, err := component.GlobalCache.Get(token)
                if err != nil || string(username) == "" {
                        log.Println(err)
                        c.AbortWithStatusJSON(http.StatusUnauthorized, component.RestResponse{Message: "user hasn't logged in yet"})
                        return
                }

                // Casbin enforces policy
                ok, err := enforce(string(username), obj, act, component.Enforcer)
                if err != nil {
                        log.Println(err)
                        c.AbortWithStatusJSON(http.StatusInternalServerError, component.RestResponse{Message: "error occurred when authorizing user"})
                        return
                }
                if !ok {
                        c.AbortWithStatusJSON(http.StatusForbidden, component.RestResponse{Message: "forbidden"})
                        return
                }

                c.Next()
        }
}

func enforce(sub string, obj string, act string, enforcer *casbin.Enforcer) (bool, error) {
        // Load policies from DB dynamically
        err := enforcer.LoadPolicy()
        if err != nil {
                return false, fmt.Errorf("failed to load policy from DB: %w", err)
        }
        // Verify
        ok, err := enforcer.Enforce(sub, obj, act)
        return ok, err
}

func AuthorizeAdapterAndModel(obj string, act string, adapter *gormadapter.Adapter, model string) gin.HandlerFunc {
        return func(c *gin.Context) {

                // Get current user/subject
                token := c.Request.Header.Get("token")
                if token == "" {
                        c.AbortWithStatusJSON(401, component.RestResponse{Message: "token is nil"})
                        return
                }
                username, err := component.GlobalCache.Get(token)
                if err != nil || string(username) == "" {
                        log.Println(err)
                        c.AbortWithStatusJSON(401, component.RestResponse{Message: "user hasn't logged in yet"})
                        return
                }

                // Load model configuration file and policy store adapter
                enforcer, err := casbin.NewEnforcer(model, adapter)
                // Casbin enforces policy
                ok, err := enforce(string(username), obj, act, enforcer)

                if err != nil {
                        log.Println(err)
                        c.AbortWithStatusJSON(500, component.RestResponse{Message: "error occurred when authorizing user"})
                        return
                }
                if !ok {
                        c.AbortWithStatusJSON(403, component.RestResponse{Message: "forbidden"})
                        return
                }

                c.Next()
        }
}

</code></pre>
<p>reader.go 读取yaml配置文件的根据类,使用了viter</p>
<pre><code class="language-golang">package config

import (
        "fmt"
        "github.com/spf13/viper"
        "log"
        "sync"
        "time"
)

type Config struct {
        Server   *Server
        Mysql      *DB
        LocalCache *LocalCache
        Casbin   *Casbin
}

type Server struct {
        Port int64
}

type DB struct {
        Username string
        Password string
        Host   string
        Port   int64
        Dbname   string
        TimeOutstring
}

type LocalCache struct {
        ExpireTime time.Duration
}

type Casbin struct {
        Model string
}

var (
        once   sync.Once
        Reader = new(Config)
)

func (config *Config) ReadConfig() *Config {
        once.Do(func() {
                viper.SetConfigName("config")   // filename
                viper.SetConfigType("yaml")   // filename extension : yaml | json |
                viper.AddConfigPath("./config") // workspace dir : ./
                var err error
                err = viper.ReadInConfig() // read config
                if err != nil {            // handler err
                        log.Fatalf(fmt.Sprintf("Fatal error config file: %s \n", err))
                }
                err = viper.Unmarshal(config)
                if err != nil {
                        log.Fatalf(fmt.Sprintf("Fatal error viper unmarshal config: %s \n", err))
                }
        })
        return Reader
}

</code></pre>
<p>配置文件</p>
<pre><code class="language-yml">server:
port: 8080

mysql:
username: root
password: pwd
host: 127.0.0.1
port: 3306
dbname: casbin_demo
timeout: 10s

localCache:
expireTime: 60

casbin:
model: config/rbac_model.conf
</code></pre>
<p>persistence.go, gorm,bigcache, casbin 初始化,这里用的casbin是从数据库读取policy</p>
<pre><code class="language-go">package component

import (
        "fmt"
        "github.com/allegro/bigcache"
        "github.com/casbin/casbin/v2"
        gormadapter "github.com/casbin/gorm-adapter/v3"
        _ "github.com/go-sql-driver/mysql"
        "go-web-demo/config"
        "gorm.io/driver/mysql"
        "gorm.io/gorm"
        "log"
        "time"
)

var (
        DB          *gorm.DB
        GlobalCache *bigcache.BigCache
        Enforcer    *casbin.Enforcer
)

// CreateByConfig create components
func CreateByConfig() {

        ConnectDB()

        CreateLocalCache()

        CreateCasbinEnforcer()
}

func ConnectDB() {
        // connect to DB
        var err error
        dbConfig := config.Reader.ReadConfig().Mysql
        if dbConfig == nil {
                log.Fatalf(fmt.Sprintf("db config is nil"))
        }
        // config
        username := dbConfig.Username
        password := dbConfig.Password
        host := dbConfig.Host
        port := dbConfig.Port
        Dbname := dbConfig.Dbname
        timeout := dbConfig.TimeOut

        dbUrl := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8&amp;parseTime=True&amp;loc=Local&amp;timeout=%s", username, password, host, port, Dbname, timeout)
        log.Println("connect db url: " + dbUrl)
        DB, err = gorm.Open(mysql.Open(dbUrl), &amp;gorm.Config{})

        if err != nil {
                log.Fatalf(fmt.Sprintf("failed to connect to DB: %v", err))
        }
}

func CreateLocalCache() {
        var err error
        cacheConfig := config.Reader.ReadConfig().LocalCache
        if cacheConfig == nil {
                log.Fatalf(fmt.Sprintf("cache config is nil"))
        }
        // Initialize cache to store current user in cache.
        GlobalCache, err = bigcache.NewBigCache(bigcache.DefaultConfig(cacheConfig.ExpireTime * time.Second)) // Set expire time to 30 s
        if err != nil {
                log.Fatalf(fmt.Sprintf("failed to initialize cahce: %v", err))
        }
}

func CreateCasbinEnforcer() {
        var err error

        // casbin model
        config := config.Reader.ReadConfig().Casbin
        if config == nil {
                log.Fatalf(fmt.Sprintf("casbin config is nil"))
        }
        model := config.Model
        //Initialize casbin adapter
        adapter, _ := gormadapter.NewAdapterByDB(DB)

        // Load model configuration file and policy store adapter
        Enforcer, err = casbin.NewEnforcer(model, adapter)
        if err != nil {
                log.Fatalf(fmt.Sprintf("failed to create casbin enforcer: %v", err))
        }
   
}

</code></pre>
<p>到这里准备工作基本完成,我们来写一个通用的 登录,注册,退出 业务吧</p>
<p>user_handler.go</p>
<pre><code class="language-go">package handler

import (
        "fmt"
        "github.com/gin-gonic/gin"
        "github.com/gin-gonic/gin/binding"
        "go-web-demo/component"
        "go-web-demo/handler/request"
        "go-web-demo/service"
        "net/http"
)

func Login(c *gin.Context) {
        loginRequest := &amp;request.Login{}
        err := c.ShouldBindBodyWith(loginRequest, binding.JSON)
        if err != nil {
                panic(fmt.Errorf("request body bind error: %v", err))
        }
        token := service.Login(loginRequest)

        c.JSON(http.StatusOK, component.RestResponse{Code: 1, Data: token, Message: loginRequest.Username + " logged in successfully"})

}

func Logout(c *gin.Context) {
        token := c.Request.Header.Get("token")

        if token == "" {
                panic(fmt.Errorf("token error: token is nil"))
        }

        bytes, err := component.GlobalCache.Get(token)

        if err != nil {
                panic(fmt.Errorf("token error: failed to get username: %v", err))
        }

        username := string(bytes)
        // Authentication

        // Delete store current subject in cache
        err = component.GlobalCache.Delete(token)
        if err != nil {
                panic(fmt.Errorf("failed to delete current subject in cache: %w", err))
        }

        c.JSON(http.StatusOK, component.RestResponse{Code: 1, Data: token, Message: username + " logout in successfully"})
}

func Register(c *gin.Context) {
        register := &amp;request.Register{}
        err := c.ShouldBindBodyWith(register, binding.JSON)
        if err != nil {
                c.JSON(400, component.RestResponse{Code: -1, Message: " bind error"})
                return
        }

        service.Register(register)

        c.JSON(http.StatusOK, component.RestResponse{Code: 1, Data: nil, Message: "register successfully"})
}

</code></pre>
<p>service.user.go</p>
<p>这里要注意 注册的时候我们做了两个操作,注册到user表,把policy写入到casbin_rule表,要保证他们要同时成功,所以要用事务</p>
<pre><code class="language-go">func Login(loginRequest *request.Login) string {
        password := loginRequest.Password
        username := loginRequest.Username

        // Authentication
        user := dao.GetByUsername(username)
        if password != user.Password {
                panic(fmt.Errorf(username + " logged error : password error"))
        }

        // Generate random uuid token
        u, err := uuid.NewRandom()
        if err != nil {
                panic(fmt.Errorf("failed to generate UUID: %w", err))
        }
        // Sprintf token
        token := fmt.Sprintf("%s-%s", u.String(), "token")
        // Store current subject in cache
        err = component.GlobalCache.Set(token, []byte(username))
        if err != nil {
                panic(fmt.Errorf("failed to store current subject in cache: %w", err))
        }
        // Send cache key back to client cookie
        //c.SetCookie("current_subject", token, 30*60, "/resource", "", false, true)
        return token
}

func Register(register *request.Register) {
        var err error
        e := component.Enforcer
        err = e.GetAdapter().(*gormadapter.Adapter).Transaction(e, func(copyEnforcer casbin.IEnforcer) error {
                // Insert to table
                db := copyEnforcer.GetAdapter().(*gormadapter.Adapter).GetDb()
                res := db.Exec("insert into user (username,password) values(?,?)", register.Username, register.Password)

                //User has Username and Password
                //res := db.Table("user").Create(&amp;User{
                //        Username: register.Username,
                //        Password: register.Password,
                //})

                if err != nil || res.RowsAffected &lt; 1 {
                        return fmt.Errorf("insert error: %w", err)
                }

                _, err = copyEnforcer.AddRoleForUser(register.Username, "role::user")
                if err != nil {
                        return fmt.Errorf("add plocy error: %w", err)
                }
                return nil
        })

        if err != nil {
                panic(err)
        }

}
</code></pre>
<p>dao.user.go 对数据库的操作</p>
<pre><code class="language-go">package dao

import "go-web-demo/component"

type User struct {
        Id       int64 `gorm:"primaryKey"`
        Username string
        Password string
        Email    string
        Phone    string
}

func (u *User) TableName() string {
        return "user"
}

func GetByUsername(username string) *User {
        res := new(User)
        component.DB.Model(&amp;User{}).Where("username = ?", username).First(res)
        return res
}

func Insert(username string, password string) (int64, error, int64) {
        user := &amp;User{Username: username, Password: password}
        res := component.DB.Create(&amp;user)

        return user.Id, res.Error, res.RowsAffected
}

</code></pre>
<p>最后一步,启动web服务,配置路由</p>
<pre><code class="language-go">package main

import (
        "fmt"
        "github.com/gin-contrib/cors"
        "github.com/gin-gonic/gin"
        "go-web-demo/component"
        "go-web-demo/config"
        "go-web-demo/handler"
        "go-web-demo/middleware"
        "log"
)

var (
        router *gin.Engine
)

func init() {
        //Initialize components from config yaml: mysql locaCache casbin
        component.CreateByConfig()

        // Initialize gin engine
        router = gin.Default()

        // Initialize gin middleware
        corsConfig := cors.DefaultConfig()
        corsConfig.AllowAllOrigins = true
        corsConfig.AllowCredentials = true
        router.Use(cors.New(corsConfig))
        router.Use(middleware.Recover)

        // Initialize gin router
        user := router.Group("/user")
        {
                user.POST("/login", handler.Login)
                user.POST("/logout", handler.Logout)
                user.POST("/register", handler.Register)
        }

        resource := router.Group("/api")
        {
                resource.Use(middleware.DefaultAuthorize("user::resource", "read-write"))
                resource.GET("/resource", handler.ReadResource)
                resource.POST("/resource", handler.WriteResource)
        }

}

func main() {
        // Start
        port := config.Reader.Server.Port
        err := router.Run(":" + port)
        if err != nil {
                panic(fmt.Sprintf("failed to start gin engine: %v", err))
        }
        log.Println("application is now running...")
}

</code></pre>
<p>表结构和相关测试数据</p>
<pre><code class="language-sql">CREATE DATABASE /*!32312 IF NOT EXISTS*/`casbin_demo` /*!40100 DEFAULT CHARACTER SET utf8 */;

USE `casbin_demo`;

/*Table structure for table `casbin_rule` */

DROP TABLE IF EXISTS `casbin_rule`;

CREATE TABLE `casbin_rule` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`ptype` varchar(100) NOT NULL,
`v0` varchar(100) DEFAULT NULL,
`v1` varchar(100) DEFAULT NULL,
`v2` varchar(100) DEFAULT NULL,
`v3` varchar(100) DEFAULT NULL,
`v4` varchar(100) DEFAULT NULL,
`v5` varchar(100) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `idx_casbin_rule` (`v0`,`v1`,`v2`,`v3`,`v4`,`v5`)
) ENGINE=InnoDB AUTO_INCREMENT=64 DEFAULT CHARSET=utf8;

/*Data for the table `casbin_rule` */

insertinto `casbin_rule`(`id`,`ptype`,`v0`,`v1`,`v2`,`v3`,`v4`,`v5`) values

(3,'p','role::admin','admin::resource','read-write','','',''),

(5,'p','role::user','user::resource','read-write','','',''),

(57,'g','test1','role::user','','','',''),

(59,'g','role::admin','role::user','','','',''),

(63,'g','test2','role::admin',NULL,NULL,NULL,NULL);

/*Table structure for table `user` */

DROP TABLE IF EXISTS `user`;

CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(50) DEFAULT NULL,
`password` varchar(50) DEFAULT NULL,
`email` varchar(50) DEFAULT NULL,
`phone` varchar(50) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=39 DEFAULT CHARSET=utf8;

/*Data for the table `user` */

insertinto `user`(`id`,`username`,`password`,`email`,`phone`) values

(36,'test1','123',NULL,NULL),

(38,'test2','123',NULL,NULL);
</code></pre><br><br>
来源:https://www.cnblogs.com/weloe/p/17309521.html
頁: [1]
查看完整版本: 快速搭建一个go语言web后端服务脚手架