快速搭建一个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);
复制代码 免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |