如何在登录后清除会话并仅访问关于页面?

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

How to clear the session and only visit an about page after login?

问题

这段代码有两个部分。第一部分是设置和清除会话,第二部分是登录和注销。

它的功能是什么?

在第二部分中,如果在数据库中找到了电子邮件和密码,并且匹配为true,则设置会话并转到具有关于文件的about()函数。如果调用了注销,则清除会话并重定向到主页。

它应该做什么?

问题是,即使我已注销并清除了会话,我仍然可以访问关于页面。如果我未登录,我不希望被允许访问关于页面。

第一部分

var cookieHandler = securecookie.New(
	securecookie.GenerateRandomKey(64),
	securecookie.GenerateRandomKey(32),
)

func setSession(email, password string, res http.ResponseWriter) {
	value := map[string]string{ "email": email, "password": password}
	encoded, err := cookieHandler.Encode("session", value)
	if err == nil {
		cookie := &http.Cookie{ Name: "session", Value: encoded, Path: "/"}
		http.SetCookie(res, cookie)
	}
}

func clearSession(res http.ResponseWriter) {
	cookie := &http.Cookie{ Name: "session", Value: "", Path: "/", MaxAge: -1}
	http.SetCookie(res, cookie)
}

第二部分

func about(res http.ResponseWriter, req *http.Request) {
	if err := tmpl.ExecuteTemplate(res, "about.html", nil); err != nil {
		log.Fatal("template didn't execute", nil)
	}
}

func loginAuth(res http.ResponseWriter, req *http.Request) {
	email := req.FormValue("email")
	password := req.FormValue("password")
	match := database.Findaccount(email, password)
	if match == true {
		setSession(email, password, res)
		about(res, req)
		fmt.Println("You're logged in")
	} else {
		tmpl.ExecuteTemplate(res, "login.html", nil)
		fmt.Println("Enter the correct email or password")
	}
}

func logout(res http.ResponseWriter, req *http.Request) {
	clearSession(res)
	http.Redirect(res, req, "/", 302)
}
英文:

This code has two parts. One is to set and clear the session and the second part is login and logout.

What it does?

In the second part, If an email and password are found in the database and the match is true then it set the session and move to the about() function which has an about file. If the logout is called then it clears the session and redirects to the home page.

What it should do?

The problem is that even if I am logged out and the session is cleared, I can still visit an about page. I don't want to be allowed to visit an about page if I am not logged in.

First part

var cookieHandler = securecookie.New(
	securecookie.GenerateRandomKey(64),
	securecookie.GenerateRandomKey(32),
)

func setSession(email, password string, res http.ResponseWriter) {
	value := map[string]string{ "email": email, "password": password}
	encoded, err := cookieHandler.Encode("session", value)
	if err == nil {
		cookie := &http.Cookie{ Name:  "session", Value: encoded, Path:  "/"}
		http.SetCookie(res, cookie)
	}
}

func clearSession(res http.ResponseWriter) {
	cookie := &http.Cookie{ Name: "session", Value: "", Path: "/", MaxAge: -1}
	http.SetCookie(res, cookie)
}

Second part

func about(res http.ResponseWriter, req *http.Request) {
	if err := tmpl.ExecuteTemplate(res, "about.html", nil); err != nil {
		log.Fatal("template didn't execute", nil)
	}
}

func loginAuth(res http.ResponseWriter, req *http.Request) {
	email := req.FormValue("email")
	password := req.FormValue("password")
	match := database.Findaccount(email, password)
	if match == true {
		setSession(email, password, res)
		about(res, req)
		fmt.Println("You're logged in")
	} else {
		tmpl.ExecuteTemplate(res, "login.html", nil)
		fmt.Println("Enter the correct email or password")
	}
}

func logout(res http.ResponseWriter, req *http.Request) {
	clearSession(res)
	http.Redirect(res, req, "/", 302)
}

答案1

得分: 2

一般情况下,有几件事情你不应该做:

  • 不要直接使用cookie编码器,而是使用cookie会话存储。
  • 不要在处理程序内部调用另一个处理程序,而是使用重定向。这样可以防止在响应中重复写入头部/正文。
  • 不要将用户/密码传递给cookie,即使经过编码,在2021年,我们甚至可能完全阻止通过表单发送这些信息(你可以考虑只发送哈希值,并在服务器端重新哈希哈希值以确定是否可以继续)。

