源码:https://github.com/weloe/go-web-demo
首先添加一下自定义的middleware
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
}
}
到这里准备工作基本完成,我们来写一个通用的 登录,注册,退出 业务吧
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
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;