Bootstrap

gin-vue-admin 框架详解

gin-vue-admin 框架详解

碎碎念

秋招进入一家游戏公司做golang后台工程师,最近来实习了。导师给了一个课题,要求先熟悉一下gin-vue-admin框架,大概看了一下框架的流程,感觉应该是和renrenfast差不多的逻辑。网上的一些文档都是说怎么去使用的,我这里记录一下gin-vue-admin的启动流程吧(主要讲一下后台逻辑,我也是前端小菜鸡,什么都不懂只会使用的)

知识储备

  1. gin框架,如何启动web服务及其相关api
  2. vue框架、element-ui
  3. 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文件

注释里面写的很清楚了:

  1. viper是用来加载配置到全局配置的,我们可以通过global来使用全局的信息,例如mysql、redis等等的西悉尼
  2. zap是用来进行日志记录的,和springboot的log差不多吧,反正就是写日志的
  3. gorm的话感觉和mybatis不太一样,反而和hibernate差不多,支持自动建表,通过对象来查询
  4. 最后的启动项目关键在于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
	}
}

基本的业务逻辑应该就是这样的,然后还有一些分页的框架,一些细节我没认真去看。如果有错误的话,欢迎大家指正,交流学习。

;