在使用Gorilla Sessions时,Golang中的会话变量未保存。

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

Sessions variables in golang not saved while using gorilla sessions

问题

Session Variables在使用gorilla sessions web toolkit时无法在请求之间保持。当我启动服务器并输入localhost:8100/时,页面会重定向到login.html,因为会话值不存在。登录后,我在存储中设置了会话变量,并将页面重定向到home.html。但是,当我打开一个新标签并输入localhost:8100/时,页面应该使用已存储的会话变量重定向到home.html,但实际上页面被重定向到login.html。

以下是代码:

package main

import (
	"crypto/md5"
	"encoding/hex"
	"fmt"
	"github.com/gocql/gocql"
	"github.com/gorilla/mux"
	"github.com/gorilla/sessions"
	"net/http"
	"time"
)

var store = sessions.NewCookieStore([]byte("something-very-secret"))

var router = mux.NewRouter()

func init() {

	store.Options = &sessions.Options{
		Domain:   "localhost",
		Path:     "/",
		MaxAge:   3600 * 1, // 1 hour
		HttpOnly: true,
	}
}

func main() {
	//session handling
	router.HandleFunc("/", SessionHandler)
	router.HandleFunc("/signIn", SignInHandler)
	router.HandleFunc("/signUp", SignUpHandler)
	router.HandleFunc("/logOut", LogOutHandler)
	http.Handle("/", router)
	http.ListenAndServe(":8100", nil)
}

//handler for signIn
func SignInHandler(res http.ResponseWriter, req *http.Request) {

	email := req.FormValue("email")
	password := req.FormValue("password")

	//Generate hash of password
	hasher := md5.New()
	hasher.Write([]byte(password))
	encrypted_password := hex.EncodeToString(hasher.Sum(nil))

	//cassandra connection
	cluster := gocql.NewCluster("localhost")
	cluster.Keyspace = "gbuy"
	cluster.DefaultPort = 9042
	cluster.Consistency = gocql.Quorum
	session, _ := cluster.CreateSession()
	defer session.Close()

	//select query
	var firstname string
	stmt := "SELECT firstname FROM USER WHERE email= '" + email + "' and password ='" + encrypted_password + "';"
	err := session.Query(stmt).Scan(&firstname)
	if err != nil {
		fmt.Fprintf(res, "failed")
	} else {
		if firstname == "" {
			fmt.Fprintf(res, "failed")
		} else {
			fmt.Fprintf(res, firstname)
		}
	}

	//store in session variable
	sessionNew, _ := store.Get(req, "loginSession")

	// Set some session values.
	sessionNew.Values["email"] = email
	sessionNew.Values["name"] = firstname

	// Save it.
	sessionNew.Save(req, res)
	//store.Save(req,res,sessionNew)

	fmt.Println("Session after logging:")
	fmt.Println(sessionNew)

}

//handler for signUp
func SignUpHandler(res http.ResponseWriter, req *http.Request) {

	fName := req.FormValue("fName")
	lName := req.FormValue("lName")
	email := req.FormValue("email")
	password := req.FormValue("passwd")
	birthdate := req.FormValue("date")
	city := req.FormValue("city")
	gender := req.FormValue("gender")

	//Get current timestamp and format it.
	sysdate := time.Now().Format("2006-01-02 15:04:05-0700")

	//Generate hash of password
	hasher := md5.New()
	hasher.Write([]byte(password))
	encrypted_password := hex.EncodeToString(hasher.Sum(nil))

	//cassandra connection
	cluster := gocql.NewCluster("localhost")
	cluster.Keyspace = "gbuy"
	cluster.DefaultPort = 9042
	cluster.Consistency = gocql.Quorum
	session, _ := cluster.CreateSession()
	defer session.Close()

	//Insert the data into the Table
	stmt := "INSERT INTO USER (email,firstname,lastname,birthdate,city,gender,password,creation_date) VALUES ('" + email + "','" + fName + "','" + lName + "','" + birthdate + "','" + city + "','" + gender + "','" + encrypted_password + "','" + sysdate + "');"
	fmt.Println(stmt)
	err := session.Query(stmt).Exec()
	if err != nil {
		fmt.Fprintf(res, "failed")
	} else {
		fmt.Fprintf(res, fName)
	}
}

