likes
comments
collection
share

使用Session中间件来实现基于Redis的分布式Session解决方案

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

简介

本期的主角是前不久贡献给 hertz-examples 的 hertz_session bizdemo,项目地址在这里,这个 Demo 旨在帮助用户快速上手 Hertz 的 Session 中间件CSRF 中间件,并展示基于 Redis 的分布式 Session 解决方案。如果你不知道 Hertz 是什么,那么你可以查看我的上一篇文章,它可以帮助你快速上手这款 Golang HTTP 框架。

hertz_session 的主要特点如下:

  • 使用 thrift IDL 定义 HTTP 接口
  • 使用 hz 工具生成脚手架代码
  • 使用 hertz-contrib/sessions 存储Session
  • 使用 hertz-contrib/csrf 防御跨站请求伪造攻击
  • 使用 GormMySQL
  • 使用 AdminLTE 的前端页面

获取Demo

使用以下命令来获取这个 Demo:

git clone https://github.com/cloudwego/hertz-examples.git
cd bizdemo/hertz_session

项目结构

使用Session中间件来实现基于Redis的分布式Session解决方案

  • biz 存放主要业务逻辑代码,包括处理 HTTP 请求的 handler ,进行数据库操作的 dal,中间件 mw
  • idl 存放 thrft IDL
  • pkg 为一些工具方法,业务常量,errmsg,模板渲染等
  • static 存放前端的静态文件,均截取自 AdminLTE
  • 其他主目录下的文件包括主启动 main.go,docker 配置文件等

中间件的使用

Session 中间件

基于 Redis 的分布式 Session 解决方案是指将不同服务器的 Session 统一存储在 Redis 或 Redis 集群中,旨在解决分布式系统下多个服务器的 Session 不同步的问题。

  1. 所以这里我们通过使用 Hertz 的 Session 中间件将 Session 存储在 Redis 中,初始化代码如下:
// biz/mw/session.go
func InitSession(h *server.Hertz) {
	store, err := redis.NewStore(consts.MaxIdleNum, consts.TCP, consts.RedisAddr, consts.RedisPasswd, []byte(consts.SessionSecretKey))
	if err != nil {
		panic(err)
	}
	h.Use(sessions.New(consts.HertzSession, store))
}
  • 首先通过传入地址密码等参数使用 redis.NewStore 与 Redis 建立连接
  • 通过 h.Use 方法使用 Session 中间件,并刚刚返回的存储连接对象传入,其中第一个形参为 Cookie 的名字,此处通过定义常量传入
  1. 在初始化完毕后,我们就可以在用户通过验证并登录后将用户的信息(此处为用户名)存储在 Session 中,以下为 Login Handler Session 部分的核心代码:
// biz/handler/user/user_service.go/Login
session := sessions.Default(c)
session.Set(consts.Username, req.Username)
_ = session.Save()
  • 首先通过 sessions.Default 获取一个 Session 对象
  • 通过 Set 方法将用户名存储到 Session 中
  • 最后调用 Save 方法进行保存 Session 对象

这样在用户登录后我们就会在 Redis 存储的 Session 对象中保存一份用户的信息,用户下一次就无需进行登录即可访问对应的页面了。

  1. 在本例中,只设置了 index.html 这一个主页,用户对主页进行访问时会进行判断,如果用户已经登录则可以访问,如果未登录则会重定向到登录页面 login.html,判断是否登录的核心代码如下:
// pkg/render/render.go
h.GET("/index.html", func(ctx context.Context, c *app.RequestContext) {
    session := sessions.Default(c)
    username := session.Get(consts.Username)
    if username == nil {
        c.HTML(http.StatusOK, "index.html", hutils.H{
            "message": utils.BuildMsg(consts.PageErr),
        })
        c.Redirect(http.StatusMovedPermanently, []byte("/login.html"))
        return
    }
    c.HTML(http.StatusOK, "index.html", hutils.H{
        "message": utils.BuildMsg(username.(string)),
    })
})

其实就是跟第二步的 Login 是差不多的流程,只不过把 Set 换成了 Get,并且不用 Save

  1. 最后在用户登出时,我们需要对用户的 Session 进行清理,核心代码如下:
// biz/handler/user/user_service.go/Logout
session := sessions.Default(c)
session.Delete(consts.Username)
_ = session.Save()