总的来说,有几件事情你总是要做的:

  • 编写测试。
  • 使用中间件。
  • 始终提供小的可重现示例。

话虽如此,我已经写了一个类似的代码,并添加了一些存根(主要是为了数据库),我删除了模板支持(我没有心情编写HTML),更重要的是,我编写了测试!

对于问题“如何清除会话”:
从存储中删除值,写入存储。

对于问题“只有在登录后才能访问关于页面?”:
使用一个中间件将该处理程序包装起来,该中间件验证附加到用户cookie存储的登录数据。

-- main.go --
package main

import (
	"crypto/sha256"
	"encoding/gob"
	"fmt"
	"log"
	"net/http"

	"github.com/gorilla/mux"
	"github.com/gorilla/securecookie"
	"github.com/gorilla/sessions"
)

// 注意:不要将密钥存储在源代码中。通过环境变量或标志(或两者)传递它,并且不要意外地将其与代码一起提交。确保密钥足够随机-即使用Go的crypto/rand或securecookie.GenerateRandomKey(32)并持久化结果。

var store = sessions.NewCookieStore(
	securecookie.GenerateRandomKey(32),
)

// 模拟db包
func dbLookupUser(user, pwd string) bool {
	return user == "user" && pwd == "pwd"
}
func dbLookupHash(h string) bool {
	return h == hash("user", "pwd")
}

func hash(s ...interface{}) string {
	hr := sha256.New()
	fmt.Fprint(hr, s...)
	return fmt.Sprintf("%x", hr.Sum(nil))
}

// hashKey是会话映射存储的类型化键,以防止意外覆盖。
type hashKey string

func init() {
	gob.Register(hashKey(""))
}

func loginAuth(res http.ResponseWriter, req *http.Request) {
	email := req.FormValue("email")
	password := req.FormValue("password")
	match := dbLookupUser(email, password)
	if match == true {
		session, _ := store.Get(req, "session-name")
		session.Values["hash"] = hash(email, password)
		// 在写入响应/从处理程序返回之前保存它。
		err := session.Save(req, res)
		if err == nil {
			// about(res, req) // 不要这样做!
			// 关于处理程序可能希望设置自己的http响应头
			// 这将与我们在这里做的冲突。
			// 最好使用重定向
			http.Redirect(res, req, "/about", http.StatusFound)
			return
		}
	} else {
		fmt.Fprintf(res, "请重试") // 使用模板代替!
		// tmpl.ExecuteTemplate(res, "login.html", nil)
	}
}

func logout(res http.ResponseWriter, req *http.Request) {
	session, _ := store.Get(req, "session-name")
	delete(session.Values, hashKey("hash"))
	_ = session.Save(req, res)
	http.Redirect(res, req, "/", 302)
}

func about(res http.ResponseWriter, req *http.Request) {
	fmt.Fprintf(res, "欢迎访问关于页面")
}

func requireLogin(next http.HandlerFunc) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		session, _ := store.Get(r, "session-name")
		var h string
		if x, ok := session.Values[hashKey("hash")]; ok {
			h = x.(string)
		}

		var match bool
		if h != "" {
			match = dbLookupHash(h)
		}

		if !match {
			// 写入错误并停止处理程序链
			http.Error(w, "禁止访问", http.StatusForbidden)
			return
		}
		next(w, r)
	}
}

func main() {

	r := mux.NewRouter()
	r.HandleFunc("/", loginAuth)
	r.HandleFunc("/logout", logout)
	r.HandleFunc("/about", requireLogin(about))

	log.Fatal(http.ListenAndServe("localhost:8080", r))
}


-- main_test.go --
package main

import (
	"net/http"
	"net/http/httptest"
	"net/url"
	"testing"
)

func TestLogin(t *testing.T) {

	req := httptest.NewRequest("POST", "http://example.com/foo", nil)
	form := url.Values{}
	form.Set("email", "user")
	form.Set("password", "pwd")
	req.Form = form

	w := httptest.NewRecorder()
	loginAuth(w, req)

	resp := w.Result()
	// body, _ := io.ReadAll(resp.Body)

	if wanted := http.StatusFound; resp.StatusCode != wanted {
		t.Fatalf("无效的响应代码,得到=%v,期望=%v", resp.StatusCode, wanted)
	}
	// 实现更多检查
}

