使用 gorilla 会话时未保存 golang 中的会话变量
Posted
技术标签:
【中文标题】使用 gorilla 会话时未保存 golang 中的会话变量【英文标题】:Sessions variables in golang not saved while using gorilla sessions 【发布时间】:2014-03-18 21:49:41 【问题描述】:在使用 gorilla 会话 Web 工具包时,不会跨请求维护会话变量。 当我启动服务器并键入 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)
谁能告诉我我做错了什么。提前致谢。
【问题讨论】:
【参考方案1】:首先:你永远不应该使用 md5 来散列密码。 Read this article 为什么,然后使用 Go 的 bcrypt package。您还应该parameterise your SQL queries 否则您将面临灾难性 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 中的“资源”选项卡(资源 > Cookie)对于调试此类问题非常有用,因为您可以看到 cookie 过期、路径和其他设置。
您还检查了session.Values["email"] == nil
,这不起作用。 Go 中的空字符串就是""
,因为session.Values
是map[string]interface
,所以需要将type assert 的值转为字符串:
即
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)
我们处理“不是字符串”的情况,所以如果会话不是我们所期望的(客户端修改了它,或者我们的程序的旧版本使用了不同的类型),我们会明确说明程序应该做什么。
您在保存会话时没有检查错误。
sessionNew.Save(req, res)
...应该是:
err := sessionNew.Save(req, res)
if err != nil
// handle the error case
您应该在SessionHandler
在提供静态文件之前获取/验证会话(不过,您这样做的方式非常迂回):
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/")))
【讨论】:
我添加了 init() 并删除了将路径设置为 /loginSession 的 session.Options,但它仍然不起作用。 它仍然无法正常工作。我在会话处理程序中添加了打印会话变量的打印语句,登录后,这就是我得到的输出。 SessionHandler 中的会话 & map[] 0xc2000f3330 true 0xc2000c0ea0 loginSession 登录后的会话:& map[email:tushr1388@gmail.com name:Tushar] 0xc2001a7930 true SessionHandler 中的会话 & map[] 0xc2001a7c00 true 0xc2000c0ea0Session 最后一个会话当您打开一个新选项卡并输入 localhost:8100/ 时会打印处理程序 @tushR 你能用一个运行版本的代码更新你的问题吗?检查/处理所有与会话相关的错误并考虑我上面的建议? 我在 main() 之外声明了路由器,因此它在全局范围内并且可以在 SessionHandler() 中访问,并且在添加块以检查保存会话中的错误后,它不会进入该块,因此保存会话没有错误。【参考方案2】:问题是您在调用session.Save
之前写信给响应。这可以防止标头被写入,从而防止您的 cookie 被发送到客户端。
在session.Query
之后的代码中,您在响应中调用Fprintf
,一旦此代码执行,调用sessionNew.Save
基本上什么都不做。删除任何写入响应的代码,然后重试。
如果响应已经写入,我猜 gorilla 工具包的会话应该在调用 Save 时返回错误。
【讨论】:
我只是遇到了一个问题,即我的 cookie 没有使用 gorilla/sessions 设置。你的回答很到位。谢谢!【参考方案3】:从评论链开始,请尝试从会话选项中删除 Domain
约束,或将其替换为可解析的 FQDN(例如使用 /etc/hosts
)。
这似乎是 Chromium 中的一个错误,其中未发送具有显式“localhost”域的 cookie。这个问题似乎并没有出现在 Firefox 中。
我能够让你的演示工作使用
store.Options = &sessions.Options
// Domain: "localhost",
MaxAge: 3600 * 1, // 1 hour
HttpOnly: true,
【讨论】:
我用的是火狐,去掉域后还是不行。问题是SessionHandler函数中的store.get()没有返回我们保存在LoginHandler中的已保存会话。 从浏览器中删除现有的cookie,然后设置Domain: ""
(这很糟糕,只能在测试中完成!)。在这样做并硬编码会话值之后,我设法让它工作:/
路由重定向到'/html/home.html. Deleting the session had it re-direct to
/html/login.html`。您的应用程序仍然存在很多问题:您不应该在每次请求时都重新连接到数据库;检查您的会话错误;并且您的静态文件服务器不执行任何操作。阅读golang.org/doc/articles/wiki 并尽早进行重构。
我在前端有html和angularjs,在后端有golang,那么如何在golang中设置会话变量在浏览器中反映为cookies。我是否需要在 angularjs 代码中将会话变量从 golang 设置为 cookie?我在前端没有太多经验,所以请您解释一下在后端创建会话并将它们反映在前端的流程。
@tushR 如果您有一个 $http 调用来进行身份验证,该调用在响应中正确设置了一个 cookie(通过 Set-Cookie 标头),那么对该域的任何进一步 $http 调用都将被“验证”/part的会议。使用开发人员工具来确定 Set-Cookie 是否是登录 HTTP 响应的一部分,然后浏览器是否在随后的 $http 调用中发送相同的 cookie(通过 Cookie 标头)。我对问题的分析是您试图设置一个“本地主机”域 cookie,浏览器会忽略该 cookie,并且不会在后续请求中发送。
@Alex,感谢您的信息,但我的问题仍然存在。我应该在 AngularJS 中设置 cookie 并在 Golang 中使用会话变量吗?【参考方案4】:
在我的情况下,问题是路径。我知道问题不在于它,但是当您搜索 Google 时,此帖子首先出现。所以,我以这样的路径开始会话:
/usuario/login
所以路径设置为 /usuario,然后,当我从 /
发出另一个请求时,cookie 没有设置,因为 / 与 /usuario 不同
我通过指定路径来修复它,我知道这应该很明显,但我花了几个小时才意识到它。所以:
&sessions.Options
MaxAge: 60 * 60 * 24,
HttpOnly: true,
Path: "/", // <-- This is very important
有关一般 cookie 的更多信息:https://developer.mozilla.org/es/docs/Web/HTTP/Cookies
【讨论】:
【参考方案5】:使用服务器端“FilesystemStore”而不是“CookieStore”来保存会话变量。另一种选择是将会话更新为请求的上下文变量,即将会话存储在上下文中,并让浏览器在每个请求中传递它,使用 gorilla/context 包中的 context.Set()。
使用“CookieStore”对客户端来说很繁重,因为随着 cookie 中存储的信息量的增加,每个请求和响应都会通过网络传输更多信息。它的优点是不需要在服务器端存储会话信息。如果在服务器上存储会话信息不是一个约束,理想的方法应该是将登录和身份验证相关信息存储在服务器端的“非 cookie”会话存储中,并将令牌传递给客户端。服务器将维护令牌和会话信息的映射。 “FilesystemStore”允许您执行此操作。
虽然“FilesystemStore”和“CookieStore”都实现了“Store”接口,但它们各自的“Save()”函数的实现略有不同。这两个函数的源代码CookieStore.Save() 和FilesystemStore.Save() 将帮助我们理解为什么“CookieStore”不能持久化会话信息。 FilesystemStore 的 Save() 方法除了将会话信息写入响应头之外,还将信息保存在服务器端会话文件中。在“CookieStore”实现中,如果浏览器无法将新修改的 cookie 从响应中发送到下一个请求,则请求可能会失败。在“FilesystemStore”实现中,提供给浏览器的令牌始终保持不变。会话信息在文件中更新,并在需要时根据请求令牌获取。
【讨论】:
以上是关于使用 gorilla 会话时未保存 golang 中的会话变量的主要内容,如果未能解决你的问题,请参考以下文章
Golang Gorilla mux 与 http.FileServer 返回 404