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

打印 上一主题 下一主题

主题 901|帖子 901|积分 2703

快速搭建一个go语言web后端服务脚手架
源码:https://github.com/weloe/go-web-demo
web框架使用gin,数据操作使用gorm,访问控制使用casbin
首先添加一下自定义的middleware
recover_control.go ,统一处理panic error返回的信息
  1. package middleware
  2. import (
  3.         "fmt"
  4.         "github.com/gin-gonic/gin"
  5.         "go-web-demo/component"
  6.         "log"
  7.         "net/http"
  8. )
  9. func Recover(c *gin.Context) {
  10.         defer func() {
  11.                 if r := recover(); r != nil {
  12.                         // print err msg
  13.                         log.Printf("panic: %v\n", r)
  14.                         // debug.PrintStack()
  15.                         // response same struct
  16.                         c.JSON(http.StatusBadRequest, component.RestResponse{Code: -1, Message: fmt.Sprintf("%v", r)})
  17.                 }
  18.         }()
  19.         c.Next()
  20. }
复制代码
access_control.go 使用casbin进行访问控制的中间件
  1. package middleware
  2. import (
  3.         "fmt"
  4.         "github.com/casbin/casbin/v2"
  5.         gormadapter "github.com/casbin/gorm-adapter/v3"
  6.         "github.com/gin-gonic/gin"
  7.         _ "github.com/go-sql-driver/mysql"
  8.         "go-web-demo/component"
  9.         "log"
  10.         "net/http"
  11. )
  12. // DefaultAuthorize determines if current subject has been authorized to take an action on an object.
  13. func DefaultAuthorize(obj string, act string) gin.HandlerFunc {
  14.         return func(c *gin.Context) {
  15.                 // Get current user/subject
  16.                 token := c.Request.Header.Get("token")
  17.                 if token == "" {
  18.                         c.AbortWithStatusJSON(http.StatusUnauthorized, component.RestResponse{Message: "token is nil"})
  19.                         return
  20.                 }
  21.                 username, err := component.GlobalCache.Get(token)
  22.                 if err != nil || string(username) == "" {
  23.                         log.Println(err)
  24.                         c.AbortWithStatusJSON(http.StatusUnauthorized, component.RestResponse{Message: "user hasn't logged in yet"})
  25.                         return
  26.                 }
  27.                 // Casbin enforces policy
  28.                 ok, err := enforce(string(username), obj, act, component.Enforcer)
  29.                 if err != nil {
  30.                         log.Println(err)
  31.                         c.AbortWithStatusJSON(http.StatusInternalServerError, component.RestResponse{Message: "error occurred when authorizing user"})
  32.                         return
  33.                 }
  34.                 if !ok {
  35.                         c.AbortWithStatusJSON(http.StatusForbidden, component.RestResponse{Message: "forbidden"})
  36.                         return
  37.                 }
  38.                 c.Next()
  39.         }
  40. }
  41. func enforce(sub string, obj string, act string, enforcer *casbin.Enforcer) (bool, error) {
  42.         // Load policies from DB dynamically
  43.         err := enforcer.LoadPolicy()
  44.         if err != nil {
  45.                 return false, fmt.Errorf("failed to load policy from DB: %w", err)
  46.         }
  47.         // Verify
  48.         ok, err := enforcer.Enforce(sub, obj, act)
  49.         return ok, err
  50. }
  51. func AuthorizeAdapterAndModel(obj string, act string, adapter *gormadapter.Adapter, model string) gin.HandlerFunc {
  52.         return func(c *gin.Context) {
  53.                 // Get current user/subject
  54.                 token := c.Request.Header.Get("token")
  55.                 if token == "" {
  56.                         c.AbortWithStatusJSON(401, component.RestResponse{Message: "token is nil"})
  57.                         return
  58.                 }
  59.                 username, err := component.GlobalCache.Get(token)
  60.                 if err != nil || string(username) == "" {
  61.                         log.Println(err)
  62.                         c.AbortWithStatusJSON(401, component.RestResponse{Message: "user hasn't logged in yet"})
  63.                         return
  64.                 }
  65.                 // Load model configuration file and policy store adapter
  66.                 enforcer, err := casbin.NewEnforcer(model, adapter)
  67.                 // Casbin enforces policy
  68.                 ok, err := enforce(string(username), obj, act, enforcer)
  69.                 if err != nil {
  70.                         log.Println(err)
  71.                         c.AbortWithStatusJSON(500, component.RestResponse{Message: "error occurred when authorizing user"})
  72.                         return
  73.                 }
  74.                 if !ok {
  75.                         c.AbortWithStatusJSON(403, component.RestResponse{Message: "forbidden"})
  76.                         return
  77.                 }
  78.                 c.Next()
  79.         }
  80. }