func TestLoginFailure(t *testing.T) {

	req := httptest.NewRequest("POST", "http://example.com/foo", nil)
	form := url.Values{}
	form.Set("email", "!user")
	form.Set("password", "!pwd")
	req.Form = form

	w := httptest.NewRecorder()
	loginAuth(w, req)

	resp := w.Result()
	// body, _ := io.ReadAll(resp.Body)

	if wanted := http.StatusOK; resp.StatusCode != wanted {
		t.Fatalf("无效的响应代码,得到=%v,期望=%v", resp.StatusCode, wanted)
	}
	// 实现更多检查
}

func TestAboutNotLogged(t *testing.T) {

	req := httptest.NewRequest("POST", "http://example.com/foo", nil)

	w := httptest.NewRecorder()
	requireLogin(about)(w, req)

	resp := w.Result()
	// body, _ := io.ReadAll(resp.Body)

	if wanted := http.StatusForbidden; resp.StatusCode != wanted {
		t.Fatalf("无效的响应代码,得到=%v,期望=%v", resp.StatusCode, wanted)
	}
	// 实现更多检查
}

func TestAboutLogged(t *testing.T) {

	req := httptest.NewRequest("POST", "http://example.com/foo", nil)

	w := httptest.NewRecorder()
	session, _ := store.Get(req, "session-name")
	session.Values[hashKey("hash")] = hash("user", "pwd")
	err := session.Save(req, w)
	if err != nil {
		t.Fatal(err)
	}

	hdr := w.Header()
	req.Header.Add("Cookie", hdr["Set-Cookie"][0])

	w = httptest.NewRecorder()
	requireLogin(about)(w, req)

	resp := w.Result()
	// body, _ := io.ReadAll(resp.Body)

	if wanted := http.StatusOK; resp.StatusCode != wanted {
		t.Fatalf("无效的响应代码,得到=%v,期望=%v", resp.StatusCode, wanted)
	}
	// 实现更多检查
}

英文:

Few things you don't want to do, in general:

  • Don't use cookie encoder directly. Use a cookie session store.
  • Don't call an handler within an handler, prefer a redirect. This should prevent writing twice the headers/body on the response.
  • Don't pass the user/password in the cookie, even encoded, in 2021 we may even prevent sending that through the form at all (you might consider sending only a hash and re hash the hash on the server side to figure out if things are good to go).

Few things you always want to do:

  • Write tests.
  • Make use of middlewares.
  • Always provide small reproducible examples.

That being said, I have written a lookalike code with some stubs (mostly for db), I removed template support (i was not in the mood to write HTML) and more importantly I wrote tests !!

To the question How to clear the session :
Delete the values from the store, write the store

To the question and only visit an about page after login?:
Wrap that handler with a middleware that verifies login data attached to the user cookie store

-- main.go --
package main

import (
	"crypto/sha256"
	"encoding/gob"
	"fmt"
	"log"
	"net/http"

	"github.com/gorilla/mux"
	"github.com/gorilla/securecookie"
	"github.com/gorilla/sessions"
)

// Note: Don't store your key in your source code. Pass it via an
// environmental variable, or flag (or both), and don't accidentally commit it
// alongside your code. Ensure your key is sufficiently random - i.e. use Go's
// crypto/rand or securecookie.GenerateRandomKey(32) and persist the result.

var store = sessions.NewCookieStore(
	securecookie.GenerateRandomKey(32),
)

//emulate db package
func dbLookupUser(user, pwd string) bool {
	return user == "user" && pwd == "pwd"
}
func dbLookupHash(h string) bool {
	return h == hash("user", "pwd")
}

func hash(s ...interface{}) string {
	hr := sha256.New()
	fmt.Fprint(hr, s...)
	return fmt.Sprintf("%x", hr.Sum(nil))
}

// hashKey is a typed key for the session map store to prevent unintented overwrites.
type hashKey string

func init() {
	gob.Register(hashKey(""))
}

