说明

项目中用到了session鉴权,为此记录

为什么要使用session鉴权

首先要了解HTTP的无状态协议。比较专业的说法是因为它的每个请求都是完全独立的,每个请求包含了处理这个请求所需的完整的数据,发送请求不涉及到状态变更。通俗的说法是服务器不记得你是谁,也就是说你的下一次请求必须要经历重新认证的过程(类似用户名密码登录)。作为用户来说,肯定是不希望这样的。因此,我们可以使用session鉴权来解决这个问题。

优点:HTTP的无状态协议,使用元数据(如Cookies头)来维护会话,使得会话与连接本身独立起来,即使连接断开了,会话状态也不会受到严重伤害,保持会话也不需要保持连接本身。此外,无状态的优点还在于对中间件友好,中间件无需完全理解通信双方的交互过程,只需要能正确分片消息即可,而且中间件可以很方便地将消息在不同的连接上传输而不影响正确性,这就方便了负载均衡等组件的设计。

缺点:单个请求需要的所有信息都必须要包含在请求中一次发送到服务端,这就导致单个消息的结构往往比较复杂,必须能够支持大量元数据。同时,这也导致了相同的数据在多个请求上往往需要反复传输,这在一定程度上降低了效率。

session鉴权

  1. 用户登陆成功后,将所需信息保存在session对象中,保存在数据库里
  2. 服务器生成一个Cookie,给用户颁发一个唯一的sessionid,并通过http的响应头返回,即Set-Cookie。
  3. 浏览器接收到用户的请求后,会自动保存里面的Cookie
  4. 在用户后续的请求里,浏览器会自动在请求头带上Cookie,服务器获取Cookie中的sessionid,从而进行验证
  5. 若用户关闭浏览器,会话状态即消失

session和jwt的区别

主要区别:session的Cookie是存在服务器端,而jwt的token则存在客户端

说明:session鉴权,服务器端生成Cookie,并存在数据库中,将其返回给客户端,客户端接下来的请求中会带上这个Cookie,服务器将会获取Cookie中的sessionid,从而进行验证

jwt鉴权,服务器生成token,并返回给客户端,在后续的请求中,客户端需加上相应的请求头,服务器解密进行对比,从而进行验证

go语言,gin框架,redis实现session鉴权

不会一次性给出所有代码,由于是一个项目开发,因此要将各个功能的代码进行封装,来提高代码可读性和可维护性

配置

1
2
3
4
5
6
7
8
9
10
11
//config.yaml
session:
name: //会话名称
driver: redis //驱动类型

redis:
host: "127.0.0.1"
port: 6379
db: 0
user: root
pass:

读取配置

1
2
3
4
5
6
7
8
9
10
11
12
13
//使用viper读取config.yaml的配置信息
var Config = viper.New()

func init() {
Config.SetConfigName("config") // 设置配置文件的名称(不含扩展名)
Config.SetConfigType("yaml") // 设置配置文件的类型
Config.AddConfigPath(".") // 添加配置文件的搜索路径(当前目录)
Config.WatchConfig() // 启用自动监视配置文件的更改
err := Config.ReadInConfig() // 将配置文件读取到Config变量中
if err != nil {
log.Fatal("Config not find", err)
}
}

具体配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
type driver string

const (
Memory driver = "memory" //内存存储:会话数据存储在应用程序的内存中。这种方式的优势是速度快,适用于小型应用或开发环境。然而,缺点是在应用程序重新启动时,所有的会话数据会丢失。
Redis driver = "redis" //外部存储(例如 Redis、数据库等):会话数据存储在外部数据库或缓存系统中,如 Redis、数据库等。这种方式的优势是数据持久性强,即使应用程序重新启动也不会丢失会话数据。这也使得多个应用实例能够共享相同的会话数据,从而实现横向扩展。
)

var defaultName = "" //是默认的会话名称

// sessionConfig 结构体用于存储会话配置信息
type sessionConfig struct {
Driver string
Name string
}

// getConfig 函数用于获取会话配置信息
func getConfig() sessionConfig {

wc := sessionConfig{}
wc.Driver = string(Memory) // 设置默认驱动类型为内存
// 如果配置中存在会话驱动配置,则使用配置中的值
if config.Config.IsSet("session.driver") {
wc.Driver = strings.ToLower(config.Config.GetString("session.driver"))
}

wc.Name = defaultName // 设置默认会话名称为 ""
// 如果配置中存在会话名称配置,则使用配置中的值
if config.Config.IsSet("session.name") {
wc.Name = strings.ToLower(config.Config.GetString("session.name"))
}

return wc
}