复制代码
reader.go 读取yaml配置文件的根据类,使用了viter
  1. package config
  2. import (
  3.         "fmt"
  4.         "github.com/spf13/viper"
  5.         "log"
  6.         "sync"
  7.         "time"
  8. )
  9. type Config struct {
  10.         Server     *Server
  11.         Mysql      *DB
  12.         LocalCache *LocalCache
  13.         Casbin     *Casbin
  14. }
  15. type Server struct {
  16.         Port int64
  17. }
  18. type DB struct {
  19.         Username string
  20.         Password string
  21.         Host     string
  22.         Port     int64
  23.         Dbname   string
  24.         TimeOut  string
  25. }
  26. type LocalCache struct {
  27.         ExpireTime time.Duration
  28. }
  29. type Casbin struct {
  30.         Model string
  31. }
  32. var (
  33.         once   sync.Once
  34.         Reader = new(Config)
  35. )
  36. func (config *Config) ReadConfig() *Config {
  37.         once.Do(func() {
  38.                 viper.SetConfigName("config")   // filename
  39.                 viper.SetConfigType("yaml")     // filename extension : yaml | json |
  40.                 viper.AddConfigPath("./config") // workspace dir : ./
  41.                 var err error
  42.                 err = viper.ReadInConfig() // read config
  43.                 if err != nil {            // handler err
  44.                         log.Fatalf(fmt.Sprintf("Fatal error config file: %s \n", err))
  45.                 }
  46.                 err = viper.Unmarshal(config)
  47.                 if err != nil {
  48.                         log.Fatalf(fmt.Sprintf("Fatal error viper unmarshal config: %s \n", err))
  49.                 }
  50.         })
  51.         return Reader
  52. }
复制代码
配置文件
  1. server:
  2.   port: 8080
  3. mysql:
  4.   username: root
  5.   password: pwd
  6.   host: 127.0.0.1
  7.   port: 3306
  8.   dbname: casbin_demo
  9.   timeout: 10s
  10. localCache:
  11.   expireTime: 60
  12. casbin:
  13.   model: config/rbac_model.conf
