快速搭建一个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: %vn", 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); 

发表评论

评论已关闭。

相关文章