接口
API | 说明 | 是否认证、鉴权 |
---|---|---|
/api/v1/login | 登陆 | 无需认证 |
/api/v1/home | 普通、会员均可登陆页面 | 需认证 |
/api/v1/registervip | 普通用户注册会员 | 需认证 |
/api/v1/vip | 仅会员 | 需认证、鉴权 |
逻辑
用户通过login
登陆、成功服务端返回JWT
给用户,并且保存到redis
中,用户访问其它接口首先需通过认证中间件,用户访问vip
接口,触发casbin
RBAC模型鉴权,如果无权限返回错误信息, 用户访问registervip
获取权限,并向viptable
注册会员信息。
数据库
-
mysql
:-
user库
-
user
表 (用户表) -
viptable
表 (通过mysql_event
控制会员时间) -
CREATE EVENT clearVip ON SCHEDULE EVERY 1 MINUTE STARTS '2024-09-27 14:52:30.000' ON COMPLETION NOT PRESERVE DISABLE ON SLAVE DO BEGIN DELETE FROM viptable WHERE created_at + INTERVAL 1 HOUR <= NOW(); END
-
-
rule_db
库casbin_rule
(权限表、保存会员曾注册记录)
-
-
redis
:
保存JWT
信息 key为token:userid
value为JWT
项目结构
│ go.mod
│ go.sum
│ main.go
│
├─.idea
│ .gitignore
│ .name
│ com.mypro.www.iml
│ modules.xml
│ workspace.xml
│
├─aboutJWT
│ JWTHandler.go
│
├─config
│ model.conf
│
├─controller
│ home.go
│ login.go
│ registervip.go
│ vip.go
│
├─db
│ initMysql.go
│ initRedis.go
│
├─middle
│ authHandler.go
│ roleHandler.go
│
├─models
│ model.go
│
└─tools
aboutRole.go
core.go
getUserInfoFromToken.go
strClean.go
│ go.mod
module com.mypro.www
go 1.22.0
require (
github.com/casbin/casbin/v2 v2.100.0
github.com/casbin/gorm-adapter/v3 v3.28.0
github.com/gin-gonic/gin v1.10.0
github.com/golang-jwt/jwt/v5 v5.0.0
github.com/redis/go-redis/v9 v9.6.1
gorm.io/driver/mysql v1.5.7
gorm.io/gorm v1.25.12
)
require (
filippo.io/edwards25519 v1.1.0 // indirect
github.com/bmatcuk/doublestar/v4 v4.6.1 // indirect
github.com/bytedance/sonic v1.12.3 // indirect
github.com/bytedance/sonic/loader v0.2.0 // indirect
github.com/casbin/govaluate v1.2.0 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/cloudwego/base64x v0.1.4 // indirect
github.com/cloudwego/iasm v0.2.0 // indirect
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.5 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/glebarez/go-sqlite v1.20.3 // indirect
github.com/glebarez/sqlite v1.7.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.22.1 // indirect
github.com/go-sql-driver/mysql v1.8.1 // indirect
github.com/goccy/go-json v0.10.3 // indirect
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect
github.com/golang-sql/sqlexp v0.1.0 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
github.com/jackc/pgx/v5 v5.4.3 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.8 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/microsoft/go-mssqldb v1.6.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230126093431-47fa9a501578 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect
golang.org/x/arch v0.10.0 // indirect
golang.org/x/crypto v0.27.0 // indirect
golang.org/x/net v0.29.0 // indirect
golang.org/x/sys v0.25.0 // indirect
golang.org/x/text v0.18.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
gorm.io/driver/postgres v1.5.7 // indirect
gorm.io/driver/sqlserver v1.5.3 // indirect
gorm.io/plugin/dbresolver v1.3.0 // indirect
modernc.org/libc v1.22.2 // indirect
modernc.org/mathutil v1.5.0 // indirect
modernc.org/memory v1.5.0 // indirect
modernc.org/sqlite v1.20.3 // indirect
)
│ go.sum
│ main.go
package main
import (
"com.mypro.www/controller"
"com.mypro.www/middle"
"com.mypro.www/tools"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
r.Use(tools.Cors())
r.POST("/api/v1/login", controller.Login)
protected := r.Group("/")
protected.Use(middle.TokenAuthMiddleware())
protected.GET("/api/v1/home", controller.Home)
protected.GET("/api/v1/vip", middle.RoleMiddleware(), controller.ValidateVip)
protected.GET("/api/v1/registervip", controller.RegisterVip)
_ = r.Run(":8888")
}
├─aboutJWT
│ JWTHandler.go
package aboutJWT
import (
"com.mypro.www/db"
"context"
"errors"
"fmt"
"github.com/golang-jwt/jwt/v5"
"github.com/redis/go-redis/v9"
"time"
)
// 过期时间
const (
accessTokenExpiration = 60 * time.Minute
)
var (
jwtSecretKey = []byte("xhbx")
ctx = context.Background()
redisCli *redis.Client
)
type JwtPayLoad struct {
UserId int `json:"userid"`
UserName string `json:"username"`
}
type CustomClaims struct {
JwtPayLoad
jwt.RegisteredClaims
}
func init() {
redisCli = db.InitRedis()
}
// 生成token
func GenerateTokens(userid int, username string) (string, error) {
// 创建登陆token
accessClaims := CustomClaims{
JwtPayLoad: JwtPayLoad{
UserId: userid,
UserName: username,
},
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(accessTokenExpiration)),
},
}
accessTokenObj := jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims)
accessToken, err := accessTokenObj.SignedString(jwtSecretKey)
if err != nil {
return "", err
}
// 将 token 存储到 Redis 中
err = redisCli.Set(ctx, fmt.Sprintf("token:%d", userid), accessToken, accessTokenExpiration).Err()
if err != nil {
return "", err
}
return accessToken, nil
}
// 解析token
func ValidateToken(tokenString string) (*jwt.Token, error) {
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, errors.New("unexpected signing method")
}
return jwtSecretKey, nil
})
if err != nil {
return nil, err
}
if claims, ok := token.Claims.(*CustomClaims); ok && token.Valid {
if claims.ExpiresAt.Time.Before(time.Now()) {
return nil, errors.New("token has expired")
}
return token, nil
}
return token, nil
}
// 获取redis中存储的token
func GetRedisToken(userid int) (string, error) {
redisToken, err := redisCli.Get(ctx, fmt.Sprintf("token:%d", userid)).Result()
if err != nil {
fmt.Println(err)
return "nil", err
}
return redisToken, nil
}
│
├─config
│ model.conf
[request_definition]
r = sub, obj, act
[policy_definition]
p = sub, obj, act
[policy_effect]
e = some(where (p.eft == allow))
[role_definition]
g = _, _
[matchers]
m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act
│
├─controller
│ home.go
package controller
import (
"github.com/gin-gonic/gin"
)
// 无需casbin鉴权路由
func Home(c *gin.Context) {
c.JSON(200, gin.H{
"code": "200",
"msg": "success",
})
}
│ login.go
package controller
import (
"com.mypro.www/aboutJWT"
"com.mypro.www/db"
"com.mypro.www/models"
"fmt"
"github.com/gin-gonic/gin"
"net/http"
)
func Login(c *gin.Context) {
var userinfo struct {
Username string `json:"username"`
Password string `json:"password"`
}
_ = c.ShouldBindJSON(&userinfo)
if userinfo.Username == "" || userinfo.Password == "" {
c.JSON(http.StatusBadRequest, gin.H{"code": http.StatusUnauthorized, "error": "Invaild username or password"})
return
}
var userRes models.UserInfo
db.GetDB("user").Table("user").Where("username = ?", userinfo.Username).First(&userRes)
if userRes.Password != userinfo.Password {
c.JSON(http.StatusUnauthorized, gin.H{"code": http.StatusUnauthorized, "error": "Invaild username or password"})
return
}
accessToken, err := aboutJWT.GenerateTokens(userRes.Userid, userRes.Username)
if err != nil {
fmt.Println(err)
c.JSON(http.StatusInternalServerError, gin.H{"code": http.StatusInternalServerError, "msg": err.Error()})
return
}
if accessToken == "" {
c.JSON(http.StatusUnauthorized, gin.H{"code": http.StatusUnauthorized, "msg": "access token Generate failed"})
}
c.Header("Authorization", "Bearer "+accessToken)
c.JSON(http.StatusOK, gin.H{
"code": 200,
"status": "login successful",
})
}
│ registervip.go
package controller
import (
"com.mypro.www/tools"
"github.com/gin-gonic/gin"
"net/http"
)
// 注册会员
func RegisterVip(c *gin.Context) {
// 解析JWT,获取用户名
tokenString := c.GetHeader("Authorization")
tokenString = tools.CleanStr(tokenString)
username, _, _ := tools.GetUserInfo(tokenString)
isSuccess, err := tools.GrantRole(username, "vip")
if err != nil {
c.JSON(http.StatusForbidden, gin.H{
"code": http.StatusForbidden,
"msg": err.Error(),
})
return
}
if isSuccess {
c.JSON(http.StatusOK, gin.H{
"code": http.StatusOK,
"msg": "grant VIP success!",
})
return
} else {
c.JSON(http.StatusBadRequest, gin.H{
"code": http.StatusBadRequest,
"msg": "grant VIP failed",
})
}
}
│ vip.go
package controller
import "github.com/gin-gonic/gin"
// 会员
func ValidateVip(c *gin.Context) {
c.JSON(200, gin.H{
"code": 200,
"msg": "欢迎你,vip用户",
})
}
│
├─db
│ initMysql.go
package db
import (
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"time"
)
var (
UserDB *gorm.DB
RuleDB *gorm.DB
err error
)
const (
dsn1 = "dbuser1:NSD2021@tedu.cn@tcp(192.168.40.199:13306)/user?charset=utf8mb4&parseTime=True&loc=Local"
dsn2 = "dbuser1:NSD2021@tedu.cn@tcp(192.168.40.199:13306)/rule_db?charset=utf8mb4&parseTime=True&loc=Local"
)
// 初始化数据库连接
func init() {
var mysqlLogger logger.Interface
// 用户库逻辑
UserDB, err = gorm.Open(mysql.Open(dsn1), &gorm.Config{
Logger: mysqlLogger,
DisableForeignKeyConstraintWhenMigrating: true,
})
if err != nil {
panic(err)
}
configureDB(UserDB)
// 角色库逻辑
RuleDB, err = gorm.Open(mysql.Open(dsn2), &gorm.Config{
Logger: mysqlLogger,
DisableForeignKeyConstraintWhenMigrating: true,
})
if err != nil {
panic(err)
}
configureDB(RuleDB)
}
// 配置数据库连接
func configureDB(db *gorm.DB) {
sqlDB, _ := db.DB()
sqlDB.SetMaxIdleConns(10)
sqlDB.SetMaxOpenConns(100)
sqlDB.SetConnMaxLifetime(time.Hour)
}
// 调用全局的DB连接
func GetDB(dbname string) *gorm.DB {
switch dbname {
case "user":
return UserDB
case "rule":
return RuleDB
default:
return nil
}
}
│ initRedis.go
package db
import (
"fmt"
"github.com/redis/go-redis/v9"
)
var redisConfig = "192.168.40.180:6379"
func InitRedis() *redis.Client {
rdb := redis.NewClient(&redis.Options{
Addr: redisConfig,
})
if rdb == nil {
fmt.Println("Redis init failed. null")
}
return rdb
}
│
├─middle
│ authHandler.go
package middle
import (
"com.mypro.www/aboutJWT"
"com.mypro.www/tools"
"github.com/gin-gonic/gin"
"net/http"
)
// 中间件验证Token
func TokenAuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
// 获取accessToken
tokenString := c.GetHeader("Authorization")
// 如果没有accessToken
if tokenString == "" {
c.JSON(http.StatusUnauthorized, gin.H{"code": 401, "msg": "没有accessToken"})
c.Abort()
return
}
// 删除没用的前缀
tokenString = tools.CleanStr(tokenString)
// 解析下发的token是否有效
token, err := aboutJWT.ValidateToken(tokenString)
if err != nil || !token.Valid {
c.JSON(http.StatusUnauthorized, gin.H{"code": 501, "error": "accessToken不正确或已过期"})
c.Abort()
return
}
_, userid, _ := tools.GetUserInfo(tokenString)
// 获取redis中存储的token
redisToken, err := aboutJWT.GetRedisToken(userid)
// 验证Token在redis中是否已经失效
if tokenString != redisToken {
c.JSON(http.StatusUnauthorized, gin.H{"code": 502, "error": "Token已过期"})
c.Abort()
return
}
c.Next()
}
}
│ roleHandler.go
package middle
import (
"com.mypro.www/db"
"com.mypro.www/models"
"com.mypro.www/tools"
"errors"
"fmt"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
func RoleMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
tokenString := c.GetHeader("Authorization")
tokenString = tools.CleanStr(tokenString)
username, _, _ := tools.GetUserInfo(tokenString)
// 获取用户的请求信息
obj := c.Request.URL.RequestURI()
act := c.Request.Method
// 检查是否有权限访问
isVip := tools.CheckRole(username, obj, act)
// 身份是会员的话
if isVip {
DB := db.GetDB("user")
var vip models.VipUser
tx := DB.Table("viptable").Where("username = ? ", username).First(&vip)
if tx.Error != nil {
if errors.Is(tx.Error, gorm.ErrRecordNotFound) {
// 记录不存在,说明 VIP 可能过期
c.JSON(500, gin.H{
"code": 500,
"msg": "VIP已过期",
})
c.Abort()
return
// 处理过期逻辑
} else {
// 其他错误处理
fmt.Println("查询出错:", tx.Error)
}
} else {
// 找到记录,说明 VIP 仍然有效
fmt.Println("VIP 仍然有效", vip)
// 继续业务逻辑
c.Next()
return
}
}
c.JSON(500, gin.H{
"code": 500,
"msg": "非会员用户请注册会员",
})
c.Abort()
return
}
}
│
├─models
│ model.go
package models
import "time"
type UserInfo struct {
Userid int `json:"userid" db:"userid" gorm:"userid"`
Username string `json:"username" db:"username" gorm:"username"`
Password string `json:"password" db:"password" gorm:"password"`
}
type VipUser struct {
Vid int `json:"vid" db:"vid" gorm:"vid"`
Username string `json:"username" db:"username" gorm:"username"`
CreatedAt time.Time `json:"created_at" db:"created_at" gorm:"createdAt"`
}
│
└─tools
aboutRole.go
package tools
import (
"com.mypro.www/db"
"com.mypro.www/models"
"errors"
"fmt"
"github.com/casbin/casbin/v2"
"github.com/casbin/casbin/v2/model"
gormadapter "github.com/casbin/gorm-adapter/v3"
"gorm.io/gorm"
"sync"
"time"
)
var (
e *casbin.CachedEnforcer
once sync.Once // 确保初始化只执行一次
)
// 刷新权限
func RefreshPolicy(e *casbin.CachedEnforcer) error {
if e == nil {
return errors.New("casbin enforcer is not initialized")
}
// 清空缓存并重新加载策略
e.ClearPolicy()
// 从数据库重新加载权限
return e.LoadPolicy()
}
// CasBin 连接 GormMysql
func initCasbin() (*casbin.CachedEnforcer, error) {
var err error
once.Do(func() {
DB := db.GetDB("rule")
a, err := gormadapter.NewAdapterByDB(DB)
if err != nil {
return
}
m, err := model.NewModelFromFile("./config/model.conf")
if err != nil {
return
}
e, err = casbin.NewCachedEnforcer(m, a)
})
if e == nil {
return nil, err // 确保在发生错误时返回 nil
}
return e, nil
}
// 检查用户有权限访问
func CheckRole(sub, obj, act string) (isVip bool) {
e, err := initCasbin()
if err != nil {
fmt.Println(err)
return false
}
err = RefreshPolicy(e)
if err != nil {
fmt.Println(err)
}
ok, _ := e.Enforce(sub, obj, act)
if ok {
fmt.Printf("%s对%s仍有%s权限", sub, obj, act)
return true
}
fmt.Printf("%s对%s没有%s权限", sub, obj, act)
return false
}
// 授予用户会员身份
func GrantRole(username, role string) (isgrantRole bool, err error) {
e, err := initCasbin()
if err != nil {
fmt.Println(err)
return false, err
}
// 更新用户角色信息
ok, err := e.AddRoleForUser(username, role)
if err != nil {
fmt.Println(err)
return false, err
}
if ok {
err = RefreshPolicy(e)
if err != nil {
fmt.Println(err)
}
if err != nil {
return false, err
}
var vipUser models.VipUser
DB := db.GetDB("user")
// 更新viptable会员表信息,先判断一下vip信息是否存在,如果存在仅续期一个小时,如果不存在再进行数据库写入的添加操作
// 尝试查询用户
if err := DB.Table("viptable").Where("username = ?", username).First(&vipUser).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
// 用户不存在,插入新记录
vipUser.Username = username
vipUser.CreatedAt = time.Now()
if err := DB.Table("viptable").Create(&vipUser).Error; err != nil {
return false, err
}
return true, nil
} else {
// 其他查询错误
return false, err
}
} else {
// 用户已存在,更新 CreatedAt 字段
vipUser.CreatedAt = time.Now()
if err := DB.Table("viptable").Model(&vipUser).Where("username = ?", username).Update("created_at", vipUser.CreatedAt.Add(time.Hour)).Error; err != nil {
return false, err
}
return true, nil // 更新表成功,重新获取VIP
}
} else {
return false, nil
}
}
core.go
package tools
import (
"github.com/gin-gonic/gin"
"net/http"
)
// 解决前后端跨域
func Cors() gin.HandlerFunc {
return func(c *gin.Context) {
method := c.Request.Method
c.Header("Access-Control-Allow-Origin", "*") // 可将将 * 替换为指定的域名
c.Header("Access-Control-Allow-Methods", "*")
c.Header("Access-Control-Allow-Headers", "*")
c.Header("Access-Control-Expose-Headers", "*")
c.Header("Access-Control-Allow-Credentials", "true")
if method == "OPTIONS" {
c.AbortWithStatus(http.StatusNoContent)
}
c.Next()
}
}
getUserInfoFromToken.go
package tools
import (
"com.mypro.www/aboutJWT"
"github.com/golang-jwt/jwt/v5"
)
func GetUserInfo(tokenStr string) (string, int, error) {
// 获取用户JWT里面的用户名和用户id
token, err := aboutJWT.ValidateToken(tokenStr)
if err != nil {
return "", 0, err
}
userClaims, _ := token.Claims.(jwt.MapClaims)
username := userClaims["username"].(string)
useridFloat := userClaims["userid"].(float64)
userid := int(useridFloat)
return username, userid, nil
}
strClean.go
package tools
import "strings"
func CleanStr(str string) string {
str = strings.Trim(str, `"`)
str = strings.TrimPrefix(str, "Bearer ")
return str
}