英文:
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 becausesession.Values
is amap[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.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论