3

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

 1 year ago
source link: https://www.cnblogs.com/weloe/p/17309521.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK