golang高性能可定制HTTP会话管理插件库sessions的使用

Golang高性能可定制HTTP会话管理插件库sessions的使用

Build Status Coverage Status Go Report Card GoDoc

简介

这是一个非常简单、高性能且高度可定制的Go HTTP服务器会话服务库。默认情况下,该服务将会话存储在redis中,并通过cookie将会话传输给客户端。但这些都可以轻松自定义。

快速开始

var sesh *sessions.Service

// 发布一个新会话并将其写入ResponseWriter
userSession, err := sesh.IssueUserSession("fakeUserID", "{\"foo\":\"bar\"}", w)
if err != nil {
    log.Printf("Err issuing user session: %v\n", err)
    http.Error(w, "Internal Server Error", http.StatusInternalServerError)
    return
}

...

// 从请求中获取有效的用户会话指针。nil指针表示没有或无效的会话
userSession, err := sesh.GetUserSession(r)
if err != nil {
    log.Printf("Err fetching user session: %v\n", err)
    http.Error(w, "Internal Server Error", http.StatusInternalServerError)
    return
}
// nil会话指针表示401未授权
if userSession == nil {
    http.Error(w, "Unathorized", http.StatusUnauthorized)
    return
}

...

// 延长会话过期时间。注意会话过期需要手动延长
if err := sesh.ExtendUserSession(userSession, r, w); err != nil {
    log.Printf("Err extending user session: %v\n", err)
    http.Error(w, "Internal Server Error", http.StatusInternalServerError)
    return
}

...

// 使用户会话无效,从redis中删除它并使ResponseWriter上的cookie过期
if err := sesh.ClearUserSession(userSession, w); err != nil {
    log.Printf("Err clearing user session: %v\n", err)
    http.Error(w, "Internal Server Error", http.StatusInternalServerError)
    return
}

性能

基准测试需要运行redis-server。设置REDIS_URL环境变量,否则基准测试会查找":6379"。

$ (cd benchmark && go test -bench=.)

setting up benchmark tests
BenchmarkBaseServer-2              20000             72479 ns/op
BenchmarkValidSession-2            10000            151650 ns/op
PASS
shutting down benchmark tests
ok      github.com/adam-hanna/sessions/benchmark        3.727s

设计

默认情况下,服务将会话存储在redis中,并将哈希的sessionID通过cookie传输给客户端。但可以通过创建实现接口的自定义结构轻松定制。

会话服务的一般流程如下:

  1. 创建存储、认证和传输服务
  2. 用户登录后调用IssueUserSession函数
  3. 使用GetUserSession函数检查请求中是否包含有效会话
  4. 用户注销时调用ClearUserSession函数

API

user.Session

type Session struct {
    ID        string
    UserID    string
    ExpiresAt time.Time
    JSON      string
}

Session是用于存储会话数据的结构体。JSON字段允许您设置任何自定义信息。

IssueUserSession

func (s *Service) IssueUserSession(userID string, json string, w http.ResponseWriter) (*user.Session, error)

IssueUserSession授予一个新的用户会话,将该会话信息写入存储,并在http.ResponseWriter上写入会话。

ClearUserSession

func (s *Service) ClearUserSession(userSession *user.Session, w http.ResponseWriter) error

ClearUserSession用于从存储中删除用户会话,并清除ResponseWriter上的cookie。

GetUserSession

func (s *Service) GetUserSession(r *http.Request) (*user.Session, error)

GetUserSession从请求中包含的哈希sessionID返回用户会话。此方法仅返回有效会话。

ExtendUserSession

func (s *Service) ExtendUserSession(userSession *user.Session, r *http.Request, w http.ResponseWriter) error

ExtendUserSession按Options.ExpirationDuration延长会话的ExpiresAt。

完整示例

package main

import (
    "crypto/rand"
    "encoding/base64"
    "encoding/json"
    "io"
    "log"
    "net/http"
    "time"

    "github.com/adam-hanna/sessions"
    "github.com/adam-hanna/sessions/auth"
    "github.com/adam-hanna/sessions/store"
    "github.com/adam-hanna/sessions/transport"
)

