likes
comments
collection
share

Go 实践|基于 session 的登录流程

作者站长头像
站长
· 阅读数 15

记录登录流程的笔记

cookie、session、 redis

背景

登录流程,大家都是很熟悉的。大部分网站都是会使用 session 验证登录状态。流程如下:

  • 用户输入用户名和密码提交登录表单。
  • 服务器端校验用户名和密码。校验通过后存储用户信息。
  • 后续客户端每次请求时,服务端获取到对应的 session 数据,验证登录状态。

一个简单的登录流程就完成,但是如果深入学习挖掘的时候会发现有很多可以优化的地方。本文主要使用 go gin 框架对登录流程思考的记录。


初始版本:仅使用 session

登录流程

服务端验证账号密码正确后,生成 session ,然后保存session。然后用户发起其他请求时只需获取session,验证session 正常,就可以正常访问。

代码实现

// 用户发起登录请求的处理
func LoginHandle(c *gin.Context){
	// step1.获取账号和密码
    // step2.校验账号和密码()
    // step3.生成 session
    session := sessions.Default(ctx)
	session.Set("UserSession", entities.UserInfo{
		UserId:       userName,
		UserName:     user.UserName,
	})
    session.Save()
    ctx.JSON(http.StatusOK, gin.H{"code": 200, "message": "登录成功"})
}

// 使用gin 中间对后续用户发起其他请求时验证登录状态
func AuthUser(c *gin.Context){
    userInfo := session.Get("UserSession")
}

存在问题

  1. 没有设置 session 过期时间,长时间保留用户信息存在安全风险
  2. 没有设置独立的 session ID 作为映射,不仅扩展性弱,而且对于不同请求的用户状态就无法实现
  3. 没有设置 Cookie 。HTTP本身是一种无状态的协议,需要通过Cookie来维持用户状态。所以应该同时使用Set-Cookie头部来将这个session ID发送给客户端。

优化版本:Cookie 和 session 存储

Cookie 和过期时间

session 机制通常需要配置 Cookie 来实现的,服务端校验账号密码后生成一个 session ID ,同时通过 Set-Cookie头部来将这个session ID发送给客户端。客户端接收并存储Cookie ,后续请求都会携带Cookie。服务端通过获取Cookie 中的 session Id,同时根据 session Id 查找对应的session信息。

func LoginFunc(ctx *gin.Context) {
	session := sessions.Default(ctx)

	// 生成唯一的 session Id
	sessionId := uuid.New().String()
	session.Set(sessionId, entities.UserInfo{
		UserId:   123456,
		UserName: "UserName",
	})
	sessionTTL := 30000
	// 设置 Cookie
	ctx.SetCookie("web-sid", sessionId, sessionTTL, "/", "", false, true)

	// 设置过期时间
	session.Options(sessions.Options{MaxAge: sessionTTL})

	session.Save()
	ctx.JSON(http.StatusOK, gin.H{"code": 200, "message": "登录成功"})
}

// 使用gin 中间对后续用户发起其他请求时验证登录状态
func AuthUser(c *gin.Context){
    // 从 cookie 中获取 sessionId
    data, err := c.Cookie("web-sid")
	if err != nil {
		return
	}
    // 根据 sessionId 获取到对应的内容
   session := sessions.Default(c)
	userInfo := session.Get(data)
}

解决了过期时间,独立的sessionID,还有设置cookie 其实还是有问题存在。比如:所有的用户信息都存储在 session 中,如果用户信息内容多的话就占用大量的内存。而且,如果服务器是集群模式,那这种存储方式就容易导致不一致的问题,所以可以使用外部存储 redis 来支持session 共享。

redis 存储 session 对应信息

使用 redis 代替 session, 直接将用户信息存储在 redis 中,而每个session Id 当作缓存的 key。如果还想使用 session ,可以只对session 存储对应用户Id,相当于session只存储 sessionId 与用户 Id 的映射。

sessionId := uuid.New().String()
sessionTTL := 30000
userInfo := entities.UserInfo{
		UserId:   123456,
		UserName: "UserName",
}
// 使用 redis 存储session 信息
redis.Set(sessionId, userInfo, sessionTTL)

// 如果使用 session 的话,session 只存储 UserId ,redis 中存储用户信息,key是UserId
session := sessions.Default(c) 
session.Set("userId", userId)
session.Save()

redis.Set(userId, userInfo, sessionTTL)

单机模式:Gin Store

当然,在单机的情况下,可以使用 Gin 框架自带的 Store 存储session。

store := cookie.NewStore([]byte("userInfo")) 
sessions.Sessions("mysession", store)

session := sessions.Default(c) 
session.Set("userId", userId)
session.Save()

session中不存储用户信息的原因是:

  • 用户信息变更时就需要同时更新 session ,独立存储就可以降低维护成本,比如变更时直接删除redis 信息。
  • 如果用户信息是很多的数据量,就会导致内存的占用,且加载和保存 session 都会耗时。
  • 当服务是分布式场景时就会造成共享和同步 session 的困难(使用 redis+cookie 方案)。
  • 当服务扩展需求依赖登录状态时就会增加困难,比如需要实现单点登录(使用 redis+cookie 方案)。

所以,登录成功后服务端产生的 session ID 通过 Cookie 的方式写入客户端,然后客户端每次请求都携带 Cookie。服务端从 Cookie 中获取 session ID,再从存储中获取到用户信息来完成其他的业务。