//handler for logOut
func LogOutHandler(res http.ResponseWriter, req *http.Request) {
	sessionOld, err := store.Get(req, "loginSession")

	fmt.Println("Session in logout")
	fmt.Println(sessionOld)
	if err = sessionOld.Save(req, res); err != nil {
		fmt.Println("Error saving session: %v", err)
	}
}

//handler for Session
func SessionHandler(res http.ResponseWriter, req *http.Request) {

	router.PathPrefix("/").Handler(http.FileServer(http.Dir("../static/")))
	session, _ := store.Get(req, "loginSession")

	fmt.Println("Session in SessionHandler")
	fmt.Println(session)



	if val, ok := session.Values["email"].(string); ok {
		// if val is a string
		switch val {
		case "": {
			http.Redirect(res, req, "html/login.html", http.StatusFound) }
		default:
			http.Redirect(res, req, "html/home.html", http.StatusFound)
		}
	} else {
		// if val is not a string type
		http.Redirect(res, req, "html/login.html", http.StatusFound)
	}
}

有人能告诉我我做错了什么吗?提前谢谢。

英文:

Session Variables are not maintained across request while using gorilla sessions web toolkit.
When I start the server and type localhost:8100/ page is directed to login.html since session values do not exist.After I login I set the session variable in the store and the page is redirected to home.html. But when I open a new tab and type localhost:8100/ the page should be directed to home.html using already stored session variables, but the page is instead redirected to login.html.
Following is the code.

    package main
import (
"crypto/md5"
"encoding/hex"
"fmt"
"github.com/gocql/gocql"
"github.com/gorilla/mux"
"github.com/gorilla/sessions"
"net/http"
"time"
)
var store = sessions.NewCookieStore([]byte("something-very-secret"))
var router = mux.NewRouter()
func init() {
store.Options = &sessions.Options{
Domain:   "localhost",
Path:     "/",
MaxAge:   3600 * 1, // 1 hour
HttpOnly: true,
}
}
func main() {
//session handling
router.HandleFunc("/", SessionHandler)
router.HandleFunc("/signIn", SignInHandler)
router.HandleFunc("/signUp", SignUpHandler)
router.HandleFunc("/logOut", LogOutHandler)
http.Handle("/", router)
http.ListenAndServe(":8100", nil)
}
//handler for signIn
func SignInHandler(res http.ResponseWriter, req *http.Request) {
email := req.FormValue("email")
password := req.FormValue("password")
//Generate hash of password
hasher := md5.New()
hasher.Write([]byte(password))
encrypted_password := hex.EncodeToString(hasher.Sum(nil))
//cassandra connection
cluster := gocql.NewCluster("localhost")
cluster.Keyspace = "gbuy"
cluster.DefaultPort = 9042
cluster.Consistency = gocql.Quorum
session, _ := cluster.CreateSession()
defer session.Close()
//select query
var firstname string
stmt := "SELECT firstname FROM USER WHERE email= '" + email + "' and password ='" + encrypted_password + "';"
err := session.Query(stmt).Scan(&firstname)
if err != nil {
fmt.Fprintf(res, "failed")
} else {
if firstname == "" {
fmt.Fprintf(res, "failed")
} else {
fmt.Fprintf(res, firstname)
}
}
//store in session variable
sessionNew, _ := store.Get(req, "loginSession")
// Set some session values.
sessionNew.Values["email"] = email
sessionNew.Values["name"] = firstname
// Save it.
sessionNew.Save(req, res)
//store.Save(req,res,sessionNew)
fmt.Println("Session after logging:")
fmt.Println(sessionNew)
}
//handler for signUp
func SignUpHandler(res http.ResponseWriter, req *http.Request) {
fName := req.FormValue("fName")
lName := req.FormValue("lName")
email := req.FormValue("email")
password := req.FormValue("passwd")
birthdate := req.FormValue("date")
city := req.FormValue("city")
gender := req.FormValue("gender")
//Get current timestamp and format it.
sysdate := time.Now().Format("2006-01-02 15:04:05-0700")
//Generate hash of password
hasher := md5.New()
hasher.Write([]byte(password))
encrypted_password := hex.EncodeToString(hasher.Sum(nil))
//cassandra connection
cluster := gocql.NewCluster("localhost")
cluster.Keyspace = "gbuy"
cluster.DefaultPort = 9042
cluster.Consistency = gocql.Quorum
session, _ := cluster.CreateSession()
defer session.Close()
//Insert the data into the Table
stmt := "INSERT INTO USER (email,firstname,lastname,birthdate,city,gender,password,creation_date) VALUES ('" + email + "','" + fName + "','" + lName + "','" + birthdate + "','" + city + "','" + gender + "','" + encrypted_password + "','" + sysdate + "');"
fmt.Println(stmt)
err := session.Query(stmt).Exec()
if err != nil {
fmt.Fprintf(res, "failed")
} else {
fmt.Fprintf(res, fName)
}
}
//handler for logOut
func LogOutHandler(res http.ResponseWriter, req *http.Request) {
sessionOld, err := store.Get(req, "loginSession")
fmt.Println("Session in logout")
fmt.Println(sessionOld)
if err = sessionOld.Save(req, res); err != nil {
fmt.Println("Error saving session: %v", err)
}
}
//handler for Session
func SessionHandler(res http.ResponseWriter, req *http.Request) {
router.PathPrefix("/").Handler(http.FileServer(http.Dir("../static/")))
session, _ := store.Get(req, "loginSession")
fmt.Println("Session in SessionHandler")
fmt.Println(session)
if val, ok := session.Values["email"].(string); ok {
// if val is a string
switch val {
case "": {
http.Redirect(res, req, "html/login.html", http.StatusFound) }
default:
http.Redirect(res, req, "html/home.html", http.StatusFound)
}
} else {
// if val is not a string type
http.Redirect(res, req, "html/login.html", http.StatusFound)
}
}