// SessionJSON 用于编组和解组自定义会话json信息
type SessionJSON struct {
    CSRF string `json:"csrf"`
}

var sesh *sessions.Service

var issueSession = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    csrf, err := generateKey()
    if err != nil {
        log.Printf("Err generating csrf: %v\n", err)
        http.Error(w, "Internal Server Error", http.StatusInternalServerError)
        return
    }

    myJSON := SessionJSON{
        CSRF: csrf,
    }
    JSONBytes, err := json.Marshal(myJSON)
    if err != nil {
        log.Printf("Err marhsalling json: %v\n", err)
        http.Error(w, "Internal Server Error", http.StatusInternalServerError)
        return
    }

    userSession, err := sesh.IssueUserSession("fakeUserID", string(JSONBytes[:]), w)
    if err != nil {
        log.Printf("Err issuing user session: %v\n", err)
        http.Error(w, "Internal Server Error", http.StatusInternalServerError)
        return
    }
    log.Printf("In issue; user's session: %v\n", userSession)

    // 注意: 我们在cookie中设置csrf,但在请求头中查找它
    csrfCookie := http.Cookie{
        Name:     "csrf",
        Value:    csrf,
        Expires:  userSession.ExpiresAt,
        Path:     "/",
        HttpOnly: false,
        Secure:   false, // 注意: 在开发中不能使用安全cookie
    }
    http.SetCookie(w, &csrfCookie)

    w.WriteHeader(http.StatusOK)
})

var requiresSession = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    userSession, err := sesh.GetUserSession(r)
    if err != nil {
        log.Printf("Err fetching user session: %v\n", err)
        http.Error(w, "Internal Server Error", http.StatusInternalServerError)
        return
    }
    // nil会话指针表示401未授权
    if userSession == nil {
        http.Error(w, "Unathorized", http.StatusUnauthorized)
        return
    }
    log.Printf("In require; user session expiration before extension: %v\n", userSession.ExpiresAt.UTC())

    myJSON := SessionJSON{}
    if err := json.Unmarshal([]byte(userSession.JSON), &myJSON); err != nil {
        log.Printf("Err unmarshalling json: %v\n", err)
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    log.Printf("In require; user's custom json: %v\n", myJSON)

    // 注意: 我们在cookie中设置csrf,但在请求头中查找它
    csrf := r.Header.Get("X-CSRF-Token")
    if csrf != myJSON.CSRF {
        log.Printf("Unauthorized! CSRF token doesn't match user session")
        http.Error(w, "Unauthorized", http.StatusUnauthorized)
        return
    }

    // 注意会话过期需要手动延长
    if err = sesh.ExtendUserSession(userSession, r, w); err != nil {
        log.Printf("Err extending user session: %v\n", err)
        http.Error(w, "Internal Server Error", http.StatusInternalServerError)
        return
    }
    log.Printf("In require; users session expiration after extension: %v\n", userSession.ExpiresAt.UTC())

    // 也需要延长csrf cookie
    csrfCookie := http.Cookie{
        Name:     "csrf",
        Value:    csrf,
        Expires:  userSession.ExpiresAt,
        Path:     "/",
        HttpOnly: false,
        Secure:   false, // 注意: 在开发中不能使用安全cookie
    }
    http.SetCookie(w, &csrfCookie)

    w.WriteHeader(http.StatusOK)
})

