(三)后台管理系统鉴权处理

后台管理系统鉴权处理

一、JWT

  • 查找资源
  • 获取token
  • 解析token
  • 获取用户信息
// 基于 JWT 的授权
// 如果存在 session, 则直接从 session 中获取用户信息
// 如果不存在 session, 则从 Authorization 中获取 token, 并解析 token 获取用户信息, 并设置到 session 中
func JWTAuth() gin.HandlerFunc {
	return func(c *gin.Context) {
		db := c.MustGet(g.CTX_DB).(*gorm.DB)
		// 系统管理的资源需要做验证, 没有加进来的不需要
		// 去除路劲前面的/api 获取后边的路径和Method,然后在数据库里面查询是否存在这个资源
		url, method := c.FullPath()[4:], c.Request.Method
		resource, err := model.GetResource(db, url, method)
		//func GetResource(db *gorm.DB, uri, method string) (resource Resource, err error) {
		//	result := db.Where(&Resource{Url: uri, Method: method}).First(&resource)
		//	return resource, result.Error
		//}
		if err != nil {
			// 没有找到的资源, 直接跳过后续验证
			if errors.Is(err, gorm.ErrRecordNotFound) {
				//slog.Debug("[middleware-JWTAuth] resource not exist, skip jwt auth")
				fmt.Println(err)
				c.Set("skip_check", true)
				c.Next()
				c.Set("skip_check", false)
				return
			}
			handle.ReturnError(c, g.ErrDbOp, err)
			return
		}
		// 匿名资源, 直接跳过后续验证
		if resource.Anonymous {
			slog.Debug(fmt.Sprintf("[middleware-JWTAuth] resource: %s %s is anonymous, skip jwt auth!", url, method))
			c.Set("skip_check", true)
			c.Next()
			c.Set("skip_check", false)
			return
		}
		// 获取 token
		authorization := c.Request.Header.Get("Authorization")
		if authorization == "" {
			handle.ReturnError(c, g.ErrTokenNotExist, nil)
			return
		}
		// token 的正确格式: `Bearer [tokenString]`
		parts := strings.Split(authorization, " ")
		if len(parts) != 2 || parts[0] != "Bearer" {
			handle.ReturnError(c, g.ErrTokenType, nil)
			return
		}
		// 解析 token
		claims, err := jwt.ParseToken(g.Conf.JWT.Secret, parts[1])
		if err != nil {
			handle.ReturnError(c, g.ErrTokenWrong, err)
			return
		}
		// 判断 token 已过期
		if time.Now().Unix() > claims.ExpiresAt.Unix() {
			handle.ReturnError(c, g.ErrTokenRuntime, nil)
			return
		}
		// 获取用户信息
		user, err := model.GetUserAuthInfoById(db, claims.UserId)
		if err != nil {
			handle.ReturnError(c, g.ErrUserNotExist, err)
			return
		}
		// session
		session := sessions.Default(c)
		session.Set(g.CTX_USER_AUTH, claims.UserId)
		session.Save()
		// gin context
		c.Set(g.CTX_USER_AUTH, user)
	}
}

二、资源访问权限验证

// 资源访问权限验证
func PermissionCheck() gin.HandlerFunc {
	return func(c *gin.Context) {
		if c.GetBool("skip_check") {
			c.Next()
			return
		}
		db := c.MustGet(g.CTX_DB).(*gorm.DB)
		auth, err := handle.CurrentUserAuth(c)
		if err != nil {
			handle.ReturnError(c, g.ErrUserNotExist, err)
			return
		}
		if auth.IsSuper {
			//slog.Debug("[middleware-PermissionCheck]: super admin no need to check, pass!")
			c.Next()
			return
		}
		url := c.FullPath()[4:]
		method := c.Request.Method
		//slog.Debug(fmt.Sprintf("[middleware-PermissionCheck] %v, %v, %v\n", auth.Username, url, method))
		for _, role := range auth.Roles {
			//slog.Debug(fmt.Sprintf("[middleware-PermissionCheck] %v\n", role.Name))
			pass, err := model.CheckRoleAuth(db, role.ID, url, method)
			if err != nil {
				handle.ReturnError(c, g.ErrDbOp, err)
				return
			}
			if !pass {
				handle.ReturnError(c, g.ErrPermission, nil)
				return
			}
		}
		//slog.Debug("[middleware-PermissionCheck]: pass")
		c.Next()
	}
}

三、记录日志

// 记录操作日志中间件
func OperationLog() gin.HandlerFunc {
	return func(c *gin.Context) {
		// TODO: 记录文件上传
		// 不记录 GET 请求操作记录 (太多了) 和 文件上传操作记录 (请求体太长)
		// strings.Contains(c.Request.RequestURI,"upload") 如果请求的 URI 包含 "upload" 字符串,则条件为真,否则为假
		if c.Request.Method != "GET" && !strings.Contains(c.Request.RequestURI, "upload") {
			// 自定义响应写入器
			blw := &CustomResponseWriter{
				body:           bytes.NewBufferString(""),
				ResponseWriter: c.Writer,
			}
			c.Writer = blw
			// 获取当前登录用户信息
			auth, _ := handle.CurrentUserAuth(c)

			body, _ := io.ReadAll(c.Request.Body)
			c.Request.Body = io.NopCloser(bytes.NewBuffer(body))

			ipAddress := utils.IP.GetIpAddress(c)
			ipSource := utils.IP.GetIpSource(ipAddress)

			moduleName := getOptResource(c.HandlerName())
			operationLog := model.OperationLog{
				OptModule:     moduleName, // TODO: 优化
				OptType:       GetOptString(c.Request.Method),
				OptUrl:        c.Request.RequestURI,
				OptMethod:     c.HandlerName(),
				OptDesc:       GetOptString(c.Request.Method) + moduleName, // TODO: 优化
				RequestParam:  string(body),
				RequestMethod: c.Request.Method,
				UserId:        auth.UserInfoId,
				Nickname:      auth.UserInfo.Nickname,
				IpAddress:     ipAddress,
				IpSource:      ipSource,
			}
			c.Next()
			operationLog.ResponseData = blw.body.String() // 从缓存中获取响应体内容

			db := c.MustGet(g2.CTX_DB).(*gorm.DB)
			if err := db.Create(&operationLog).Error; err != nil {
				slog.Error("操作日志记录失败: ", err)
				handle.ReturnError(c, g2.ErrDbOp, err)
				return
			}
		} else {
			c.Next()
		}
	}
}

四、实时监听用户

// 监听在线状态中间件
// 登录时: 移除用户的强制下线标记
// 退出登录时: 添加用户的在线标记
func ListenOnline() gin.HandlerFunc {
	return func(c *gin.Context) {
		ctx := context.Background()
		rdb := c.MustGet(g2.CTX_RDB).(*redis.Client)
		auth, err := handle.CurrentUserAuth(c)
		if err != nil {
			handle.ReturnError(c, g2.ErrUserAuth, err)
			return
		}
		onlineKey := g2.ONLINE_USER + strconv.Itoa(auth.ID)
		offlineKey := g2.OFFLINE_USER + strconv.Itoa(auth.ID)
		// 判断当前用户是否被强制下线
		if rdb.Exists(ctx, offlineKey).Val() == 1 {
			fmt.Println("用户被强制下线")
			handle.ReturnError(c, g2.ErrForceOffline, nil)
			c.Abort()
			return
		}
		// 每次发送请求会更新 Redis 中的在线状态: 重新计算 10 分钟
		rdb.Set(ctx, onlineKey, auth, 10*time.Minute)
		c.Next()
	}
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

席万里

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值