Can somebody tell me what I am doing wrong. Thanks in advance.

答案1

得分: 29

首先,你绝对不应该使用md5来哈希密码。阅读一下这篇文章,了解一下为什么,然后使用Go的bcrypt包。此外,你应该对SQL查询进行参数化处理,否则你将容易受到灾难性的SQL注入攻击。

无论如何,你需要解决以下几个问题:

  • 你的会话无法“保持”,原因是你将Path设置为/loginSession,所以当用户访问任何其他路径(如/)时,会话对于该范围无效。

你应该在程序初始化时设置会话存储并在那里设置选项:

var store = sessions.NewCookieStore([]byte("something-very-secret"))

func init() {
    store.Options = &sessions.Options{
        Domain:   "localhost",
        Path:     "/",
        MaxAge:   3600 * 8, // 8 hours
        HttpOnly: true,
    }
}

如果已登录用户始终位于类似/accounts的子路径中,那么你可能会设置一个更具体的路径。但在你的情况下,这不是发生的情况。

我还应该补充一点,Chrome的Web Inspector中的“资源”选项卡(资源 > Cookies)非常有用,可以用于调试此类问题,因为你可以查看cookie的过期时间、路径和其他设置。

  • 你还在检查session.Values["email"] == nil,这是不起作用的。在Go中,空字符串只是"",因为session.Values是一个map[string]interface{},所以你需要将值断言为字符串类型:

例如:

if val, ok := session.Values["email"].(string); ok {
    // 如果val是一个字符串
    switch val {
    case "":
        http.Redirect(res, req, "html/login.html", http.StatusFound)
    default:
        http.Redirect(res, req, "html/home.html", http.StatusFound)
    }
} else {
    // 如果val不是字符串类型
    http.Redirect(res, req, "html/login.html", http.StatusFound)
}

我们处理“不是字符串”情况,以便明确指定如果会话不符合我们的预期(客户端修改了它,或者我们程序的旧版本使用了不同的类型),程序应该做什么。

  • 保存会话时你没有检查错误。
sessionNew.Save(req, res)

应该改为:

err := sessionNew.Save(req, res)
if err != nil {
    // 处理错误情况
}
  • 在提供静态文件之前,你应该在SessionHandler中获取/验证会话(不过你的方法非常绕):
func SessionHandler(res http.ResponseWriter, req *http.Request) {
    session, err := store.Get(req, "loginSession")
    if err != nil {
        // 处理错误
    }

    if session.Values["email"] == nil {
        http.Redirect(res, req, "html/login.html", http.StatusFound)
    } else {
        http.Redirect(res, req, "html/home.html", http.StatusFound)
    }
    // 这里不应该有这个 - 路由器在这个函数中没有作用域!你应该在main()中设置它,并用一个检查有效会话的函数包装它。
    router.PathPrefix("/").Handler(http.FileServer(http.Dir("../static/")))
}
英文:

First up: you should never, ever, use md5 to hash passwords. Read this article on why, and then use Go's bcrypt package. You should also parameterise your SQL queries else you are open to catastrophic SQL injection attacks.

Anyway: there are a few problems you need to address here:

  • Your sessions aren't "sticking" is that you're setting the Path as /loginSession - so when a user visits any other path (i.e. /), the session isn't valid for that scope.

You should be setting up a session store on program initialisation and setting the options there:

var store = sessions.NewCookieStore([]byte("something-very-secret"))
func init() {
store.Options = &sessions.Options{
Domain:   "localhost",
Path:     "/",
MaxAge:   3600 * 8, // 8 hours
HttpOnly: true,
}

The reason you might set a more specific path is if logged in users are always within a sub-route like /accounts. In your case, that's not what's happening.

I should add that Chrome's "Resource" tab in the Web Inspector (Resources > Cookies) is incredibly useful for debugging issues like these as you can see the cookie expiry, path and other settings.

  • You're also checking session.Values["email"] == nil, which doesn't work. An empty string in Go is just "", and because session.Values is a map[string]interface{}, you need to type assert the value to a string:

i.e.

if val, ok := session.Values["email"].(string); ok {
// if val is a string
switch val {
case "":
http.Redirect(res, req, "html/login.html", http.StatusFound)
default:
http.Redirect(res, req, "html/home.html", http.StatusFound)
}
} else {
// if val is not a string type
http.Redirect(res, req, "html/login.html", http.StatusFound)
}

We deal with the "not a string" case so we're explicit about what the program should do if the session is not how we expected (client modified it, or an older version of our program used a different type).

  • You are not checking errors when saving your sessions.

      sessionNew.Save(req, res)
    

... should be:

    err := sessionNew.Save(req, res)
if err != nil {
// handle the error case
}
  • You should get/validate the session in SessionHandler before serving static files (you are doing it in a very roundabout way, however):

      func SessionHandler(res http.ResponseWriter, req *http.Request) {
    session, err := store.Get(req, "loginSession")
    if err != nil {
    // Handle the error
    }
    if session.Values["email"] == nil {
    http.Redirect(res, req, "html/login.html", http.StatusFound)
    } else {
    http.Redirect(res, req, "html/home.html", http.StatusFound)
    }
    // This shouldn't be here - router isn't scoped in this function! You should set this in your main() and wrap it with a function that checks for a valid session.
    router.PathPrefix("/").Handler(http.FileServer(http.Dir("../static/")))
    }
    

答案2

得分: 6

问题在于在调用session.Save之前,你已经向响应中写入了内容。这样会阻止头部信息被写入,因此无法将cookie发送给客户端。

session.Query之后的代码中,你在响应上调用了Fprintf,一旦执行了这段代码,调用sessionNew.Save实际上没有任何效果。移除任何向响应中写入内容的代码,然后再试一次。

我猜测gorilla toolkit的session在调用Save时,如果响应已经被写入,应该返回一个错误。

英文:

The problem is you're writing to the response before calling session.Save. That prevents the headers from being written and thus your cookie from being sent to the client.

In the code after session.Query you're calling Fprintf on the response, as soon as this code executes, calling sessionNew.Save essentially does nothing. Remove any code that writes to the response and try again.

I guess gorilla toolkit's session ought to return an error when calling Save if the response has already been written to.

答案3

得分: 2

根据评论链的内容,请尝试从会话选项中删除Domain约束,或者用一个可以解析的完全限定域名(例如使用/etc/hosts)来替换它。

这似乎是Chromium中的一个错误,当使用显式的“localhost”域的cookie时,它们不会被发送。这个问题在Firefox中似乎没有出现。

我成功地使用以下代码使您的演示工作起来:

store.Options = &sessions.Options{
    // Domain: "localhost",
    MaxAge:   3600 * 1, // 1小时
    HttpOnly: true,
}
英文:

Following on from the comment chain, please try removing the Domain constraint from the session options, or replace it with a FQDN that resolves (using /etc/hosts for example).

This appears to be a bug in Chromium where cookies with an explicit 'localhost' domain aren't sent. The issue doesn't seem to present itself in Firefox.

I was able to get your demo working using

store.Options = &sessions.Options{
// Domain: "localhost",
MaxAge:   3600 * 1, // 1 hour
HttpOnly: true,
}

答案4

得分: 2

在我的情况下,问题出在路径上。我知道这个问题与此无关,但是当你在谷歌搜索时,这篇帖子会首先出现。所以,我在一个路径下启动了会话,例如:

/usuario/login

所以路径被设置为/usuario,然后,当我从/发出另一个请求时,cookie没有被设置,因为 /不等于/usuario

我通过指定一个路径来修复了这个问题,我知道这应该是显而易见的,但是我花了几个小时才意识到这一点。所以:

&sessions.Options{
MaxAge:   60 * 60 * 24,
HttpOnly: true,
Path:     "/", // <-- 这一点非常重要
}

有关通用 cookie 的更多信息,请参阅:https://developer.mozilla.org/es/docs/Web/HTTP/Cookies

英文:

In my case the problem was the Path. I know the question is not about it, but this post appears first when you search Google. So, I was starting the session in a path like:

/usuario/login

So the path was set to /usuario, and then, when I made another requests from / the cookie was not set because / is not same as /usuario

I fixed it by specifying a Path, i know this should be obvious but took me some hours to realize it. So:

&sessions.Options{
MaxAge:   60 * 60 * 24,
HttpOnly: true,
Path:     "/", // <-- This is very important
}

More info about general cookies: https://developer.mozilla.org/es/docs/Web/HTTP/Cookies

答案5

得分: 0

使用服务器端的“FilesystemStore”而不是“CookieStore”来保存会话变量。另一种选择是将会话更新为请求的上下文变量,即将会话存储在上下文中,并让浏览器在每个请求中传递它,使用gorilla/context包中的context.Set()方法。

使用“CookieStore”对客户端来说是重量级的,因为随着存储在cookie中的信息量增加,每个请求和响应都会传输更多的信息。它的优势在于无需在服务器端存储会话信息。如果在服务器上存储会话信息不是一个限制,理想的方式应该是将登录和身份验证相关的信息存储在服务器端的“非cookie”会话存储中,并只传递一个令牌给客户端。服务器将维护一个令牌和会话信息的映射关系。使用“FilesystemStore”可以实现这一点。

尽管“FilesystemStore”和“CookieStore”都实现了“Store”接口,但它们的“Save()”函数的实现略有不同。查看这两个函数的源代码,CookieStore.Save()FilesystemStore.Save(),将帮助我们理解为什么“CookieStore”无法持久化会话信息。FilesystemStore的Save()方法除了将会话信息写入响应头之外,还将信息保存在服务器端的会话文件中。在“CookieStore”实现中,如果浏览器无法将新修改的cookie从响应发送到下一个请求,请求可能会失败。在“FilesystemStore”实现中,给浏览器的令牌始终保持不变。会话信息在文件中更新,并根据请求的令牌在需要时进行获取。

英文:

Use a server side "FilesystemStore" instead of a "CookieStore" to save the session variables. Another alternative would be to update the session as a context variable for the request i.e., store the session in the context and let the browser pass it around in every request, using the context.Set() from the gorilla/context package.

Using "CookieStore" is heavy for the client because as the amount of information stored in the cookie grows, more information is transmitted over the wire for every request and response. The advantage it serves is that there is no need to store the session information on the server side. If it is not a constraint to store session information on the server, the ideal way should be to store login and authentication related information on a server side "non-cookie" session store and just pass a token to the client. The server would maintain a map of the token and session information. The "FilesystemStore" allows you to do this.

Though both the "FilesystemStore" and "CookieStore" implement the "Store" interface, each of their "Save()" function's implementations are slightly different. The source code for both the functions, CookieStore.Save() and FilesystemStore.Save() will help us understand why "CookieStore" is not able to persist the session information. The FilesystemStore's Save() method apart from writing the session information to the response header, also saves the information on the server side session file. In a "CookieStore" implementation, if the browser is not able to send the new modified cookie from a response to the next request, the request might fail. In a "FilesystemStore" implementation, the token that is given to the browser always remains the same. The session information is updated in a file and is fetched based on the requesting token, whenever required.

huangapple
  • 本文由 发表于 2014年2月19日 05:24:41
  • 转载请务必保留本文链接:https://go.coder-hub.com/21865681.html
匿名

发表评论

匿名网友

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

确定