func loginAuth(res http.ResponseWriter, req *http.Request) {
	email := req.FormValue("email")
	password := req.FormValue("password")
	match := dbLookupUser(email, password)
	if match == true {
		session, _ := store.Get(req, "session-name")
		session.Values["hash"] = hash(email, password)
		// Save it before we write to the response/return from the handler.
		err := session.Save(req, res)
		if err == nil {
			// about(res, req) // don't!
			// the about handler might want to setup its own http response headers
			// That would conflict with what we did here.
			// prefer a redirect
			http.Redirect(res, req, "/about", http.StatusFound)
			return
		}
	} else {
		fmt.Fprintf(res, "try again") // use a templatee instead!
		// tmpl.ExecuteTemplate(res, "login.html", nil)
	}
}

func logout(res http.ResponseWriter, req *http.Request) {
	session, _ := store.Get(req, "session-name")
	delete(session.Values, hashKey("hash"))
	_ = session.Save(req, res)
	http.Redirect(res, req, "/", 302)
}

func about(res http.ResponseWriter, req *http.Request) {
	fmt.Fprintf(res, "welcome to about page")
}

func requireLogin(next http.HandlerFunc) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		session, _ := store.Get(r, "session-name")
		var h string
		if x, ok := session.Values[hashKey("hash")]; ok {
			h = x.(string)
		}

		var match bool
		if h != "" {
			match = dbLookupHash(h)
		}

		if !match {
			// Write an error and stop the handler chain
			http.Error(w, "Forbidden", http.StatusForbidden)
			return
		}
		next(w, r)
	}
}

func main() {

	r := mux.NewRouter()
	r.HandleFunc("/", loginAuth)
	r.HandleFunc("/logout", logout)
	r.HandleFunc("/about", requireLogin(about))

	log.Fatal(http.ListenAndServe("localhost:8080", r))
}


-- main_test.go --
package main

import (
	"net/http"
	"net/http/httptest"
	"net/url"
	"testing"
)

func TestLogin(t *testing.T) {

	req := httptest.NewRequest("POST", "http://example.com/foo", nil)
	form := url.Values{}
	form.Set("email", "user")
	form.Set("password", "pwd")
	req.Form = form

	w := httptest.NewRecorder()
	loginAuth(w, req)

	resp := w.Result()
	// body, _ := io.ReadAll(resp.Body)

	if wanted := http.StatusFound; resp.StatusCode != wanted {
		t.Fatalf("invalid response code, got=%v wanted=%v", resp.StatusCode, wanted)
	}
	// implement more check
}

func TestLoginFailure(t *testing.T) {

	req := httptest.NewRequest("POST", "http://example.com/foo", nil)
	form := url.Values{}
	form.Set("email", "!user")
	form.Set("password", "!pwd")
	req.Form = form

	w := httptest.NewRecorder()
	loginAuth(w, req)

	resp := w.Result()
	// body, _ := io.ReadAll(resp.Body)

	if wanted := http.StatusOK; resp.StatusCode != wanted {
		t.Fatalf("invalid response code, got=%v wanted=%v", resp.StatusCode, wanted)
	}
	// implement more check
}

func TestAboutNotLogged(t *testing.T) {

	req := httptest.NewRequest("POST", "http://example.com/foo", nil)

	w := httptest.NewRecorder()
	requireLogin(about)(w, req)

	resp := w.Result()
	// body, _ := io.ReadAll(resp.Body)

	if wanted := http.StatusForbidden; resp.StatusCode != wanted {
		t.Fatalf("invalid response code, got=%v wanted=%v", resp.StatusCode, wanted)
	}
	// implement more check
}

func TestAboutLogged(t *testing.T) {

	req := httptest.NewRequest("POST", "http://example.com/foo", nil)

	w := httptest.NewRecorder()
	session, _ := store.Get(req, "session-name")
	session.Values[hashKey("hash")] = hash("user", "pwd")
	err := session.Save(req, w)
	if err != nil {
		t.Fatal(err)
	}

	hdr := w.Header()
	req.Header.Add("Cookie", hdr["Set-Cookie"][0])

	w = httptest.NewRecorder()
	requireLogin(about)(w, req)

	resp := w.Result()
	// body, _ := io.ReadAll(resp.Body)

	if wanted := http.StatusOK; resp.StatusCode != wanted {
		t.Fatalf("invalid response code, got=%v wanted=%v", resp.StatusCode, wanted)
	}
	// implement more check
}

huangapple
  • 本文由 发表于 2021年8月7日 16:12:17
  • 转载请务必保留本文链接:https://go.coder-hub.com/68690550.html
匿名

发表评论

匿名网友

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

确定