gin-vue-admin 框架详解
碎碎念
秋招进入一家游戏公司做golang后台工程师,最近来实习了。导师给了一个课题,要求先熟悉一下gin-vue-admin框架,大概看了一下框架的流程,感觉应该是和renrenfast差不多的逻辑。网上的一些文档都是说怎么去使用的,我这里记录一下gin-vue-admin的启动流程吧(主要讲一下后台逻辑,我也是前端小菜鸡,什么都不懂只会使用的)
知识储备
- gin框架,如何启动web服务及其相关api
- vue框架、element-ui
- docker docker-compose知识
目录结构
项目使用支持使用docker-compose部署,使用go module管理包,包含两个项目server(后台)、web(前端)。当然了,我们也可以不使用容器化的技术,手动部署,前提是把MySQL、redis、node.js、nginx环境搭建好。
├─server (后端文件夹)
│ ├─api (API)
│ ├─config (配置包)
│ ├─core (內核)
│ ├─docs (swagger文档目录)
│ ├─global (全局对象)
│ ├─initialiaze (初始化)
│ ├─middleware (中间件)
│ ├─model (结构体层)
│ ├─resource (资源)
│ ├─router (路由)
│ ├─service (服务)
│ └─utils (公共功能)
└─web (前端文件)
├─public (发布模板)
└─src (源码包)
├─api (向后台发送ajax的封装层)
├─assets (静态文件)
├─components(组件)
├─router (前端路由)
├─store (vuex 状态管理仓)
├─style (通用样式文件)
├─utils (前端工具库)
└─view (前端页面)
项目启动及其使用
github地址:https://github.com/flipped-aurora/gin-vue-admin
github上面有项目的启动及其使用过程,这里不再赘述
后台启动流程,逻辑执行
其实我看到这个项目的使用,感觉和renrenfast是一样的逻辑,只不过renrenfast使用的是springboot
,而这里使用的gin
,仅此而已
这里我先讲一下rerenfast的基本逻辑:
renrenfast使用的是springboot+mybatis+vue+shiro
所集成的一个框架,使用前后端分离技术。springboot
其实就是和ssm一样,使用MVC架构把项目逻辑一层一层划分,使用mybatis
做数据持久化处理。在用户登录的时候,后台给生成一个token返回。往后请求后台的时候,shiro
会把相关的请求进行拦截,验证token,确定角色和权限,再考虑是否进行下去还是拒绝。前端动态展示的页面路由由数据库获取,再在前端动态展示,同时会请求所有的角色信息(权限信息)存储在local storage里面,通过角色信息(权限信息)来判断用户的角色,动态的展示相应展示的页面…
然后就是今天的主角了:gin-vue-admin
server目录下面有一个main.go
文件
注释里面写的很清楚了:
viper
是用来加载配置到全局配置的,我们可以通过global来使用全局的信息,例如mysql、redis等等的西悉尼zap
是用来进行日志记录的,和springboot的log差不多吧,反正就是写日志的gorm
的话感觉和mybatis不太一样,反而和hibernate
差不多,支持自动建表,通过对象来查询- 最后的启动项目关键在于
core.RunWindowsServer()
package main
import (
"gin-vue-admin/core"
"gin-vue-admin/global"
"gin-vue-admin/initialize"
)
// @title Swagger Example API
// @version 0.0.1
// @description This is a sample Server pets
// @securityDefinitions.apikey ApiKeyAuth
// @in header
// @name x-token
// @BasePath /
func main() {
global.GVA_VP = core.Viper() // 初始化Viper
global.GVA_LOG = core.Zap() // 初始化zap日志库
global.GVA_DB = initialize.Gorm() // gorm连接数据库
if global.GVA_DB != nil {
initialize.MysqlTables(global.GVA_DB) // 初始化表
// 程序结束前关闭数据库链接
db, _ := global.GVA_DB.DB()
defer db.Close()
}
core.RunWindowsServer()
}
这里主要就是初始化了redis,路由,配置server信息,然后就是启动服务,下面在看一下Router := initialize.Routers()
是怎么初始化路由的
//core.RunWindowsServer()
func RunWindowsServer() {
if global.GVA_CONFIG.System.UseMultipoint {
// 初始化redis服务
initialize.Redis()
}
Router := initialize.Routers()
Router.Static("/form-generator", "./resource/page")
address := fmt.Sprintf(":%d", global.GVA_CONFIG.System.Addr)
s := initServer(address, Router)
// 保证文本顺序输出
// In order to ensure that the text order output can be deleted
time.Sleep(10 * time.Microsecond)
global.GVA_LOG.Info("server run success on ", zap.String("address", address))
fmt.Printf(`
欢迎使用 Gin-Vue-Admin
当前版本:V2.4.0
加群方式:微信号:shouzi_1994 QQ群:622360840
默认自动化文档地址:http://127.0.0.1%s/swagger/index.html
默认前端文件运行地址:http://127.0.0.1:8080
如果项目让您获得了收益,希望您能请团队喝杯可乐:https://www.gin-vue-admin.com/docs/coffee
`, address)
global.GVA_LOG.Error(s.ListenAndServe().Error())
}
这里就是和我们gin框架一样了,使用gin.Default()
来使用默认的路由,然后就是添加静态资源(用于swagger文档),这里可以看到有global.GVA_LOG.Info("register swagger handler")
这里就是使用了zap来进行日志的输出,使用PrivateGroup := Router.Group("")
来进行路由分组,使用PrivateGroup.Use(middleware.JWTAuth()).Use(middleware.CasbinHandler())
来做类似shiro的鉴权操作,然后就是路由的注册了
// Router := initialize.Routers()
func Routers() *gin.Engine {
var Router = gin.Default()
Router.StaticFS(global.GVA_CONFIG.Local.Path, http.Dir(global.GVA_CONFIG.Local.Path)) // 为用户头像和文件提供静态地址
// Router.Use(middleware.LoadTls()) // 打开就能玩https了
global.GVA_LOG.Info("use middleware logger")
// 跨域
//Router.Use(middleware.Cors()) // 如需跨域可以打开
global.GVA_LOG.Info("use middleware cors")
Router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
global.GVA_LOG.Info("register swagger handler")
// 方便统一添加路由组前缀 多服务器上线使用
PublicGroup := Router.Group("")
{
router.InitBaseRouter(PublicGroup) // 注册基础功能路由 不做鉴权
router.InitInitRouter(PublicGroup) // 自动初始化相关
}
PrivateGroup := Router.Group("")
PrivateGroup.Use(middleware.JWTAuth()).Use(middleware.CasbinHandler())
{
router.InitApiRouter(PrivateGroup) // 注册功能api路由
router.InitJwtRouter(PrivateGroup) // jwt相关路由
router.InitUserRouter(PrivateGroup) // 注册用户路由
router.InitMenuRouter(PrivateGroup) // 注册menu路由
router.InitEmailRouter(PrivateGroup) // 邮件相关路由
router.InitSystemRouter(PrivateGroup) // system相关路由
router.InitCasbinRouter(PrivateGroup) // 权限相关路由
router.InitCustomerRouter(PrivateGroup) // 客户路由
router.InitAutoCodeRouter(PrivateGroup) // 创建自动化代码
router.InitAuthorityRouter(PrivateGroup) // 注册角色路由
router.InitSimpleUploaderRouter(PrivateGroup) // 断点续传(插件版)
router.InitSysDictionaryRouter(PrivateGroup) // 字典管理
router.InitSysOperationRecordRouter(PrivateGroup) // 操作记录
router.InitSysDictionaryDetailRouter(PrivateGroup) // 字典详情管理
router.InitFileUploadAndDownloadRouter(PrivateGroup) // 文件上传下载功能路由
router.InitExcelRouter(PrivateGroup) // 表格导入导出
// Code generated by gin-vue-admin Begin; DO NOT EDIT.
// Code generated by gin-vue-admin End; DO NOT EDIT.
}
global.GVA_LOG.Info("router register success")
return Router
}
我们再来看一下鉴权的逻辑,是使用PrivateGroup.Use(middleware.JWTAuth()).Use(middleware.CasbinHandler())
来做到的
我们可以看到,也是和renrenfast逻辑一样的,先拦截,再判断
// middleware.JWTAuth()
func JWTAuth() gin.HandlerFunc {
return func(c *gin.Context) {
// 我们这里jwt鉴权取头部信息 x-token 登录时回返回token信息 这里前端需要把token存储到cookie或者本地localStorage中 不过需要跟后端协商过期时间 可以约定刷新令牌或者重新登录
token := c.Request.Header.Get("x-token")
if token == "" {
response.FailWithDetailed(gin.H{"reload": true}, "未登录或非法访问", c)
c.Abort()
return
}
if service.IsBlacklist(token) {
response.FailWithDetailed(gin.H{"reload": true}, "您的帐户异地登陆或令牌失效", c)
c.Abort()
return
}
j := NewJWT()
// parseToken 解析token包含的信息
claims, err := j.ParseToken(token)
if err != nil {
if err == TokenExpired {
response.FailWithDetailed(gin.H{"reload": true}, "授权已过期", c)
c.Abort()
return
}
response.FailWithDetailed(gin.H{"reload": true}, err.Error(), c)
c.Abort()
return
}
if err, _ = service.FindUserByUuid(claims.UUID.String()); err != nil {
_ = service.JsonInBlacklist(model.JwtBlacklist{Jwt: token})
response.FailWithDetailed(gin.H{"reload": true}, err.Error(), c)
c.Abort()
}
if claims.ExpiresAt-time.Now().Unix() < claims.BufferTime {
claims.ExpiresAt = time.Now().Unix() + global.GVA_CONFIG.JWT.ExpiresTime
newToken, _ := j.CreateToken(*claims)
newClaims, _ := j.ParseToken(newToken)
c.Header("new-token", newToken)
c.Header("new-expires-at", strconv.FormatInt(newClaims.ExpiresAt, 10))
if global.GVA_CONFIG.System.UseMultipoint {
err, RedisJwtToken := service.GetRedisJWT(newClaims.Username)
if err != nil {
global.GVA_LOG.Error("get redis jwt failed", zap.Any("err", err))
} else { // 当之前的取成功时才进行拉黑操作
_ = service.JsonInBlacklist(model.JwtBlacklist{Jwt: RedisJwtToken})
}
// 无论如何都要记录当前的活跃状态
_ = service.SetRedisJWT(newToken, newClaims.Username)
}
}
c.Set("claims", claims)
c.Next()
}
}
// middleware.CasbinHandler()
// 拦截器
func CasbinHandler() gin.HandlerFunc {
return func(c *gin.Context) {
claims, _ := c.Get("claims")
waitUse := claims.(*request.CustomClaims)
// 获取请求的URI
obj := c.Request.URL.RequestURI()
// 获取请求方法
act := c.Request.Method
// 获取用户的角色
sub := waitUse.AuthorityId
e := service.Casbin()
// 判断策略中是否存在
success, _ := e.Enforce(sub, obj, act)
if global.GVA_CONFIG.System.Env == "develop" || success {
c.Next()
} else {
response.FailWithDetailed(gin.H{}, "权限不足", c)
c.Abort()
return
}
}
}
最后,前端页面的展示应该是通过/server/router/sys_api.go
里面进行注册并且获取展示
func InitApiRouter(Router *gin.RouterGroup) {
ApiRouter := Router.Group("api").Use(middleware.OperationRecord())
{
ApiRouter.POST("createApi", v1.CreateApi) // 创建Api
ApiRouter.POST("deleteApi", v1.DeleteApi) // 删除Api
ApiRouter.POST("getApiList", v1.GetApiList) // 获取Api列表
ApiRouter.POST("getApiById", v1.GetApiById) // 获取单条Api消息
ApiRouter.POST("updateApi", v1.UpdateApi) // 更新api
ApiRouter.POST("getAllApis", v1.GetAllApis) // 获取所有api
ApiRouter.DELETE("deleteApisByIds", v1.DeleteApisByIds) // 删除选中api
}
}
基本的业务逻辑应该就是这样的,然后还有一些分页的框架,一些细节我没认真去看。如果有错误的话,欢迎大家指正,交流学习。