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

科技资讯 投稿 5400 0 评论

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

源码:https://github.com/weloe/go-web-demo

首先添加一下自定义的middleware

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

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

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

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;

编程笔记 » 快速搭建一个go语言web后端服务脚手架

赞同 (32) or 分享 (0)
游客 发表我的评论   换个身份
取消评论

表情
(0)个小伙伴在吐槽