Golang Websocket(Gorilla)使用cookie进行身份验证

huangapple go评论88阅读模式
英文:

Golang Websocket (Gorilla) with cookie authentification

问题

我正在尝试使用gorilla websocket来启动图表。身份验证中间件通过带有JWT令牌的cookie工作。我的所有HTTP端点都可以正常工作,但是websocket却不行。在阅读了很多类似于https://stackoverflow.com/questions/29324251/gorilla-websocket-with-cookie-authentication的主题后,我发现我的cookie为空,而且websocket连接中的上下文也为空。我不明白为什么?有人可以解释一下吗?
附注:我尝试删除处理程序中的升级器,cookie和上下文传递正常,但在将连接升级到websocket协议后失败了。
这是我的文件:
端点:

func (r *router) routes(engine *gin.Engine) {
	engine.Use(r.handler.VerifyUser())

	engine.POST("/signup", r.handler.CreateUser)
	engine.POST("/signin", r.handler.LoginUser)
	engine.GET("/welcome", r.handler.Welcome)
	engine.GET("/logout", r.handler.Logout)

	engine.POST("/ws/createRoom", r.wsHandler.CreateRoom)
	engine.GET("/ws/joinRoom/:roomId", r.wsHandler.JoinRoom)
}

ws_handler

func (h *Handler) JoinRoom(c *gin.Context) {
	claims := c.Request.Context().Value("jwt").(models.Claims) //找不到“jwt”键的值
	fmt.Println(claims.ID, claims.Name)
	cookie, err := c.Cookie("chartJWT") //始终出错,没有cookie
	if err != nil {
		fmt.Printf("没有cookie,错误:%v\n", err)
	}
	fmt.Printf("cookie: %+v\n", cookie)
	conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}
}

中间件:

func (h *handler) VerifyUser() gin.HandlerFunc {
	return func(c *gin.Context) {
		notAuth := []string{"/signup", "/signin"}
		requestPath := c.Request.URL.Path

		for _, val := range notAuth {
			if val == requestPath {
				c.Next()
				return
			}
		}

		token, err := c.Cookie("chartJWT")
		if err != nil {
			c.Redirect(http.StatusPermanentRedirect, signinPage)
		}

		claims, ok := validateToken(token)
		if !ok {
			c.JSON(http.StatusBadRequest, gin.H{"error": errors.New("invalid token")})
			return
		}

		c.Request = c.Request.WithContext(context.WithValue(c.Request.Context(), "jwt", *claims))

		c.Next()
	}
}