复制代码
persistence.go, gorm,bigcache, casbin 初始化,这里用的casbin是从数据库读取policy
  1. package component
  2. import (
  3.         "fmt"
  4.         "github.com/allegro/bigcache"
  5.         "github.com/casbin/casbin/v2"
  6.         gormadapter "github.com/casbin/gorm-adapter/v3"
  7.         _ "github.com/go-sql-driver/mysql"
  8.         "go-web-demo/config"
  9.         "gorm.io/driver/mysql"
  10.         "gorm.io/gorm"
  11.         "log"
  12.         "time"
  13. )
  14. var (
  15.         DB          *gorm.DB
  16.         GlobalCache *bigcache.BigCache
  17.         Enforcer    *casbin.Enforcer
  18. )
  19. // CreateByConfig create components
  20. func CreateByConfig() {
  21.         ConnectDB()
  22.         CreateLocalCache()
  23.         CreateCasbinEnforcer()
  24. }
  25. func ConnectDB() {
  26.         // connect to DB
  27.         var err error
  28.         dbConfig := config.Reader.ReadConfig().Mysql
  29.         if dbConfig == nil {
  30.                 log.Fatalf(fmt.Sprintf("db config is nil"))
  31.         }
  32.         // config
  33.         username := dbConfig.Username
  34.         password := dbConfig.Password
  35.         host := dbConfig.Host
  36.         port := dbConfig.Port
  37.         Dbname := dbConfig.Dbname
  38.         timeout := dbConfig.TimeOut
  39.         dbUrl := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8&parseTime=True&loc=Local&timeout=%s", username, password, host, port, Dbname, timeout)
  40.         log.Println("connect db url: " + dbUrl)
  41.         DB, err = gorm.Open(mysql.Open(dbUrl), &gorm.Config{})
  42.         if err != nil {
  43.                 log.Fatalf(fmt.Sprintf("failed to connect to DB: %v", err))
  44.         }
  45. }
  46. func CreateLocalCache() {
  47.         var err error
  48.         cacheConfig := config.Reader.ReadConfig().LocalCache
  49.         if cacheConfig == nil {
  50.                 log.Fatalf(fmt.Sprintf("cache config is nil"))
  51.         }
  52.         // Initialize cache to store current user in cache.
  53.         GlobalCache, err = bigcache.NewBigCache(bigcache.DefaultConfig(cacheConfig.ExpireTime * time.Second)) // Set expire time to 30 s
  54.         if err != nil {
  55.                 log.Fatalf(fmt.Sprintf("failed to initialize cahce: %v", err))
  56.         }
  57. }
  58. func CreateCasbinEnforcer() {
  59.         var err error
  60.         // casbin model
  61.         config := config.Reader.ReadConfig().Casbin
  62.         if config == nil {
  63.                 log.Fatalf(fmt.Sprintf("casbin config is nil"))
  64.         }
  65.         model := config.Model
  66.         //Initialize casbin adapter
  67.         adapter, _ := gormadapter.NewAdapterByDB(DB)
  68.         // Load model configuration file and policy store adapter
  69.         Enforcer, err = casbin.NewEnforcer(model, adapter)
  70.         if err != nil {
  71.                 log.Fatalf(fmt.Sprintf("failed to create casbin enforcer: %v", err))
  72.         }
  73.    
  74. }
复制代码
到这里准备工作基本完成,我们来写一个通用的 登录,注册,退出 业务吧
user_handler.go
  1. package handler
  2. import (
  3.         "fmt"
  4.         "github.com/gin-gonic/gin"
  5.         "github.com/gin-gonic/gin/binding"
  6.         "go-web-demo/component"
  7.         "go-web-demo/handler/request"
  8.         "go-web-demo/service"
  9.         "net/http"
  10. )
  11. func Login(c *gin.Context) {
  12.         loginRequest := &request.Login{}
  13.         err := c.ShouldBindBodyWith(loginRequest, binding.JSON)
  14.         if err != nil {
  15.                 panic(fmt.Errorf("request body bind error: %v", err))
  16.         }
  17.         token := service.Login(loginRequest)
  18.         c.JSON(http.StatusOK, component.RestResponse{Code: 1, Data: token, Message: loginRequest.Username + " logged in successfully"})
  19. }
  20. func Logout(c *gin.Context) {
  21.         token := c.Request.Header.Get("token")
  22.         if token == "" {
  23.                 panic(fmt.Errorf("token error: token is nil"))
  24.         }
  25.         bytes, err := component.GlobalCache.Get(token)
  26.         if err != nil {
  27.                 panic(fmt.Errorf("token error: failed to get username: %v", err))
  28.         }
  29.         username := string(bytes)
  30.         // Authentication
  31.         // Delete store current subject in cache
  32.         err = component.GlobalCache.Delete(token)
  33.         if err != nil {
  34.                 panic(fmt.Errorf("failed to delete current subject in cache: %w", err))
  35.         }
  36.         c.JSON(http.StatusOK, component.RestResponse{Code: 1, Data: token, Message: username + " logout in successfully"})
  37. }
  38. func Register(c *gin.Context) {
  39.         register := &request.Register{}
  40.         err := c.ShouldBindBodyWith(register, binding.JSON)
  41.         if err != nil {
  42.                 c.JSON(400, component.RestResponse{Code: -1, Message: " bind error"})
  43.                 return
  44.         }
  45.         service.Register(register)
  46.         c.JSON(http.StatusOK, component.RestResponse{Code: 1, Data: nil, Message: "register successfully"})
  47. }
