Gin使用Session中间件获取当前登录用户

4,325 阅读4分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

简介

最近在使用Gin重构之前的SpringBoot后台,虽然现在使用JWT来做登录鉴权比较流行,但是为了保持接口的一致性还是选择使用session机制来做登录鉴权,在Gin中使用得比较多的session中间件是gin-contrib/sessions,项目的GitHub上有简单使用教程,这里记录一下对其的封装,能通过session获取到当前登录用户的信息。

需求

之前的SpringBoot项目大致可以看作是一个微型的Blog平台,有许多操作如更新、删除文章通常要鉴定权限,只有本人才能更新、删除,当时的设计是当用户登录时用seesion存储用户的基本信息(ID、Email等),之后调用更新、删除API时就只用传文章相关的参数,不需要传用户参数(用户参数可以通过header中的sessionId获取)。当时使用的Shiro来管理session,通过调用Shiro的SecurityUtils就能获取到session数据,由于有许多接口都可能要用到当前登录用户的信息,所以写了一个BaseController来封装通过session读取用户信息,之后需要用到这个功能的Controller就继承BaseController。


import org.apache.shiro.SecurityUtils;

public abstract class BaseController {
    /**
     * 返回当前登录用户信息,User是自定义的用户类,存储用户ID、Email等信息
     * @return
     */
    protected User getCurrentUser(){
    // 调用Shiro相关接口即可,都不需要上下文参数
        return (User) SecurityUtils.getSubject().getSession().getAttribute("currentUser");
    }

    /**
     * 设置当前登录用户的信息
     * @param user
     */
    protected void setCurrentUser(User user){
        SecurityUtils.getSubject().getSession().setAttribute("currentUser", user);
    }
}

在Gin中使用Session

gin-contrib/sessions官方给出的使用教程如下"secret"表示的是生成sessionID时的密钥,随便填个字符串就行;"mysession"表示的是返回给前端的sessionId名称,比如我填"SESSIONID"那么返回給前端的cookie数据就有SESSIONID这个字段

package main

import (
  "github.com/gin-contrib/sessions"
  "github.com/gin-contrib/sessions/cookie"
  "github.com/gin-gonic/gin"
)

func main() {
  r := gin.Default()
  store := cookie.NewStore([]byte("secret")) // 设置生成sessionId的密钥
  // mysession是返回給前端的sessionId名
  r.Use(sessions.Sessions("mysession", store))

  r.GET("/hello", func(c *gin.Context) {
    session := sessions.Default(c)

    if session.Get("hello") != "world" {
      session.Set("hello", "world")
      session.Save()
    }

    c.JSON(200, gin.H{"hello": session.Get("hello")})
  })
  r.Run(":8000")
}

image.png

通过Session中存取用户信息

官方只介绍了简单类型的数据存取,但是有些时候我们可能需要在session中存取结构体对象,比如我在上面需求中提到的要存取用户信息。这个时候就要注意,得先使用gob注册结构体,gob是golang自带得序列化编解码工具,在对自定义struct编解码时要调用gob.Register进行注册,否则在调用session.Save()时会返回以下错误,并且还不容易发现,当时在这里卡了好久。。。

securecookie: error - caused by: securecookie: error - caused by: gob: type not registered for interface: model.User

在使用session读取User结构体时还要使用Golang的类型转换(断言),因为session.Get获取到是一个interface。再简单的编写一下路由,包含一个login登录接口,登录成功时将用户信息写入session;还有一个测试接口,不用传递参数返回当前登录用户的用户名。完整测试代码如下:

type User struct {
	Id       int    `json:"id"`
	Email    string `json:"email"`
	Username string `json:"username"`
	Password string `json:"password"`
}

func getCurrentUser(c *gin.Context) (userInfo User) {
	session := sessions.Default(c)
	userInfo = session.Get("currentUser").(User) // 类型转换一下
	return
}

func setCurrentUser(c *gin.Context, userInfo User) {
	session := sessions.Default(c)
	session.Set("currentUser", userInfo)
        // 一定要Save否则不生效,若未使用gob注册User结构体,调用Save时会返回一个Error
	session.Save() 
}

func setupRouter(r *gin.Engine) {
	r.POST("/login", func(c *gin.Context) {
		var loginVo User
		if c.ShouldBindJSON(&loginVo) != nil {
			c.String(http.StatusOK, "参数错误")
			return
		}
		if loginVo.Email == db.Email && loginVo.Password == db.Password {
			setCurrentUser(c, *db) // 邮箱和密码正确则将当前用户信息写入session中
			c.String(http.StatusOK, "登录成功")
		} else {
			c.String(http.StatusOK, "登录失败")
		}
	})

	r.GET("/sayHello", func(c *gin.Context) {
		userInfo := getCurrentUser(c)
		c.String(http.StatusOK, "Hello "+userInfo.Name)
	})
}

var db = &User{Id: 10001, Email: "abc@gmail.cn", Username: "Alice", Password: "123456"} // 不操作数据库,把所有用户信息写死在代码里

func main() {
	gob.Register(User{}) // 注册User结构体
	r := gin.Default()
	store := cookie.NewStore([]byte("snaosnca"))
	r.Use(sessions.Sessions("SESSIONID", store))
	setupRouter(r)
	r.Run(":8080")
}

测试运行

完整可运行代码见Github go run ./main.go再使用Postman发送登录请求,得到下图结果:

image.png 登录成功后再向/sayHello接口发送一个不带参数的get请求,得到下图结果,说明使用session成功存取了当前登录用户的信息。

image.png