var clearSession = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    userSession, err := sesh.GetUserSession(r)
    if err != nil {
        log.Printf("Err fetching user session: %v\n", err)
        http.Error(w, "Internal Server Error", http.StatusInternalServerError)
        return
    }
    // nil会话指针表示401未授权
    if userSession == nil {
        http.Error(w, "Unathorized", http.StatusUnauthorized)
        return
    }

    log.Printf("In clear; session: %v\n", userSession)

    myJSON := SessionJSON{}
    if err := json.Unmarshal([]byte(userSession.JSON), &myJSON); err != nil {
        log.Printf("Err unmarshalling json: %v\n", err)
        http.Error(w, "Internal Server Error", http.StatusInternalServerError)
        return
    }
    log.Printf("In require; user's custom json: %v\n", myJSON)

    // 注意: 我们在cookie中设置csrf,但在请求头中查找它
    csrf := r.Header.Get("X-CSRF-Token")
    if csrf != myJSON.CSRF {
        log.Printf("Unauthorized! CSRF token doesn't match user session")
        http.Error(w, "Unauthorized", http.StatusUnauthorized)
        return
    }

    if err = sesh.ClearUserSession(userSession, w); err != nil {
        log.Printf("Err clearing user session: %v\n", err)
        http.Error(w, "Internal Server Error", http.StatusInternalServerError)
        return
    }

    // 也需要清除csrf cookie
    aLongTimeAgo := time.Now().Add(-1000 * time.Hour)
    csrfCookie := http.Cookie{
        Name:     "csrf",
        Value:    "",
        Expires:  aLongTimeAgo,
        Path:     "/",
        HttpOnly: false,
        Secure:   false, // 注意: 在开发中不能使用安全cookie
    }
    http.SetCookie(w, &csrfCookie)

    w.WriteHeader(http.StatusOK)
})

func main() {
    seshStore := store.New(store.Options{})

    // 例如 `$ openssl rand -base64 64`
    seshAuth, err := auth.New(auth.Options{
        Key: []byte("DOZDgBdMhGLImnk0BGYgOUI+h1n7U+OdxcZPctMbeFCsuAom2aFU4JPV4Qj11hbcb5yaM4WDuNP/3B7b+BnFhw=="),
    })
    if err != nil {
        log.Fatal(err)
    }

    seshTransport := transport.New(transport.Options{
        HTTPOnly: true,
        Secure:   false, // 注意: 在开发中不能使用安全cookie!
    })

    sesh = sessions.New(seshStore, seshAuth, seshTransport, sessions.Options{})

    http.HandleFunc("/issue", issueSession)
    http.HandleFunc("/require", requiresSession)
    http.HandleFunc("/clear", clearSession) // 也需要有效的会话

    log.Println("Listening on localhost:3000")
    log.Fatal(http.ListenAndServe("127.0.0.1:3000", nil))
}

// 感谢
// https://astaxie.gitbooks.io/build-web-application-with-golang/en/06.2.html#unique-session-ids
func generateKey() (string, error) {
    b := make([]byte, 16)
    if _, err := io.ReadFull(rand.Reader, b); err != nil {
        return "", err
    }
    return base64.URLEncoding.EncodeToString(b), nil
}

许可证

MIT许可证


更多关于golang高性能可定制HTTP会话管理插件库sessions的使用的实战教程也可以访问 https://www.itying.com/category-94-b0.html

1 回复

更多关于golang高性能可定制HTTP会话管理插件库sessions的使用的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


Golang高性能可定制HTTP会话管理库sessions使用指南

sessions是Golang中一个流行的HTTP会话管理库,它提供了高性能、可定制的会话管理功能。下面我将详细介绍如何使用这个库。

基本安装

首先安装sessions库:

go get github.com/gorilla/sessions

基本使用示例

package main

import (
    "fmt"
    "net/http"
    
    "github.com/gorilla/sessions"
)

// 初始化cookie存储
var store = sessions.NewCookieStore([]byte("your-secret-key"))

func main() {
    http.HandleFunc("/", handler)
    http.ListenAndServe(":8080", nil)
}

func handler(w http.ResponseWriter, r *http.Request) {
    // 获取会话
    session, err := store.Get(r, "session-name")
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    
    // 设置会话值
    session.Values["foo"] = "bar"
    session.Values[42] = true
    
    // 保存会话
    err = session.Save(r, w)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }
    
    // 读取会话值
    foo := session.Values["foo"]
    fmt.Fprintf(w, "Session value: %v", foo)
}