// getRedisConfig 函数用于获取 Redis 配置信息
func getRedisConfig() redisConfig {
Info := redisConfig{
Host: "localhost",
Port: "6379",
DB: 0,
Password: "",
}
// 如果配置中存在 Redis 主机配置,则使用配置中的值
if config.Config.IsSet("redis.host") {
Info.Host = config.Config.GetString("redis.host")
}
// 如果配置中存在 Redis 端口配置,则使用配置中的值
if config.Config.IsSet("redis.port") {
Info.Port = config.Config.GetString("redis.port")
}
// 如果配置中存在 Redis 数据库配置,则使用配置中的值
if config.Config.IsSet("redis.db") {
Info.DB = config.Config.GetInt("redis.db")
}
// 如果配置中存在 Redis 密码配置,则使用配置中的值
if config.Config.IsSet("redis.password") {
Info.DB = config.Config.GetInt("redis.password")
}
return Info
}

创建Redis客户端(以便后续对Redis操作)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
type redisConfig struct {
Host string
Port string
DB int
Password string
}

// RedisClient 是一个全局变量,表示 Redis 客户端
var RedisClient *redis.Client
var RedisInfo redisConfig

func init() {
info := getConfig()

RedisClient = redis.NewClient(&redis.Options{
Addr: info.Host + ":" + info.Port,
Password: info.Password,
DB: info.DB,
})
RedisInfo = info
}

创建基于 Redis 的会话存储

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
type redisConfig struct {
Host string
Port string
DB int
Password string
}

func setRedis(r *gin.Engine, name string) {
Info := getRedisConfig()
// 创建基于 Redis 的会话存储,使用 sessionRedis.NewStore 函数
// 参数 10 表示存储的最大空闲连接数,"tcp" 表示 Redis 的网络协议,
// Info.Host+":"+Info.Port 表示 Redis 的地址,"" 表示密码(没有密码时为空字符串),
// []byte("secret") 表示用于加密的密钥
store, _ := sessionRedis.NewStore(10, "tcp", Info.Host+":"+Info.Port, "", []byte("secret"))
r.Use(sessions.Sessions(name, store))
}

选择会话的存储方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
func Init(r *gin.Engine) {
// 获取会话配置信息
config := getConfig()
// 根据配置选择合适的会话存储方式
switch config.Driver {
case string(Redis):
setRedis(r, config.Name)
break
case string(Memory):
setMemory(r, config.Name)
break
default:
log.Fatal("ConfigError")
}
}

使用

1
2
3
4
5
6
7
8
9
10
11
// SetUserSession 函数用于设置用户的会话信息。
func SetUserSession(c *gin.Context, user *models.User) error {
// 获取当前请求的会话对象
webSession := sessions.Default(c)
// 设置会话的选项,包括最大存活时间(MaxAge)、路径(Path)等
webSession.Options(sessions.Options{MaxAge: 3600 * 24 * 7, Path: "/api"})
// 将用户的ID存储在会话中
webSession.Set("id", user.ID)
// 保存会话信息
return webSession.Save()
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func GetUserSession(c *gin.Context) (*models.User, error) {
webSession := sessions.Default(c)
id := webSession.Get("id")
if id == nil {
return nil, errors.New("")
}
user, _ := userServices.GetUserID(id.(int))
// 如果未找到用户,清除用户会话并返回nil和错误
if user == nil {
ClearUserSession(c)
return nil, errors.New("")
}
return user, nil
}

func ClearUserSession(c *gin.Context) {
webSession := sessions.Default(c)
webSession.Delete("id")
webSession.Save()
return
}

更新

1
2
3
4
5
6
7
8
9
10
11
func UpdateUserSession(c *gin.Context) (*models.User, error) {
user, err := GetUserSession(c)
if err != nil {
return nil, err
}
err = SetUserSession(c, user)
if err != nil {
return nil, err
}
return user, nil
}

额外知识:同源策略

浏览器的同源策略 - Web 安全 | MDN (mozilla.org)