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


    快速搭建一个go语言web后端服务脚手架
    源码:https://github.com/weloe/go-web-demo

    web框架使用gin,数据操作使用gorm,访问控制使用casbin

    首先添加一下自定义的middleware

    recover_control.go ,统一处理panic error返回的信息

    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()
    }
    
    

    access_control.go 使用casbin进行访问控制的中间件

    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()
    	}
    }
    
    

    reader.go 读取yaml配置文件的根据类,使用了viter

    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
    	TimeOut  string
    }
    
    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
    }
    
    

    配置文件

    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
    

    persistence.go, gorm,bigcache, casbin 初始化,这里用的casbin是从数据库读取policy

    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&parseTime=True&loc=Local&timeout=%s", username, password, host, port, Dbname, timeout)
    	log.Println("connect db url: " + dbUrl)
    	DB, err = gorm.Open(mysql.Open(dbUrl), &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))
    	}
        
    }
    
    

    到这里准备工作基本完成,我们来写一个通用的 登录,注册,退出 业务吧

    user_handler.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 := &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 := &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"})
    }
    
    

    service.user.go

    这里要注意 注册的时候我们做了两个操作,注册到user表,把policy写入到casbin_rule表,要保证他们要同时成功,所以要用事务

    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(&User{
    		//	Username: register.Username,
    		//	Password: register.Password,
    		//})
    
    		if err != nil || res.RowsAffected < 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)
    	}
    
    }
    

    dao.user.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(&User{}).Where("username = ?", username).First(res)
    	return res
    }
    
    func Insert(username string, password string) (int64, error, int64) {
    	user := &User{Username: username, Password: password}
    	res := component.DB.Create(&user)
    
    	return user.Id, res.Error, res.RowsAffected
    }
    
    

    最后一步,启动web服务,配置路由

    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...")
    }
    
    

    表结构和相关测试数据

    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` */
    
    insert  into `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` */
    
    insert  into `user`(`id`,`username`,`password`,`email`,`phone`) values 
    
    (36,'test1','123',NULL,NULL),
    
    (38,'test2','123',NULL,NULL);
    
  • 相关阅读:
    软考高级 | 系统架构设计师笔记(一)
    领悟《信号与系统》之 傅立叶变换的性质与应用
    git从入门到会用
    Vulhub 靶场使用
    Laravel 模型的关联写入&多对多的关联写入 ⑩③
    【Android】Handler为什么会造成内存泄漏
    WebPack自动吐出脚本
    Kata3.0.0 x LifseaOS x 龙蜥内核三管齐下!带你体验最新的安全容器之旅
    四十九、Hadoop HA部署(MINI版)
    纳什均衡求解器
  • 原文地址:https://www.cnblogs.com/weloe/p/17309521.html