跟上面同理,注意需要 Save 否则这次删除操作就是无效的。

以上就是 Session 中间件的使用,是不是非常简单,Session 中间件将大多数需要考虑的复杂逻辑都进行了封装,例如不同用户 Session 在 Redis 中的存储,而我们只需要调用简单的接口即可完成对应的业务流程。

CSRF 中间件

接下来是 CSRF 中间件的使用,关于什么是 CSRF 攻击,以下解释参考自维基百科:

跨站请求伪造,也被称为 one-click attack 或 session riding ,缩写为 CSRF 或 XSRF,是一种对网站或web应用程序的恶意利用,web应用程序信任的用户提交未经授权的命令恶意网站传播此类命令的方式有很多; 例如,特别制作的图像标签、隐藏表单、JavaScript fetch 或 XMLHttpRequests 都可以在没有用户交互甚至不知情的情况下工作。与跨站脚本(XSS)利用用户对特定网站的信任不同,CSRF利用的是网站对用户浏览器的信任,在CSRF攻击中,攻击者诱骗无辜的终端用户提交了他们无意的web请求。这可能导致在网站上执行的操作包括无意中泄露客户端或服务器数据、更改会话状态或操纵最终用户的帐户。

查阅相关资料后我理解的就是恶意网站利用一些网站对于用户浏览器的信任,比如利用 Cookie,发起的一些攻击。

在本 Demo 中由于 CSRF 中间件是后来加的,刚开始定义 IDL 时并没有考虑,所以在登录后只有一个登出的 GET 请求,由于 GET 被认为是安全的方法所以这里主要利用 CSRF 中间件去保护注册和登录两个 POST 表单提交。

  1. 我们同样来看一下 CSRF 的初始化和使用,具体代码如下:
func InitCSRF(h *server.Hertz) {
	h.Use(csrf.New(
		csrf.WithSecret(consts.CSRFSecretKey),
		csrf.WithKeyLookUp(consts.CSRFKeyLookUp),
		csrf.WithNext(utils.IsLogout),
		csrf.WithErrorFunc(func(ctx context.Context, c *app.RequestContext) {
			c.String(http.StatusBadRequest, errors.New(consts.CSRFErr).Error())
			c.Abort()
		}),
	))
}
  • 调用 h.Use 使用 CSRF 中间件
  • 利用 csrf.WithSecret 定义令牌
  • 利用 csrf.WithKeyLookup 定义从表单中获取 CSRF Token,默认为请求头中获取
  • 利用 csrf.WithNext 跳过没有登录的情况,即不存在 Cookie
  • 利用 csrf.WithErrorFunc 自定义异常处理
  1. 初始化完毕后我们只需在登录后的表单提交时通过 hidden 域提交生成的 CSRF Token,中间件会自动帮助我们验证是否有效,如果出现错误则会走刚刚定义的异常处理函数,由于本 Demo 使用的是模板渲染的模式,核心代码如下(以注册为例,登录同理):
// pkg/render/render.go
h.GET("/register.html", func(ctx context.Context, c *app.RequestContext) {
    if !utils.IsLogout(ctx, c) {
        token = csrf.GetToken(c)
    }
    c.HTML(http.StatusOK, "register.html", hutils.H{
        "message": utils.BuildMsg("Register a new membership"),
        "token":   utils.BuildMsg(token),
    })
})

HTML 模板核心代码如下:

<div>
	<input type="hidden" name="csrf" value="{[{ .token | BuildMsg }]}">
</div>
  • 判断用户已登录后获取对应的 Token 然后放到表单中提价即可,剩下的交给中间件自行处理。

运行

执行以下代码运行项目(项目根目录下):

docker-compose up
go run .

成功运行后访问 localhost:8888/register.html 即可,运行截图如下:

使用Session中间件来实现基于Redis的分布式Session解决方案

使用Session中间件来实现基于Redis的分布式Session解决方案

使用Session中间件来实现基于Redis的分布式Session解决方案

总结

以上就是本期的所有内容了,上次说的 hertz_gorm 小咕一下,预计下篇发,很快ww。如果你觉得本文对你有帮助,欢迎点赞收藏,如果那块觉得有问题也可以评论或者私信,以上。

参考列表

转载自:https://juejin.cn/post/7175765782024945724
评论
请登录