高级配置

1. 自定义存储后端

sessions支持多种存储后端,包括Cookie、文件系统、Redis等。

使用Redis存储

import (
    "github.com/gorilla/sessions"
    "github.com/boj/redistore"
)

func newRedisStore() *redistore.RediStore {
    store, err := redistore.NewRediStore(10, "tcp", ":6379", "", []byte("secret-key"))
    if err != nil {
        panic(err)
    }
    return store
}

var store = newRedisStore()

2. 会话选项配置

var store = sessions.NewCookieStore(
    []byte("your-secret-key"),
    []byte("another-secret-key-for-encryption"),
)

// 配置会话选项
store.Options = &sessions.Options{
    Path:     "/",
    MaxAge:   86400 * 30, // 30天
    HttpOnly: true,
    Secure:   true, // 仅在HTTPS下传输
    SameSite: http.SameSiteLaxMode,
}

3. 闪存消息(Flash Messages)

闪存消息是一种特殊的会话消息,它只显示一次然后自动删除。

func flashHandler(w http.ResponseWriter, r *http.Request) {
    session, _ := store.Get(r, "session-name")
    
    // 添加闪存消息
    session.AddFlash("This is a flash message!")
    
    // 保存会话
    session.Save(r, w)
    
    // 在下一个请求中获取闪存消息
    flashes := session.Flashes()
    for _, flash := range flashes {
        fmt.Fprintf(w, "Flash: %v", flash)
    }
}

性能优化建议

  1. 使用Redis存储:对于高流量网站,使用Redis等外部存储比Cookie存储性能更好
  2. 减少会话数据大小:只存储必要的数据
  3. 适当设置会话过期时间:避免会话无限期存在
  4. 使用会话池:对于频繁创建销毁会话的场景

安全最佳实践

  1. 总是使用HTTPS并设置Secure标志
  2. 使用HttpOnly标志防止XSS攻击
  3. 定期更换密钥
  4. 避免在会话中存储敏感信息

完整示例

package main

import (
    "fmt"
    "net/http"
    
    "github.com/gorilla/sessions"
)

var store = sessions.NewCookieStore([]byte("super-secret-key"))

func loginHandler(w http.ResponseWriter, r *http.Request) {
    session, _ := store.Get(r, "auth-session")
    
    // 模拟用户认证
    if r.FormValue("username") == "admin" && r.FormValue("password") == "password" {
        session.Values["authenticated"] = true
        session.Save(r, w)
        http.Redirect(w, r, "/dashboard", http.StatusFound)
    } else {
        session.AddFlash("Invalid credentials")
        session.Save(r, w)
        http.Redirect(w, r, "/login", http.StatusFound)
    }
}

func dashboardHandler(w http.ResponseWriter, r *http.Request) {
    session, _ := store.Get(r, "auth-session")
    
    if auth, ok := session.Values["authenticated"].(bool); !ok || !auth {
        http.Error(w, "Unauthorized", http.StatusUnauthorized)
        return
    }
    
    fmt.Fprintln(w, "Welcome to the dashboard!")
}

func logoutHandler(w http.ResponseWriter, r *http.Request) {
    session, _ := store.Get(r, "auth-session")
    
    // 清除会话
    session.Values["authenticated"] = false
    session.Options.MaxAge = -1 // 立即过期
    session.Save(r, w)
    
    http.Redirect(w, r, "/login", http.StatusFound)
}

func main() {
    http.HandleFunc("/login", loginHandler)
    http.HandleFunc("/dashboard", dashboardHandler)
    http.HandleFunc("/logout", logoutHandler)
    
    http.ListenAndServe(":8080", nil)
}

sessions库提供了灵活而强大的会话管理功能,通过合理配置可以满足大多数Web应用的会话管理需求。

回到顶部