go-gin中session实现redis前缀和db库选择+单点登录
分别实现了redigo中自动加前缀和session中自动加前缀
等有空了整理一个demo放到github上,到时候求个小星星
- 在gin-contrib/sessions/redis库中redis的前缀是被封装起来了,所以自定义前缀没有内部方法
- 在这里我们自己实现一下NewStoreWithDBPrefix方法
- 配置文件可以看到引用的redis配置
- 这样方面多个项目在同一个redis中方便管理
- 也方便做单点登录
项目目录
app/
├── common/
│ ├── global/
│ ├──── global.go
├── config/
│ ├── application.yaml
├── initialize/
│ ├── config.go
│ ├── initialize.go
│ ├── redis.go
│ ├── store.go
├── middleware/
│ ├── middleware.go
├── router/
│ ├── router.go
├── main.go
下面是代码实现
配置文件:config/application.yaml
redis: &redisConfigaddr: 127.0.0.1port: 6379password: 123456db: 3size: 20max-idle: 24active: 10auth: trueprefix: demotimeout: 180
store:size: 10redis: *redisConfigkey-pairs: sessionKey
全局变量:common/global/global.go
package globalimport ("demo/config""github.com/gin-contrib/sessions""github.com/robfig/cron/v3""github.com/garyburd/redigo/redis"ut "github.com/go-playground/universal-translator""github.com/jordan-wright/email""go.uber.org/zap""gorm.io/gorm"
)var (CONFIG config.ConfigDB *gorm.DBTrans *ut.TranslatorREDISPoll *redis.PoolRootDir stringEmailPool *email.PoolStore *sessions.StoreLogger *zap.SugaredLoggerCron *cron.Cron
)
配置文件初始化:initialize/config.go
package initializeimport ("fmt""io/ioutil""log""os""path/filepath""strings""demo/common/global""demo/config""github.com/joho/godotenv""gopkg.in/yaml.v2"
)/*** 读取配置文件,最先被初始化*/func InitilizeConfig() {// 获取当前工作目录rootDir, err := os.Getwd()if err != nil {log.Fatalf("Failed to get current working directory: %s", err)}// 这是单元测试if strings.HasSuffix(rootDir, "service") || strings.HasSuffix(rootDir, "command") || strings.HasSuffix(rootDir, "util") || strings.HasSuffix(rootDir, "model") {rootDir = filepath.Dir(rootDir)}if strings.HasSuffix(rootDir, "oss") {rootDir = filepath.Dir(filepath.Dir(rootDir))}// rootDir = "/Users/dupeisheng.vendor/go/src/gitlab.bj.sensetime.com/entry_manage"// 设置工作目录if err := os.Chdir(rootDir); err != nil {log.Fatalf("Failed to change working directory: %s", err)}fmt.Println("工作目录:", rootDir)// 加载.envgodotenv.Load(".env")// 设置当前环境mode := os.Getenv("Mode")fmt.Println("当前环境:", mode)if mode != "" {mode = "-" + mode}// 加载配置文件configPath := rootDir + "/config/application" + mode + ".yaml"yamlFile, err := ioutil.ReadFile(configPath)if err != nil {log.Panicf("Failed to read file , cause is %s", err.Error())}config := config.Config{}err = yaml.Unmarshal(yamlFile, &config)global.CONFIG = configif err != nil {fmt.Println(err.Error())}
}
session初始化:initialize/store.go
package initializeimport ("fmt""demo/common/global""github.com/boj/redistore""github.com/gin-contrib/sessions""github.com/gin-contrib/sessions/redis""github.com/spf13/cast"
)const SessionMaxAge = 8 * 3600func InitilizeStore() {var Store sessions.StoreStore, err := NewStoreWithDBPrefix(global.CONFIG.Store.Size,"tcp",global.CONFIG.Store.Redis.Addr+":"+global.CONFIG.Store.Redis.Port,global.CONFIG.Store.Redis.Password,cast.ToString(global.CONFIG.Store.Redis.Db),// []byte(global.CONFIG.Store.Redis.Prefix),[]byte(global.CONFIG.Store.KeyPairs),)if err != nil {fmt.Println("创建Session-Redis存储失败" + err.Error())}Store.Options(sessions.Options{MaxAge: SessionMaxAge,})global.Store = &Store
}type store struct {*redistore.RediStore
}func NewStoreWithDBPrefix(size int, network, address, password, DB string, keyPairs ...[]byte) (redis.Store, error) {s, err := redistore.NewRediStoreWithDB(size, network, address, password, DB, keyPairs...)if err != nil {return nil, err}// 这里设置前缀s.SetKeyPrefix(global.CONFIG.Store.Redis.Prefix + ":session:")return &store{s}, nil
}func (c *store) Options(options sessions.Options) {c.RediStore.Options = options.ToGorillaOptions()
}
redis初始化:initialize/redis.go
package initializeimport ("fmt""log""strings""time""demo/common/global""demo/config""github.com/garyburd/redigo/redis""github.com/samber/lo""github.com/spf13/cast"
)/*** 初始化redis,并赋值给全局变量*/func InitilizeRedis() {// 创建redis连接池global.REDISPoll = GetRedisPool(global.CONFIG)
}func GetRedisPool(config config.Config) *redis.Pool {return &redis.Pool{MaxIdle: config.Redis.MaxIdle, // 最大空闲连接数MaxActive: config.Redis.Active, // 最大连接数IdleTimeout: time.Duration(config.Redis.Timeout) * time.Second,Wait: true, // 超过连接数后是否等待Dial: func() (redis.Conn, error) {redisUri := fmt.Sprintf("%s:%s", config.Redis.Addr, config.Redis.Port)var redisConn redis.Connvar err errorif config.Redis.Auth {redisConn, err = redis.Dial("tcp", redisUri, redis.DialPassword(config.Redis.Password))} else {redisConn, err = redis.Dial("tcp", redisUri)}if err != nil {log.Println("获取连接失败:" + err.Error())return nil, err}// 添加 Redis 前缀if config.Redis.Prefix != "" {redisConn = &PrefixedConn{Conn: redisConn, config: config.Redis}}return redisConn, nil},TestOnBorrow: func(c redis.Conn, t time.Time) error {if time.Since(t) < time.Minute {return nil}_, err := c.Do("PING")return err},}
}// PrefixedConn 是一个实现了 redis.Conn 接口的自定义结构体,它在键名前添加了前缀
type PrefixedConn struct {redis.Connconfig config.Redis
}// 允许适配前缀的命令
var commandsWithPrefix = []string{"GET", "SET", "EXISTS", "DEL", "TYPE","RPUSH", "LPOP", "RPOP", "LLEN", "LRANGE","SADD", "SREM", "SISMEMBER", "SMEMBERS", "SCARD","HSET", "HMSET", "HGET", "HGETALL","ZADD", "ZRANGE", "ZRANGEBYSCORE", "ZREVRANGEBYSCORE", "ZREM","INCR", "INCRBY","WATCH", "MULTI", "EXEC", "EXPIRE",
}// Do 实现了 redis.Conn 接口的 Do 方法
func (c *PrefixedConn) Do(command string, args ...any) (any, error) {// 执行命令前切换到数据库if _, err := c.Conn.Do("SELECT", c.config.Db); err != nil {return nil, err}command = strings.ToUpper(command)// 判断是不是单独实现了命令 不用反射吧-性能问题switch command {case "MGET":return c.MGET(args...)case "MSET":return c.MSET(args...)}// 加前缀if len(args) > 0 {key := args[0].(string)if lo.IndexOf[string](commandsWithPrefix, command) != -1 && key != "" {args[0] = c.config.Prefix + ":" + key}}return c.Conn.Do(command, args...)
}// MGET 实现了批量获取命令
func (c *PrefixedConn) MGET(args ...any) (interface{}, error) {for i := range args {args[i] = c.config.Prefix + ":" + cast.ToString(args[i])}return c.Conn.Do("MGET", args...)
}// MSET 实现了批量设置命令
func (c *PrefixedConn) MSET(args ...any) (interface{}, error) {for i := range args {if i%2 == 0 {args[i] = c.config.Prefix + ":" + cast.ToString(args[i])}}return c.Conn.Do("MSET", args...)
}
gin路由:router/router.go
package routerimport ("github.com/gin-contrib/cors""github.com/gin-contrib/sessions""github.com/gin-gonic/gin"
)
// InitRouters 初始化路由
func InitRouters() *gin.Engine {// 创建默认带中间件的路由 Logger、Recoveryr := gin.Default()// 允许跨域r.Use(cors.Default())r.Use(sessions.Sessions("sessionID", *global.Store))
}
下面是单点登录实现逻辑
middleware/middleware.go
package middlewareimport ("encoding/json""net/http"myerror "demo/common/error""demo/common/global""demo/model""demo/util""github.com/gin-contrib/sessions""github.com/gin-gonic/gin""github.com/gomodule/redigo/redis""github.com/spf13/cast"
)const UserSessionExpireDuration = 8 * 3600
const UserSessionMapPrefix = "session:user:"type SessionData struct {Member model.Member `json:"member"`
}// 保存登录信息
func GenerateSession(c *gin.Context, sessionData SessionData) error {c.Request.Header.Set("Cookie", "")redisConn := global.REDISPoll.Get()defer redisConn.Close()sessionKey := UserSessionMapPrefix + cast.ToString(sessionData.Member.ID)// 单点登录删除旧的登录态oldSessionID, err := redis.String(redisConn.Do("get", sessionKey))if err != nil && err != redis.ErrNil {return err}_, err = redisConn.Do("del", oldSessionID)if err != nil {return err}// 保存用户相关token := cast.ToString(sessionData.Member.ID) + sessionData.Member.Name + util.Md5(util.GetUUID()+util.RandString(20))session := sessions.Default(c)by, err := json.Marshal(sessionData)if err != nil {return err}session.Set("authenticated", true)session.Set("data", by)session.Set("member_id", sessionData.Member.ID)session.Set("member_name", sessionData.Member.Name)session.Set("token", token)session.Save()sessionVal := "session:" + session.ID()_, err = redisConn.Do("set", sessionKey, sessionVal, "EX", UserSessionExpireDuration)return err
}// 身份验证
func Authentication(ctx *gin.Context) {session := sessions.Default(ctx)if auth, ok := session.Get("authenticated").(bool); !ok || !auth {ctx.JSON(http.StatusUnauthorized, gin.H{"code": myerror.SERVER_UNAUTHORIZED_ERROR,"msg": myerror.SERVER_UNAUTHORIZED_ERROR.String(),})ctx.Abort()return}session.Save()// 保存用户相关信息到ctxmemberId := session.Get("member_id")memberName := session.Get("member_name")data := session.Get("data")var sessionData SessionDataif data != nil {by := data.([]byte)err := json.Unmarshal(by, &sessionData)if err != nil {ctx.JSON(http.StatusUnauthorized, gin.H{"code": myerror.SERVER_SYSTEM_ERROR,"msg": myerror.SERVER_SYSTEM_ERROR.String(),})ctx.Abort()return}}ctx.Set("member_id", memberId)ctx.Set("member_name", memberName)ctx.Set("member_info", sessionData.Member)ctx.Set("data", data)// 登录续时redisConn := global.REDISPoll.Get()defer redisConn.Close()sessionKey := UserSessionMapPrefix + cast.ToString(memberId)sessionVal := "session:" + session.ID()redisConn.Do("set", sessionKey, sessionVal, "EX", UserSessionExpireDuration)ctx.Next()
}
相关文章:

go-gin中session实现redis前缀和db库选择+单点登录
分别实现了redigo中自动加前缀和session中自动加前缀 等有空了整理一个demo放到github上,到时候求个小星星 在gin-contrib/sessions/redis库中redis的前缀是被封装起来了,所以自定义前缀没有内部方法在这里我们自己实现一下NewStoreWithDBPrefix方法配…...

python-双胞胎字符串
[问题描述]:给定两个字符串s和t,每次可以任意交换s的奇数位和偶数位的字符,即奇数位的字符可以与任意其它奇数位的字符交换,偶数位的字符同样也可以与任意偶数位的字符的字符交换,问能否在有限的次数的交换下使s变为t?…...

万字长文,小白新手怎么开始做YOLO实验,从零开始教!整体思路在这里,科研指南针!
最近专栏来了很多的新手小白,对科研实验的过程感到困惑和无从下手,这篇文章就来讲解一下整体的科研流程,从选择数据集到发表论文的各个步骤,并针对大家在实验中常犯的错误进行解答。并且为大家提供通向我其他相关博客的指引&#…...

MDR-1A用什么前端:深度解析与实用指南
MDR-1A用什么前端:深度解析与实用指南 索尼MDR-1A作为一款备受瞩目的音乐耳机,其音质表现与前端设备的搭配息息相关。那么,MDR-1A用什么前端才能达到最佳的音效体验呢?本文将从四个方面、五个方面、六个方面和七个方面进行深入探…...

01Linux以及操作系统概述
课程目标 1.了解现代操作系统的整体构成及发展历史 2.了解Linux操作系统及其分支版本 3.直观上理解服务器端与桌面端版本的区别 课程实验 1.通过对CentOS和Ubuntu的演示,直观理解Linux与Windows的异同 课堂引入 本章内容主要为大家详细讲解Linux操作系统(以下简…...

华为OD刷题C卷 - 每日刷题 1
1、(两数之和): 这段代码是针对力扣(LeetCode)上的“两数之和”问题。它提供了一个Java类Solution,其中包含一个方法twoSum,该方法接收一个整数数组nums和一个整数目标值target。目的是找出数组…...

基于ELK的日志管理【开发实践】
文章目录 一、ELK简介1.1 ELK的作用与应用1.2 ELK的组成1.3 Elasticsearch1.4 Logstash1.5 Kibana1.6 ELK架构简述1.7 基础知识1.7.1 数据格式1.7.2 正排索引和倒排索引1.7.3 全文搜索 二、ES入门---基于HTTP的使用方式(了解)2.1 索引操作2.1.1 创建索引…...

音视频开发—音频相关概念:数模转换、PCM数据与WAV文件详解
文章目录 前言1.模拟数字转换(ADC)1.1ADC的关键步骤: 2.数字模拟转换(DAC)2.1DAC 的基本流程包括: 3.PCM数据3.1PCM 数据的关键要素包括: 4.WAV文件4.1 WAV的构成4.2WAV文件的标准块结构4.3WAV的…...

Elasticsearch 8.1官网文档梳理 - 十三、Search your data(数据搜索)
Search your data 这里有两个比较有用的参数需要注意一下 Search timeout:设置每个分片的搜索超时时间。从集群级别可以通过 search.default_search_timeout 来设置超时时间。如果在 search.default_search_timeout 设置的时间段内未完成搜索请求,就会…...

笔墨挥毫如游龙 最是经典铁线篆——记著名书法家王子彬
真正的书法大家,必是经历了日积月累的求索磨炼,毕竟书法从来都不是一蹴而就的艺术,因此但凡是急功近利者,其人也是远远无法达到书入臻境的创作高度。而纵观当代书坛界内,其中王子彬先生的艺术声誉可谓是广为人知,作为一名深具传统功底的实力派书法大家,王子彬先生的取法历途无疑…...

智慧校园有哪些特征
随着科技的飞速进步,教育领域正经历着一场深刻的变革。智慧校园,作为这场变革的前沿代表,正在逐步重塑我们的教育理念和实践方式。它不仅仅是一个概念,而是一个集成了物联网、大数据、人工智能等先进技术的综合生态系统࿰…...

day25回溯算法part02| 216.组合总和III 17.电话号码的字母组合
216.组合总和III 题目链接/文章讲解 | 视频讲解 class Solution { public:vector<vector<int>> result;vector<int> path;int sum;void backtracking(int n, int k, int startindex) {// int sum accumulate(path.begin(), path.end(), 0);if (sum n &am…...

AWS联网和内容分发服务
概况 VPC Amazon Virtual Private Cloud (Amazon VPC) 让您能够全面地控制自己的虚拟网络环境,包括资源放置、连接性和安全性。首先在 AWS 服务控制台中设置 VPC。然后,向其中添加资源,例如 Amazon Elastic Compute Cloud (EC2) 和 Amazon …...

vscode设置编辑器文件自动保存
步骤 1.打开vscode的设置 2.在搜索栏输入关键字“保存”; 在 Files: Auto Save 设置项,选择自动保存的模式...

SJ705C安全帽高温预处理箱
一、仪器用途 安全帽高温预处理箱是我公司根据安全帽新国家标准检测试验要求而自主设计研发制造。是安全帽检测前做高温预处理的专用设备。 二、仪器特征 1、有PID自整定温度控制仪,控制准确。 2、数显计时、计温器。 3、石英灯管加热系统;。 …...

AI盒子在智慧加油站的应用
方案背景 为规范加油站作业,保障人民生命财产安全,《加油站作业安全规范》(AQ 3010-2007)中第五条规定:卸油作业基本要求,明确防静电、防雷电、防火、人员值守、禁止其他车辆及非工作人员进入卸油区。 痛点…...

IC开发——VCS基本用法
1. 简介 VCS是编译型verilog仿真器,处理verilog的源码过程如下: VCS先将verilog/systemverilog文件转化为C文件,在linux下编译链接生成可执行文件,在linux下运行simv即可得到仿真结果。 VCS使用步骤,先编译verilog源…...

MongoDB~存储引擎了解
存储引擎 存储引擎是一个数据库的核心,主要负责内存、磁盘里数据的管理和维护。 MongoBD的优势,在于其数据模型定义的灵活性、以及可拓展性。但不要忽略,其存储引擎也是插件式的存在,支持不同类型的存储引擎,使用不同…...

JavaScript实现粒子数字倒计时效果附完整注释
<!DOCTYPE html> <html lang="en"><head><meta charset...

Dubbo SPI(Service Provider Interface)机制深度解析
Dubbo SPI(Service Provider Interface)机制是Apache Dubbo框架中一项核心的技术组件,它超越了传统Java SPI的范畴,为Dubbo带来了高度的可扩展性和灵活性。在分布式服务架构日益复杂多变的今天,Dubbo SPI机制通过巧妙的…...

常用中间件各版本下载
常用中间件下载地址 前言分布式中间件负载均衡中间件缓存中间件数据库中间件其他中间件1、Maven下载地址2、Git下载地址2、JDK下载地址3、MySQL下载地址4、Redis下载地址5、Nacos下载地址6、Tomcat下载地址7、Nginx下载地址8、RocketMQ下载地址8、RabbitMQ下载地址8、Erlang下载…...

VsCode SSH远程设置不用重复输入密码
winR输入cmd,回车,输入 C:\Users\Administrator> ssh-keygen -t rsa -b 4096 Generating public/private rsa key pair. Enter file in which to save the key (C:\Users\Administrator/.ssh/id_rsa): Enter passphrase (empty for no passphrase): …...

【Linux】:进程切换
朋友们、伙计们,我们又见面了,本期来给大家解读一下有关Linux进程切换的知识点,如果看完之后对你有一定的启发,那么请留下你的三连,祝大家心想事成! C 语 言 专 栏:C语言:从入门到精…...

MongoDB CRUD操作:删除文档
MongoDB CRUD操作:删除文档 文章目录 MongoDB CRUD操作:删除文档删除集合的所有文档删除符合条件的所有文件删除第一个符合条件的文档## 在MongoDB Atlas中删除文档删除行为索引 原子性写确认 可以使用下面的方式删除MongoDB集合的文档: 使用…...

SpringBoot集成腾讯COS流程
1.pom.xml中添加cos配置 <!--腾讯cos --> <dependency><groupId>com.qcloud</groupId><artifactId>cos_api</artifactId><version>5.6.28</version> </dependency> 2.application.yaml中添加cos配置 # 腾讯云存储cos…...

中高级前端开发岗
定位: 日常迭代任务的核心研发,具备高质、高效完成迭代任务的能力。 素质要求: 业务专家或擅长某一方向技术;有较丰富的开发经验;需要具备良好的沟通和协作能力,能够与其他部门和团队进行有效的沟通和协…...

idea常用配置
文章目录 I 常见问题1.1 取消maven忽略文件清单1.2 源根之外的java文件1.3 idea取消所有断点1.4 idea使用非模式提交界面1.5 用Service窗口展示所有服务及端口1.6 idea编码问题(加载配置文件失败)II idea 换行后自动缩进4个空格,怎么取消?I 常见问题 1.1 取消maven忽略文件…...

Spring AOP 切面按照一定规则切片并行查询Mapper并返回
需求: 有时候我们在查询mapper层时,有时候可能由于入参数据过大或者查询的范围较大,导致查询性能较慢,此时 我们需要将原本的查询按照一定规则将查询范围进行切面,然后分片查询,最后将查询结果进行组装合并…...

【vue3|第4期】Vue3的选项式与组合式
日期:2024年5月30日 作者:Commas 签名:(ง •_•)ง 积跬步以致千里,积小流以成江海…… 注释:如果您觉得有所帮助,帮忙点个赞,也可以关注我,我们一起成长;如果有不对的地方…...

算法训练营第四十五天 | LeetCode 1049 最后一块石头的重量II、LeetCode 494 目标和、LeetCode 474 一和零
LeetCode 1049 最后一块石头的重量 继续昨天没有详细说的01背包问题往下继续说。01背包问题是将dp从一维问题升维到二维之后会遇到的一类典型问题。dp数组自然而然地是一个横坐标表示物品序号-1,纵坐标表示背包重量的二维数组。01背包由一个背包是否放该物品并比照后…...