复制代码
service.user.go
这里要注意 注册的时候我们做了两个操作,注册到user表,把policy写入到casbin_rule表,要保证他们要同时成功,所以要用事务
  1. func Login(loginRequest *request.Login) string {
  2.         password := loginRequest.Password
  3.         username := loginRequest.Username
  4.         // Authentication
  5.         user := dao.GetByUsername(username)
  6.         if password != user.Password {
  7.                 panic(fmt.Errorf(username + " logged error : password error"))
  8.         }
  9.         // Generate random uuid token
  10.         u, err := uuid.NewRandom()
  11.         if err != nil {
  12.                 panic(fmt.Errorf("failed to generate UUID: %w", err))
  13.         }
  14.         // Sprintf token
  15.         token := fmt.Sprintf("%s-%s", u.String(), "token")
  16.         // Store current subject in cache
  17.         err = component.GlobalCache.Set(token, []byte(username))
  18.         if err != nil {
  19.                 panic(fmt.Errorf("failed to store current subject in cache: %w", err))
  20.         }
  21.         // Send cache key back to client cookie
  22.         //c.SetCookie("current_subject", token, 30*60, "/resource", "", false, true)
  23.         return token
  24. }
  25. func Register(register *request.Register) {
  26.         var err error
  27.         e := component.Enforcer
  28.         err = e.GetAdapter().(*gormadapter.Adapter).Transaction(e, func(copyEnforcer casbin.IEnforcer) error {
  29.                 // Insert to table
  30.                 db := copyEnforcer.GetAdapter().(*gormadapter.Adapter).GetDb()
  31.                 res := db.Exec("insert into user (username,password) values(?,?)", register.Username, register.Password)
  32.                 //User has Username and Password
  33.                 //res := db.Table("user").Create(&User{
  34.                 //        Username: register.Username,
  35.                 //        Password: register.Password,
  36.                 //})
  37.                 if err != nil || res.RowsAffected < 1 {
  38.                         return fmt.Errorf("insert error: %w", err)
  39.                 }
  40.                 _, err = copyEnforcer.AddRoleForUser(register.Username, "role::user")
  41.                 if err != nil {
  42.                         return fmt.Errorf("add plocy error: %w", err)
  43.                 }
  44.                 return nil
  45.         })
  46.         if err != nil {
  47.                 panic(err)
  48.         }
  49. }
复制代码
dao.user.go 对数据库的操作
  1. package dao
  2. import "go-web-demo/component"
  3. type User struct {
  4.         Id       int64 `gorm:"primaryKey"`
  5.         Username string
  6.         Password string
  7.         Email    string
  8.         Phone    string
  9. }
  10. func (u *User) TableName() string {
  11.         return "user"
  12. }
  13. func GetByUsername(username string) *User {
  14.         res := new(User)
  15.         component.DB.Model(&User{}).Where("username = ?", username).First(res)
  16.         return res
  17. }
  18. func Insert(username string, password string) (int64, error, int64) {
  19.         user := &User{Username: username, Password: password}
  20.         res := component.DB.Create(&user)
  21.         return user.Id, res.Error, res.RowsAffected
  22. }
