快速搭建一个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);

__EOF__

本文作者秋玻
本文链接https://www.cnblogs.com/weloe/p/17309521.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。您的鼓励是博主的最大动力!
posted @   秋玻  阅读(1631)  评论(8编辑  收藏  举报
相关博文:
阅读排行:
· 【.NET】调用本地 Deepseek 模型
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· DeepSeek “源神”启动!「GitHub 热点速览」
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库
· 我与微信审核的“相爱相杀”看个人小程序副业
点击右上角即可分享
微信分享提示