其他所有端点都可以正常工作,如果您需要其他代码,请告诉我。我不想让我的问题变得更复杂,因为我认为它非常简单,但我可能误解了某些东西(
感谢任何帮助和建议。

更新:
添加了验证和生成函数

func validateToken(jwtToken string) (*models.Claims, bool) {
	claims := &models.Claims{}

	token, err := jwt.ParseWithClaims(jwtToken, claims, func(token *jwt.Token) (interface{}, error) {
		return []byte(config.SECRETKEY), nil
	})
	if err != nil {
		return claims, false
	}

	if !token.Valid {
		return claims, false
	}

	return claims, true
}

func (h *handler) generateTokenStringForUser(id, name string) (string, error) {
	// 创建JWT声明,其中包括用户名和到期时间
	claims := models.Claims{
		ID:   id,
		Name: name,
		RegisteredClaims: jwt.RegisteredClaims{
			Issuer:    id,
			ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
		},
	}

	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	tokenString, err := token.SignedString([]byte(config.SECRETKEY))
	return tokenString, err
}

添加了登录函数,在其中添加了带有JWT字符串的cookie

func (h *handler) LoginUser(c *gin.Context) {
	var input models.LoginUserReq

	if err := c.ShouldBindJSON(&input); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	res, err := h.Service.LoginUser(context.Background(), &input)
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	token, err := h.generateTokenStringForUser(res.ID, res.Name)
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	c.SetCookie("chartJWT", token, 60*60*24, "/", "localhost", false, true)
	c.JSON(http.StatusOK, gin.H{"user": res})
}

在帮助后更新:
问题出在我的Postman设置请求上,我没有正确指定cookie。

英文:

I'm trying to use gorilla websocket to start the chart.
Authentification middleware working through cookie with JWT token.
All my endpoints through HTTP works, but websocket doesn't.
After reading a lot of topics like https://stackoverflow.com/questions/29324251/gorilla-websocket-with-cookie-authentication I found that my cookie empty, and my context in the websocket connection also empty. I don't understand why? Can anyone explain me why?
P.S.: I have tried delete upgrader from that handler and cookie and context passed well, but after upgrade connection to websocket protocol it's fails.
Here is my files:
Endpoints:

func (r *router) routes(engine *gin.Engine) {
	engine.Use(r.handler.VerifyUser())

	engine.POST("/signup", r.handler.CreateUser)
	engine.POST("/signin", r.handler.LoginUser)
	engine.GET("/welcome", r.handler.Welcome)
	engine.GET("/logout", r.handler.Logout)

	engine.POST("/ws/createRoom", r.wsHandler.CreateRoom)
	engine.GET("/ws/joinRoom/:roomId", r.wsHandler.JoinRoom)
}

ws_handler

func (h *Handler) JoinRoom(c *gin.Context) {
	claims := c.Request.Context().Value("jwt").(models.Claims) //couldn't find value with "jwt" key
	fmt.Println(claims.ID, claims.Name)
	cookie, err := c.Cookie("chartJWT") // allways err no cookie
	if err != nil {
		fmt.Printf("no cookie, error:%v\n", err)
	}
	fmt.Printf("cookie: %+v\n", cookie)
	conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

middleware:

func (h *handler) VerifyUser() gin.HandlerFunc {
	return func(c *gin.Context) {
		notAuth := []string{"/signup", "/signin"}
		requestPath := c.Request.URL.Path

		for _, val := range notAuth {
			if val == requestPath {
				c.Next()
				return
			}
		}

		token, err := c.Cookie("chartJWT")
		if err != nil {
			c.Redirect(http.StatusPermanentRedirect, signinPage)
		}

		claims, ok := validateToken(token)
		if !ok {
			c.JSON(http.StatusBadRequest, gin.H{"error": errors.New("invalid token")})
			return
		}

		c.Request = c.Request.WithContext(context.WithValue(c.Request.Context(), "jwt", *claims))

		c.Next()
	}
}

All other endpoints works, if you need any other code just let me know. I don't want to make my question more complex, as i think it's pretty simple, but i misunderstand something (
Thanks for any help and advises.

P.S.: if i turn off middleware everything works as expects.

Update:
Added validate and generate funcs

func validateToken(jwtToken string) (*models.Claims, bool) {
	claims := &models.Claims{}

	token, err := jwt.ParseWithClaims(jwtToken, claims, func(token *jwt.Token) (interface{}, error) {
		return []byte(config.SECRETKEY), nil
	})
	if err != nil {
		return claims, false
	}

	if !token.Valid {
		return claims, false
	}

	return claims, true
}

func (h *handler) generateTokenStringForUser(id, name string) (string, error) {
	// Create the JWT claims, which includes the username and expiry time
	claims := models.Claims{
		ID:   id,
		Name: name,
		RegisteredClaims: jwt.RegisteredClaims{
			Issuer:    id,
			ExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),
		},
	}

	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	tokenString, err := token.SignedString([]byte(config.SECRETKEY))
	return tokenString, err
}

added signin func where i added cookie with JWT string

func (h *handler) LoginUser(c *gin.Context) {
	var input models.LoginUserReq

	if err := c.ShouldBindJSON(&input); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	res, err := h.Service.LoginUser(context.Background(), &input)
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	token, err := h.generateTokenStringForUser(res.ID, res.Name)
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	c.SetCookie("chartJWT", token, 60*60*24, "/", "localhost", false, true)
	c.JSON(http.StatusOK, gin.H{"user": res})
}

Update after help:
The problem was in my Postman setup request where i didn't specify cookie right.

答案1

得分: 1

让我试着帮你找出问题出在哪里。首先,我简化了你的示例,只关注相关部分。如果你需要省略某些内容,请告诉我,我会更新答案。首先,让我从本地的auth包开始讲解JWT令牌的生成/验证。

auth/auth.go 文件

package auth

import (
	"fmt"
	"strings"
	"time"

	"github.com/golang-jwt/jwt"
)

func ValidateToken(jwtToken string) (*jwt.MapClaims, error) {
	// 解析令牌
	token, err := jwt.Parse(strings.Replace(jwtToken, "Bearer ", "", 1), func(token *jwt.Token) (interface{}, error) {
		_, ok := token.Method.(*jwt.SigningMethodHMAC)
		if !ok {
			return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
		}
		return []byte("Abcd1234!!"), nil
	})
	// 解析令牌时出错
	if err != nil {
		return nil, err
	}
	// 令牌有效
	var claims jwt.MapClaims
	var ok bool
	if claims, ok = token.Claims.(jwt.MapClaims); ok && token.Valid {
		return &claims, nil
	}
	return nil, fmt.Errorf("token not valid")
}

func GenerateToken(username, password string) (string, error) {
	// TODO: 在这里你可以添加与数据库的检查逻辑
	//...

	// 通过提供加密算法创建一个新的令牌
	token := jwt.New(jwt.SigningMethodHS256)

	// 设置默认/自定义声明
	claims := token.Claims.(jwt.MapClaims)
	claims["exp"] = time.Now().Add(24 * time.Hour * 3).Unix()
	claims["username"] = username
	claims["password"] = password

	tokenString, err := token.SignedString([]byte("Abcd1234!!"))
	if err != nil {
		return "", err
	}
	return tokenString, nil
}

现在,让我们转到中间件部分。

middlewares/middlewares.go 文件

package middlewares

import (
	"net/http"

	"websocketauth/auth"

	"github.com/gin-gonic/gin"
)

func VerifyUser() gin.HandlerFunc {
	return func(c *gin.Context) {
		notAuth := []string{"/signin"}
		requestPath := c.Request.URL.Path
		for _, val := range notAuth {
			if val == requestPath {
				c.Next()
				return
			}
		}

		token, err := c.Cookie("chartJWT")
		if err != nil {
			c.Redirect(http.StatusPermanentRedirect, "/signin")
		}

		claims, err := auth.ValidateToken(token)
		if err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
			return
		}

		c.Set("jwt", *claims)
		c.Next()
	}
}

要能够在上下文中上传一些内容,你应该使用c.Set(key, value)方法。现在,让我们转到处理程序部分。

handlers/handlers.go 文件

package handlers

import (
	"fmt"
	"net/http"

	"websocketauth/auth"

	"github.com/gin-gonic/gin"
	"github.com/golang-jwt/jwt"
	"github.com/gorilla/websocket"
)

var Upgrader websocket.Upgrader

type LoginUserReq struct {
	Username string `json:"username" binding:"required"`
	Password string `json:"password" binding:"required"`
}

func LoginUser(c *gin.Context) {
	var input LoginUserReq
	if err := c.ShouldBind(&input); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	// 我不知道你在 handler.Service.LoginUser() 方法中做了什么

	token, err := auth.GenerateToken(input.Username, input.Password)
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	c.SetCookie("chartJWT", token, 60*60*24, "/", "localhost", false, true)
	c.JSON(http.StatusOK, gin.H{"user": token})
}

func JoinRoom(c *gin.Context) {
	claims := c.MustGet("jwt").(jwt.MapClaims)
	fmt.Println("username", claims["username"])
	fmt.Println("password", claims["password"])
	ws, err := Upgrader.Upgrade(c.Writer, c.Request, nil)
	if err != nil {
		panic(err)
	}
	chartToken, err := c.Cookie("chartJWT")
	if err != nil {
		panic(err)
	}
	fmt.Println("chartToken", chartToken)
	_ = ws
}

由于我不知道handler.Service.LoginUser()方法的具体作用,所以省略了一些部分。要正确从上下文中读取内容,你必须使用c.MustGet(key)方法。

main.go 文件

package main

import (
	"websocketauth/handlers"
	"websocketauth/middlewares"

	"github.com/gin-gonic/gin"
	"github.com/gorilla/websocket"
)

func main() {
	handlers.Upgrader = websocket.Upgrader{
		ReadBufferSize:  1024,
		WriteBufferSize: 1024,
	}
	gin.SetMode(gin.DebugMode)
	r := gin.Default()

	r.Use(middlewares.VerifyUser())
	r.GET("/join-room", handlers.JoinRoom)
	r.POST("/signin", handlers.LoginUser)

	r.Run(":8000")
}

这是设置逻辑,没有什么值得一提的地方。

如果你还需要其他帮助,请告诉我,谢谢!

英文:

Let me try to help to figure out what was wrong. First, I simplified a little bit your example just to focus only on the relevant parts. If you need something omitted just let me know and I'll update the answer. First, let me start with the JWT token generation/verification which is made within the local auth package.

auth/auth.go file

package auth

import (
	"fmt"
	"strings"
	"time"

	"github.com/golang-jwt/jwt"
)

func ValidateToken(jwtToken string) (*jwt.MapClaims, error) {
	// parse the token
	token, err := jwt.Parse(strings.Replace(jwtToken, "Bearer ", "", 1), func(token *jwt.Token) (interface{}, error) {
		_, ok := token.Method.(*jwt.SigningMethodHMAC)
		if !ok {
			return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
		}
		return []byte("Abcd1234!!"), nil
	})
	// err while parsing the token
	if err != nil {
		return nil, err
	}
	// token valid
	var claims jwt.MapClaims
	var ok bool
	if claims, ok = token.Claims.(jwt.MapClaims); ok && token.Valid {
		return &claims, nil
	}
	return nil, fmt.Errorf("token not valid")
}

func GenerateToken(username, password string) (string, error) {
	// TODO: here you can add logic to check against a DB
	//...

	// create a new token by providing the cryptographic algorithm
	token := jwt.New(jwt.SigningMethodHS256)

	// set default/custom claims
	claims := token.Claims.(jwt.MapClaims)
	claims["exp"] = time.Now().Add(24 * time.Hour * 3).Unix()
	claims["username"] = username
	claims["password"] = password

	tokenString, err := token.SignedString([]byte("Abcd1234!!"))
	if err != nil {
		return "", err
	}
	return tokenString, nil
}

Now, let's move to the middleware part.

middlewares/middlewares.go file

package middlewares

import (
	"net/http"

	"websocketauth/auth"

	"github.com/gin-gonic/gin"
)

func VerifyUser() gin.HandlerFunc {
	return func(c *gin.Context) {
		notAuth := []string{"/signin"}
		requestPath := c.Request.URL.Path
		for _, val := range notAuth {
			if val == requestPath {
				c.Next()
				return
			}
		}

		token, err := c.Cookie("chartJWT")
		if err != nil {
			c.Redirect(http.StatusPermanentRedirect, "/signin")
		}

		claims, err := auth.ValidateToken(token)
		if err != nil {
			c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
			return
		}

		c.Set("jwt", *claims)
		c.Next()
	}
}

To be able to upload something in the context you should use c.Set(key, value) method. Now, let's move to the handlers.

handlers/handlers.go file

package handlers

import (
	"fmt"
	"net/http"

	"websocketauth/auth"

	"github.com/gin-gonic/gin"
	"github.com/golang-jwt/jwt"
	"github.com/gorilla/websocket"
)

var Upgrader websocket.Upgrader

type LoginUserReq struct {
	Username string `json:"username" binding:"required"`
	Password string `json:"password" binding:"required"`
}

func LoginUser(c *gin.Context) {
	var input LoginUserReq
	if err := c.ShouldBind(&input); err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	// I don't know what you do within the handler.Service.LoginUser() method

	token, err := auth.GenerateToken(input.Username, input.Password)
	if err != nil {
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}

	c.SetCookie("chartJWT", token, 60*60*24, "/", "localhost", false, true)
	c.JSON(http.StatusOK, gin.H{"user": token})
}

func JoinRoom(c *gin.Context) {
	claims := c.MustGet("jwt").(jwt.MapClaims)
	fmt.Println("username", claims["username"])
	fmt.Println("password", claims["password"])
	ws, err := Upgrader.Upgrade(c.Writer, c.Request, nil)
	if err != nil {
		panic(err)
	}
	chartToken, err := c.Cookie("chartJWT")
	if err != nil {
		panic(err)
	}
	fmt.Println("chartToken", chartToken)
	_ = ws
}

Missing parts, such as the handler.Service.LoginUser() method are skipped due to the fact that I don't know what they do. To properly read stuff from context, you've to use the c.MustGet(key) method.

main.go file

package main

import (
	"websocketauth/handlers"
	"websocketauth/middlewares"

	"github.com/gin-gonic/gin"
	"github.com/gorilla/websocket"
)

func main() {
	handlers.Upgrader = websocket.Upgrader{
		ReadBufferSize:  1024,
		WriteBufferSize: 1024,
	}
	gin.SetMode(gin.DebugMode)
	r := gin.Default()

	r.Use(middlewares.VerifyUser())
	r.GET("/join-room", handlers.JoinRoom)
	r.POST("/signin", handlers.LoginUser)

	r.Run(":8000")
}

This is the set up logic. Nothing worth mentioning here.
Let me know if you still need other help, thanks!

huangapple
  • 本文由 发表于 2023年6月7日 14:22:59
  • 转载请务必保留本文链接:https://go.coder-hub.com/76420390.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定