复制代码
最后一步,启动web服务,配置路由
  1. package main
  2. import (
  3.         "fmt"
  4.         "github.com/gin-contrib/cors"
  5.         "github.com/gin-gonic/gin"
  6.         "go-web-demo/component"
  7.         "go-web-demo/config"
  8.         "go-web-demo/handler"
  9.         "go-web-demo/middleware"
  10.         "log"
  11. )
  12. var (
  13.         router *gin.Engine
  14. )
  15. func init() {
  16.         //Initialize components from config yaml: mysql locaCache casbin
  17.         component.CreateByConfig()
  18.         // Initialize gin engine
  19.         router = gin.Default()
  20.         // Initialize gin middleware
  21.         corsConfig := cors.DefaultConfig()
  22.         corsConfig.AllowAllOrigins = true
  23.         corsConfig.AllowCredentials = true
  24.         router.Use(cors.New(corsConfig))
  25.         router.Use(middleware.Recover)
  26.         // Initialize gin router
  27.         user := router.Group("/user")
  28.         {
  29.                 user.POST("/login", handler.Login)
  30.                 user.POST("/logout", handler.Logout)
  31.                 user.POST("/register", handler.Register)
  32.         }
  33.         resource := router.Group("/api")
  34.         {
  35.                 resource.Use(middleware.DefaultAuthorize("user::resource", "read-write"))
  36.                 resource.GET("/resource", handler.ReadResource)
  37.                 resource.POST("/resource", handler.WriteResource)
  38.         }
  39. }
  40. func main() {
  41.         // Start
  42.         port := config.Reader.Server.Port
  43.         err := router.Run(":" + port)
  44.         if err != nil {
  45.                 panic(fmt.Sprintf("failed to start gin engine: %v", err))
  46.         }
  47.         log.Println("application is now running...")
  48. }
复制代码
表结构和相关测试数据
  1. CREATE DATABASE /*!32312 IF NOT EXISTS*/`casbin_demo` /*!40100 DEFAULT CHARACTER SET utf8 */;
  2. USE `casbin_demo`;
  3. /*Table structure for table `casbin_rule` */
  4. DROP TABLE IF EXISTS `casbin_rule`;
  5. CREATE TABLE `casbin_rule` (
  6.   `id` bigint(20) NOT NULL AUTO_INCREMENT,
  7.   `ptype` varchar(100) NOT NULL,
  8.   `v0` varchar(100) DEFAULT NULL,
  9.   `v1` varchar(100) DEFAULT NULL,
  10.   `v2` varchar(100) DEFAULT NULL,
  11.   `v3` varchar(100) DEFAULT NULL,
  12.   `v4` varchar(100) DEFAULT NULL,
  13.   `v5` varchar(100) DEFAULT NULL,
  14.   PRIMARY KEY (`id`),
  15.   UNIQUE KEY `idx_casbin_rule` (`v0`,`v1`,`v2`,`v3`,`v4`,`v5`)
  16. ) ENGINE=InnoDB AUTO_INCREMENT=64 DEFAULT CHARSET=utf8;
  17. /*Data for the table `casbin_rule` */
  18. insert  into `casbin_rule`(`id`,`ptype`,`v0`,`v1`,`v2`,`v3`,`v4`,`v5`) values
  19. (3,'p','role::admin','admin::resource','read-write','','',''),
  20. (5,'p','role::user','user::resource','read-write','','',''),
  21. (57,'g','test1','role::user','','','',''),
  22. (59,'g','role::admin','role::user','','','',''),
  23. (63,'g','test2','role::admin',NULL,NULL,NULL,NULL);
  24. /*Table structure for table `user` */
  25. DROP TABLE IF EXISTS `user`;
  26. CREATE TABLE `user` (
  27.   `id` int(11) NOT NULL AUTO_INCREMENT,
  28.   `username` varchar(50) DEFAULT NULL,
  29.   `password` varchar(50) DEFAULT NULL,
  30.   `email` varchar(50) DEFAULT NULL,
  31.   `phone` varchar(50) DEFAULT NULL,
  32.   PRIMARY KEY (`id`)
  33. ) ENGINE=InnoDB AUTO_INCREMENT=39 DEFAULT CHARSET=utf8;
  34. /*Data for the table `user` */
  35. insert  into `user`(`id`,`username`,`password`,`email`,`phone`) values
  36. (36,'test1','123',NULL,NULL),
  37. (38,'test2','123',NULL,NULL);
复制代码
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

立聪堂德州十三局店

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表