mirror of
https://github.com/alexedwards/scs.git
synced 2025-07-15 01:04:36 +02:00
[docs] Add documentation
This commit is contained in:
164
engine/cookiestore/README.md
Normal file
164
engine/cookiestore/README.md
Normal file
@ -0,0 +1,164 @@
|
||||
# cookiestore
|
||||
[](https://godoc.org/github.com/alexedwards/scs/engine/cookiestore)
|
||||
|
||||
Package cookiestore is a cookie-based storage engine for the [SCS session package](https://godoc.org/github.com/alexedwards/scs/session).
|
||||
|
||||
It stores session data in AES-encrypted and SHA256-signed cookies on the client. Key rotation is supported for increased security.
|
||||
|
||||
The cookiestore package provides a simple and easy way to implement session functionality, with no external dependencies.
|
||||
|
||||
## Usage
|
||||
|
||||
### Installation
|
||||
|
||||
Either:
|
||||
|
||||
```
|
||||
$ go get github.com/alexedwards/scs/engine/cookiestore
|
||||
```
|
||||
|
||||
Or (recommended) use use [gvt](https://github.com/FiloSottile/gvt) to vendor the `engine/cookiestore` and `session` sub-packages:
|
||||
|
||||
```
|
||||
$ gvt fetch github.com/alexedwards/scs/engine/cookiestore
|
||||
$ gvt fetch github.com/alexedwards/scs/session
|
||||
```
|
||||
|
||||
### Example
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/alexedwards/scs/engine/cookiestore"
|
||||
"github.com/alexedwards/scs/session"
|
||||
)
|
||||
|
||||
// HMAC authentication key (hexadecimal representation of 32 random bytes)
|
||||
var hmacKey = []byte("f71dc7e58abab014ddad2652475056f185164d262869c8931b239de52711ba87")
|
||||
|
||||
// AES encryption key (hexadecimal representation of 16 random bytes)
|
||||
var blockKey = []byte("911182cec2f206986c8c82440adb7d17")
|
||||
|
||||
func main() {
|
||||
// Create a new keyset using your authentication and encryption secret keys.
|
||||
keyset, err := cookiestore.NewKeyset(hmacKey, blockKey)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Create a new CookieStore instance using the keyset.
|
||||
engine := cookiestore.New(keyset)
|
||||
|
||||
sessionManager := session.Manage(engine)
|
||||
http.HandleFunc("/put", putHandler)
|
||||
http.HandleFunc("/get", getHandler)
|
||||
http.ListenAndServe(":4000", sessionManager(http.DefaultServeMux))
|
||||
}
|
||||
|
||||
func putHandler(w http.ResponseWriter, r *http.Request) {
|
||||
err := session.PutString(r, "message", "Hello world!")
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
}
|
||||
}
|
||||
|
||||
func getHandler(w http.ResponseWriter, r *http.Request) {
|
||||
msg, err := session.GetString(r, "message")
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
}
|
||||
io.WriteString(w, msg)
|
||||
}
|
||||
```
|
||||
|
||||
#### Creating a Keyset
|
||||
|
||||
Every CookieStore instance must have a Keyset, which contains your secret keys used for encrypting/decrypting the session data and signing the session cookie.
|
||||
|
||||
Keysets are created with the `NewKeyset()` function, which takes an `hmacKey` parameter (used to create the HMAC hash to sign the session cookie) and a `blockKey` parameter (used to encrypt/decrypt the session data).
|
||||
|
||||
```go
|
||||
var hmacKey = []byte("f71dc7e58abab014ddad2652475056f185164d262869c8931b239de52711ba87")
|
||||
var blockKey = []byte("911182cec2f206986c8c82440adb7d17")
|
||||
|
||||
keyset, err := cookiestore.NewKeyset(hmacKey, blockKey)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
```
|
||||
|
||||
Because cookiestore uses SHA256 as the HMAC hashing algorithm, the [recommended minimum length](https://tools.ietf.org/html/rfc2104) of the `hmacKey` parameter is at least 32 random bytes. If you're storing the key as an encoded string for convenience, the underlying entropy should still be 32 bytes (i.e you should use a 64 character hex string or 43 character base64 string).
|
||||
|
||||
The `blockKey` must be 16, 24 or 32 bytes long. The byte length you use will control which AES implementation is used. A 16 byte `blockKey` will mean that AES-128 is used to encrypt the session data, 24 bytes means AES-192 will be used, and 32 bytes means that AES-256 will be used.
|
||||
|
||||
#### Unencrypted session cookies
|
||||
|
||||
Session cookies that are signed, but not encrypted, can also be used. The cookies will remain tamper-proof, but an user or attacker will be able to read the session data in the cookie.
|
||||
|
||||
Using unencrypted session cookies is marginally faster.
|
||||
|
||||
Creating a Keyset with the `NewUnencryptedKeyset()` function will result in unencrypted cookies being used.
|
||||
|
||||
```go
|
||||
var hmacKey = []byte("f71dc7e58abab014ddad2652475056f185164d262869c8931b239de52711ba87")
|
||||
|
||||
keyset, err := cookiestore.NewUnencryptedKeyset(hmacKey)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
engine := cookiestore.New(keyset)
|
||||
```
|
||||
|
||||
|
||||
#### Key rotation
|
||||
|
||||
The cookiestore package supports key rotation for increased security.
|
||||
|
||||
An arbitrary number of old Keysets can be provided when creating a new CookieStore instance. For example:
|
||||
|
||||
```go
|
||||
keyset, err := cookiestore.NewKeyset([]byte("f71dc7e58abab014ddad2652475056f185164d262869c8931b239de52711ba87"), []byte("911182cec2f206986c8c82440adb7d17"))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
oldKeyset, err := cookiestore.NewKeyset([]byte("16bd76c6372363cd9af46f5619cc406776210b6164c48fd1200119d4cfc359e6"), []byte("5f8b7a8efac2a900a0c6be609b2e0241"))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
veryOldKeyset, err := cookiestore.NewKeyset([]byte("0c03fa487baa82dda09c4f12c7238370c58112a135318a6e3d4a4724a95cd2e0"), []byte("46ee77bfb95a765dfefca83bf53d5914"))
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
engine := cookiestore.New(keyset, oldKeyset, veryOldKeyset)
|
||||
```
|
||||
|
||||
When a session cookie is received from a client, all Keysets (including old Keysets) are looped through to try to decode the cookie.
|
||||
|
||||
When rotating Keysets, it is essential that Keysets are entirely unique. You must not change the the `blockKey` for a Keyset without also changing the `hmacKey`. Re-using `hmacKey` values will result in some valid cookies not being able to be decoded.
|
||||
|
||||
#### Cookie length
|
||||
|
||||
Cookies are limited to 4096 characters in length. Storing large amounts of session data may, when encoded and signed, exceed this length and result in an error.
|
||||
|
||||
This makes cookie-based sessions suitable for applications where the amount of session data is known in advance and small.
|
||||
|
||||
### RegenerateToken function
|
||||
|
||||
The cookiestore package is a special case for the `scs/session` package because it stores data on the client only, not the server.
|
||||
|
||||
This means that using [`session.RegenerateToken()`](https://godoc.org/github.com/alexedwards/scs/session#RegenerateToken) as a mechanism to prevent session fixation attacks is unnecessary when using cookiestore, because the signed and encrypted cookie 'token' always changes whenever the session data is modified anyway.
|
||||
|
||||
The only impact that calling `session.RegenerateToken()` will have is to reset and restart the session lifetime.
|
||||
|
||||
## Notes
|
||||
|
||||
Full godoc documentation: [https://godoc.org/github.com/alexedwards/scs/engine/cookiestore](https://godoc.org/github.com/alexedwards/scs/engine/cookiestore).
|
@ -1,5 +1,34 @@
|
||||
// TODO: Document that
|
||||
// RenegerateToken and Renew are no-ops
|
||||
// Package cookiestore is a cookie-based storage engine for the SCS session package.
|
||||
//
|
||||
// It stores session data in AES-encrypted and SHA256-signed cookies on the client.
|
||||
// It also supports key rotation for increased security.
|
||||
//
|
||||
// // HMAC authentication key (hexadecimal representation of 32 random bytes)
|
||||
// var hmacKey = []byte("f71dc7e58abab014ddad2652475056f185164d262869c8931b239de52711ba87")
|
||||
// // AES encryption key (hexadecimal representation of 16 random bytes)
|
||||
// var blockKey = []byte("911182cec2f206986c8c82440adb7d17")
|
||||
//
|
||||
// func main() {
|
||||
// // Create a new keyset using your authentication and encryption secret keys.
|
||||
// keyset, err := cookiestore.NewKeyset(hmacKey, blockKey)
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
//
|
||||
// // Create a new CookieStore instance using the keyset.
|
||||
// engine := cookiestore.New(keyset)
|
||||
//
|
||||
// sessionManager := session.Manage(engine)
|
||||
// http.ListenAndServe(":4000", sessionManager(http.DefaultServeMux))
|
||||
// }
|
||||
//
|
||||
// The cookiestore package is a special case for the scs/session package because
|
||||
// it stores data on the client only, not the server. This means that using the
|
||||
// session.RegenerateToken() function as a mechanism to prevent session fixation
|
||||
// attacks is unnecessary when using cookiestore, because the signed and encrypted
|
||||
// cookie 'token' always changes whenever the session data is modified anyway.
|
||||
// The only impact of calling session.RegenerateToken() is to reset and restart
|
||||
// the session lifetime.
|
||||
package cookiestore
|
||||
|
||||
import (
|
||||
@ -24,20 +53,36 @@ var (
|
||||
errInvalidToken = errors.New("token is invalid")
|
||||
)
|
||||
|
||||
// CookieStore represents the currently configured session storage engine.
|
||||
type CookieStore struct {
|
||||
keyset *Keyset
|
||||
oldKeysets []*Keyset
|
||||
}
|
||||
|
||||
// New returns a new CookieStore instance.
|
||||
//
|
||||
// The keyset parameter should contain the Keyset you want to use to sign and
|
||||
// encrypt session cookies.
|
||||
//
|
||||
// Optionally, the variadic oldKeyset parameter can be used to provide an arbitrary
|
||||
// number of old Keysets. This should be used to ensure that valid cookies continue
|
||||
// to work correctly after key rotation.
|
||||
func New(keyset *Keyset, oldKeysets ...*Keyset) *CookieStore {
|
||||
return &CookieStore{keyset, oldKeysets}
|
||||
}
|
||||
|
||||
func (c *CookieStore) MakeToken(b []byte, expiry time.Time) (string, error) {
|
||||
// MakeToken creates a signed, optionally encrypted, cookie token for the provided
|
||||
// session data. The returned token is limited to 4096 characters in length. An
|
||||
// error will be returned if this is exceeded.
|
||||
func (c *CookieStore) MakeToken(b []byte, expiry time.Time) (token string, err error) {
|
||||
return encodeToken(c.keyset, b, expiry)
|
||||
}
|
||||
|
||||
func (c *CookieStore) Find(token string) ([]byte, bool, error) {
|
||||
// Find returns the session data for given cookie token. It loops through all
|
||||
// available Keysets (including old Keysets) to try to decode the cookie. If
|
||||
// the cookie could not be decoded, or has expired, the returned exists flag
|
||||
// will be set to false.
|
||||
func (c *CookieStore) Find(token string) (b []byte, exists bool, error error) {
|
||||
keysets := append([]*Keyset{c.keyset}, c.oldKeysets...)
|
||||
for _, keyset := range keysets {
|
||||
b, err := decodeToken(keyset, token)
|
||||
@ -53,10 +98,14 @@ func (c *CookieStore) Find(token string) ([]byte, bool, error) {
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
// Save is a no-op. The function exists only to ensure that a CookieStore instance
|
||||
// satisfies the session.Engine interface.
|
||||
func (c *CookieStore) Save(token string, b []byte, expiry time.Time) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete is a no-op. The function exists only to ensure that a CookieStore instance
|
||||
// satisfies the session.Engine interface.
|
||||
func (c *CookieStore) Delete(token string) error {
|
||||
return nil
|
||||
}
|
||||
|
@ -1,7 +1,3 @@
|
||||
// TODO: Document that
|
||||
// * Keysets must be entirely unique (i.e HMAC keys should not be reused)
|
||||
// * The blockKey argument should be the AES key, either 16, 24, or 32 bytes to select AES-128, AES-192, or AES-256
|
||||
// * HMAC keys are recommended to be 32 random bytes. If an encoded string is used the underlying entropy should still be 32 random bytes (i.e a 64 character hex string or 43 character base64 string)
|
||||
package cookiestore
|
||||
|
||||
import (
|
||||
@ -15,11 +11,34 @@ var (
|
||||
errBlockKeyLength = errors.New("blockKey length must be 16, 24 or 32 bytes")
|
||||
)
|
||||
|
||||
// Keyset holds the secrets for signing and encrypting/decrypting session cookies.
|
||||
// It should be instantiated using the NewKeyset and NewUnencryptedKeyset functions
|
||||
// only.
|
||||
type Keyset struct {
|
||||
hmacKey []byte
|
||||
block cipher.Block
|
||||
}
|
||||
|
||||
// NewKeyset returns a pointer to a Keyset, which contains your secret keys used
|
||||
// for encrypting/decrypting the session data and signing the session cookie.
|
||||
//
|
||||
// The hmacKey parameter is used to create the HMAC hash to sign the session cookie.
|
||||
// Because cookiestore uses SHA256 as the HMAC hashing algorithm, the recommended
|
||||
// minimum length hmacKey parameter is at least 32 random bytes. If you're storing
|
||||
// the key as an encoded string for convenience, the underlying entropy should
|
||||
// still be 32 bytes (i.e you should use a 64 character hex string or 43 character
|
||||
// base64 string).
|
||||
//
|
||||
// The blockKey parameter is used to encrypt/decrypt the session data. It must be
|
||||
// 16, 24 or 32 bytes long. The byte length you use will control which AES implementation
|
||||
// is used. A 16 byte `blockKey` will mean that AES-128 is used to encrypt the
|
||||
// session data, 24 bytes means AES-192 will be used, and 32 bytes means that
|
||||
// AES-256 will be used.
|
||||
//
|
||||
// When rotating Keysets, it is essential that Keysets are entirely unique. You
|
||||
// must not change the the blockKey for a Keyset without also changing the hmacKey.
|
||||
// Re-using `hmacKey` values will result in some valid cookies not being able to
|
||||
// be decoded.
|
||||
func NewKeyset(hmacKey, blockKey []byte) (*Keyset, error) {
|
||||
if len(hmacKey) < 32 {
|
||||
return nil, errHMACKeyLength
|
||||
@ -40,6 +59,10 @@ func NewKeyset(hmacKey, blockKey []byte) (*Keyset, error) {
|
||||
return &Keyset{hmacKey, block}, nil
|
||||
}
|
||||
|
||||
// NewUnencryptedKeyset returns a pointer to a Keyset which will sign, but not
|
||||
// encrypt, the session cookie. The cookie will be tamper-proof, but an user or
|
||||
// attacker will be able to read the session data in the cookie. Using unencrypted
|
||||
// session cookies is marginally faster.
|
||||
func NewUnencryptedKeyset(hmacKey []byte) (*Keyset, error) {
|
||||
if len(hmacKey) < 32 {
|
||||
return nil, errHMACKeyLength
|
||||
|
85
engine/memstore/README.md
Normal file
85
engine/memstore/README.md
Normal file
@ -0,0 +1,85 @@
|
||||
# memstore
|
||||
[](https://godoc.org/github.com/alexedwards/scs/engine/memstore)
|
||||
|
||||
Package memstore is an in-memory storage engine for the [SCS session package](https://godoc.org/github.com/alexedwards/scs/session).
|
||||
|
||||
Warning: Because memstore uses in-memory storage only, all session data will be lost when your Go program is stopped or restarted. On the upside though, it is blazingly fast.
|
||||
|
||||
In production, memstore should only be used where this volatility is an acceptable trade-off for the high performance, and where lost session data will have a negligible impact on users. As an example, a use case could be using it to track which adverts a visitor has already been shown.
|
||||
|
||||
## Usage
|
||||
|
||||
### Installation
|
||||
|
||||
Either:
|
||||
|
||||
```
|
||||
$ go get github.com/alexedwards/scs/engine/memstore
|
||||
```
|
||||
|
||||
Or (recommended) use use [gvt](https://github.com/FiloSottile/gvt) to vendor the `engine/memstore` and `session` sub-packages:
|
||||
|
||||
```
|
||||
$ gvt fetch github.com/alexedwards/scs/engine/memstore
|
||||
$ gvt fetch github.com/alexedwards/scs/session
|
||||
```
|
||||
|
||||
### Example
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/alexedwards/scs/engine/memstore"
|
||||
"github.com/alexedwards/scs/session"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Create a new MemStore instance with a cleanup interval of 5 minutes.
|
||||
engine := memstore.New(5 * time.Minute)
|
||||
|
||||
sessionManager := session.Manage(engine)
|
||||
http.HandleFunc("/put", putHandler)
|
||||
http.HandleFunc("/get", getHandler)
|
||||
http.ListenAndServe(":4000", sessionManager(http.DefaultServeMux))
|
||||
}
|
||||
|
||||
func putHandler(w http.ResponseWriter, r *http.Request) {
|
||||
err := session.PutString(r, "message", "Hello world!")
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
}
|
||||
}
|
||||
|
||||
func getHandler(w http.ResponseWriter, r *http.Request) {
|
||||
msg, err := session.GetString(r, "message")
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
}
|
||||
io.WriteString(w, msg)
|
||||
}
|
||||
```
|
||||
|
||||
### Cleaning up expired session data
|
||||
|
||||
The memstore package provides a background 'cleanup' goroutine to delete expired session data. This stops the underlying cache from holding on to invalid sessions forever and taking up unnecessary memory.
|
||||
|
||||
You can specify how frequently to run the cleanup when creating a new MemStore instance:
|
||||
|
||||
```go
|
||||
// Run a cleanup every 30 minutes.
|
||||
memstore.New(30 * time.Minute)
|
||||
|
||||
// Setting the cleanup interval to zero prevents the cleanup from being run.
|
||||
memstore.New(0)
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
The memstore package is underpinned by the excellent [go-cache](https://github.com/patrickmn/go-cache) package.
|
||||
|
||||
Full godoc documentation: [https://godoc.org/github.com/alexedwards/scs/engine/memstore](https://godoc.org/github.com/alexedwards/scs/engine/memstore).
|
@ -1,3 +1,26 @@
|
||||
// Package memstore is a in-memory storage engine for the SCS session package.
|
||||
//
|
||||
// Warning: Because memstore uses in-memory storage only, all session data will
|
||||
// be lost when your Go program is stopped or restarted. On the upside though,
|
||||
// it is blazingly fast.
|
||||
//
|
||||
// In production, memstore should only be used where this volatility is an acceptable
|
||||
// trade-off for the high performance, and where lost session data will have a
|
||||
// negligible impact on users.
|
||||
//
|
||||
// The memstore package provides a background 'cleanup' goroutine to delete
|
||||
// expired session data. This stops the underlying cache from holding on to invalid
|
||||
// sessions forever and taking up unnecessary memory.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// func main() {
|
||||
// // Create a new memstore storage engine with a cleanup interval of 5 minutes.
|
||||
// engine := memstore.New(5 * time.Minute)
|
||||
//
|
||||
// sessionManager := session.Manage(engine)
|
||||
// http.ListenAndServe(":4000", sessionManager(http.DefaultServeMux))
|
||||
// }
|
||||
package memstore
|
||||
|
||||
import (
|
||||
@ -9,17 +32,26 @@ import (
|
||||
|
||||
var errTypeAssertionFailed = errors.New("type assertion failed: could not convert interface{} to []byte")
|
||||
|
||||
func New(sweepInterval time.Duration) *MemStore {
|
||||
return &MemStore{
|
||||
cache.New(cache.DefaultExpiration, sweepInterval),
|
||||
}
|
||||
}
|
||||
|
||||
// MemStore represents the currently configured session storage engine. It is essentially
|
||||
// a wrapper around a go-cache instance (see https://github.com/patrickmn/go-cache).
|
||||
type MemStore struct {
|
||||
*cache.Cache
|
||||
}
|
||||
|
||||
func (m *MemStore) Find(token string) ([]byte, bool, error) {
|
||||
// New returns a new MemStore instance.
|
||||
//
|
||||
// The cleanupInterval parameter controls how frequently expired session data
|
||||
// is removed by the background 'cleanup' goroutine. Setting it to 0 prevents
|
||||
// the cleanup goroutine from running (i.e. expired sessions will not be removed).
|
||||
func New(cleanupInterval time.Duration) *MemStore {
|
||||
return &MemStore{
|
||||
cache.New(cache.DefaultExpiration, cleanupInterval),
|
||||
}
|
||||
}
|
||||
|
||||
// Find returns the data for a given session token from the MemStore instance. If the session
|
||||
// token is not found or is expired, the returned exists flag will be set to false.
|
||||
func (m *MemStore) Find(token string) (b []byte, exists bool, err error) {
|
||||
v, exists := m.Cache.Get(token)
|
||||
if exists == false {
|
||||
return nil, exists, nil
|
||||
@ -33,11 +65,14 @@ func (m *MemStore) Find(token string) ([]byte, bool, error) {
|
||||
return b, exists, nil
|
||||
}
|
||||
|
||||
// Save adds a session token and data to the MemStore instance with the given expiry time.
|
||||
// If the session token already exists then the data and expiry time are updated.
|
||||
func (m *MemStore) Save(token string, b []byte, expiry time.Time) error {
|
||||
m.Cache.Set(token, b, expiry.Sub(time.Now()))
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete removes a session token and corresponding data from the MemStore instance.
|
||||
func (m *MemStore) Delete(token string) error {
|
||||
m.Cache.Delete(token)
|
||||
return nil
|
||||
|
@ -1,8 +1,127 @@
|
||||
# mysqlstore
|
||||
[](https://godoc.org/github.com/alexedwards/scs/engine/mysqlstore)
|
||||
|
||||
Package mysqlstore is a MySQL-based storage engine for the [SCS session package](https://godoc.org/github.com/alexedwards/scs/session).
|
||||
|
||||
## Usage
|
||||
|
||||
### Installation
|
||||
|
||||
Either:
|
||||
|
||||
```
|
||||
$ go get github.com/alexedwards/scs/engine/mysqlstore
|
||||
```
|
||||
|
||||
Or (recommended) use use [gvt](https://github.com/FiloSottile/gvt) to vendor the `engine/mysqlstore` and `session` sub-packages:
|
||||
|
||||
```
|
||||
$ gvt fetch github.com/alexedwards/scs/engine/mysqlstore
|
||||
$ gvt fetch github.com/alexedwards/scs/session
|
||||
```
|
||||
|
||||
### Setup
|
||||
|
||||
You should have a working MySQL database containing a `sessions` table with the definition:
|
||||
|
||||
```sql
|
||||
CREATE TABLE IF NOT EXISTS sessions (
|
||||
CREATE TABLE sessions (
|
||||
token CHAR(43) PRIMARY KEY,
|
||||
data BLOB NOT NULL,
|
||||
expiry TIMESTAMP(6) NOT NULL
|
||||
);
|
||||
INDEX ON EXPIRY.....
|
||||
CREATE INDEX sessions_expiry_idx ON sessions (expiry);
|
||||
```
|
||||
|
||||
### Example
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/alexedwards/scs/engine/mysqlstore"
|
||||
"github.com/alexedwards/scs/session"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Establish a database/sql pool
|
||||
db, err := sql.Open("mysql", "user:pass@/db")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// Create a new MySQLStore instance using the existing database/sql pool,
|
||||
// with a cleanup interval of 5 minutes.
|
||||
engine := mysqlstore.New(db, 5*time.Minute)
|
||||
|
||||
sessionManager := session.Manage(engine)
|
||||
http.HandleFunc("/put", putHandler)
|
||||
http.HandleFunc("/get", getHandler)
|
||||
http.ListenAndServe(":4000", sessionManager(http.DefaultServeMux))
|
||||
}
|
||||
|
||||
func putHandler(w http.ResponseWriter, r *http.Request) {
|
||||
err := session.PutString(r, "message", "Hello world!")
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
}
|
||||
}
|
||||
|
||||
func getHandler(w http.ResponseWriter, r *http.Request) {
|
||||
msg, err := session.GetString(r, "message")
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
}
|
||||
io.WriteString(w, msg)
|
||||
}
|
||||
```
|
||||
|
||||
### Cleaning up expired session data
|
||||
|
||||
The mysqlstore package provides a background 'cleanup' goroutine to delete expired session data. This stops the database table from holding on to invalid sessions indefinitely and growing unnecessarily large.
|
||||
|
||||
You can specify how frequently to run the cleanup when creating a new MySQLStore instance:
|
||||
|
||||
```go
|
||||
// Run a cleanup every 30 minutes.
|
||||
mysqlstore.New(db, 30*time.Minute)
|
||||
|
||||
// Setting the cleanup interval to zero prevents the cleanup from being run.
|
||||
mysqlstore.New(db, 0)
|
||||
```
|
||||
|
||||
#### Terminating the cleanup goroutine
|
||||
|
||||
It's rare that the cleanup goroutine for a MySQLStore instance needs to be terminated. It is generally intended to be long-lived and run for the lifetime of your application.
|
||||
|
||||
However, there may be occasions when your use of a MySQLStore instance is transient. A common example would be using it in a short-lived test function. In this scenario, the cleanup goroutine (which will run forever) will prevent the MySQLStore object from being garbage collected even after the test function has finished. You can prevent this by manually calling `StopCleanup()`.
|
||||
|
||||
For example:
|
||||
|
||||
```go
|
||||
func TestExample(t *testing.T) {
|
||||
db, err := sql.Open("mysql", "user:pass@/db")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
engine := New(db, time.Second)
|
||||
defer engine.StopCleanup()
|
||||
|
||||
// Run test...
|
||||
}
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
The mysqlstore package is underpinned by the [go-sql-driver/mysql](https://github.com/go-sql-driver/mysql) driver.
|
||||
|
||||
Full godoc documentation: [https://godoc.org/github.com/alexedwards/scs/engine/mysqlstore](https://godoc.org/github.com/alexedwards/scs/engine/mysqlstore).
|
@ -1,3 +1,38 @@
|
||||
// Package mysqlstore is a MySQL-based storage engine for the SCS session package.
|
||||
//
|
||||
// A working MySQL database is required, containing a sessions table with
|
||||
// the definition:
|
||||
//
|
||||
// CREATE TABLE sessions (
|
||||
// token CHAR(43) PRIMARY KEY,
|
||||
// data BLOB NOT NULL,
|
||||
// expiry TIMESTAMP(6) NOT NULL
|
||||
// );
|
||||
// CREATE INDEX sessions_expiry_idx ON sessions (expiry);
|
||||
//
|
||||
// The mysqlstore package provides a background 'cleanup' goroutine to delete expired
|
||||
// session data. This stops the database table from holding on to invalid sessions
|
||||
// forever and growing unnecessarily large.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// func main() {
|
||||
// // Establish a database/sql pool
|
||||
// db, err := sql.Open("mysql", "user:pass@/db")
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
// defer db.Close()
|
||||
//
|
||||
// // Create a new MySQLStore instance using the existing database/sql pool,
|
||||
// // with a cleanup interval of 5 minutes.
|
||||
// engine := mysqlstore.New(db, 5*time.Minute)
|
||||
//
|
||||
// sessionManager := session.Manage(engine)
|
||||
// http.ListenAndServe(":4000", sessionManager(http.DefaultServeMux))
|
||||
// }
|
||||
//
|
||||
// It is underpinned by the go-sql-driver/mysql driver (https://github.com/go-sql-driver/mysql).
|
||||
package mysqlstore
|
||||
|
||||
import (
|
||||
@ -5,22 +40,32 @@ import (
|
||||
"log"
|
||||
"time"
|
||||
|
||||
// Register go-sql-driver/mysql with database/sql
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
)
|
||||
|
||||
// MySQLStore represents the currently configured session storage engine.
|
||||
type MySQLStore struct {
|
||||
*sql.DB
|
||||
stopSweeper chan bool
|
||||
stopCleanup chan bool
|
||||
}
|
||||
|
||||
func New(db *sql.DB, sweepInterval time.Duration) *MySQLStore {
|
||||
// New returns a new MySQLStore instance.
|
||||
//
|
||||
// The cleanupInterval parameter controls how frequently expired session data
|
||||
// is removed by the background cleanup goroutine. Setting it to 0 prevents
|
||||
// the cleanup goroutine from running (i.e. expired sessions will not be removed).
|
||||
func New(db *sql.DB, cleanupInterval time.Duration) *MySQLStore {
|
||||
m := &MySQLStore{DB: db}
|
||||
if sweepInterval > 0 {
|
||||
go m.startSweeper(sweepInterval)
|
||||
if cleanupInterval > 0 {
|
||||
go m.startCleanup(cleanupInterval)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// Find returns the data for a given session token from the MySQLStore instance. If
|
||||
// the session token is not found or is expired, the returned exists flag will be
|
||||
// set to false.
|
||||
func (m *MySQLStore) Find(token string) ([]byte, bool, error) {
|
||||
var b []byte
|
||||
row := m.DB.QueryRow("SELECT data FROM sessions WHERE token = ? AND UTC_TIMESTAMP(6) < expiry", token)
|
||||
@ -33,6 +78,8 @@ func (m *MySQLStore) Find(token string) ([]byte, bool, error) {
|
||||
return b, true, nil
|
||||
}
|
||||
|
||||
// Save adds a session token and data to the MySQLStore instance with the given expiry
|
||||
// time. If the session token already exists then the data and expiry time are updated.
|
||||
func (m *MySQLStore) Save(token string, b []byte, expiry time.Time) error {
|
||||
_, err := m.DB.Exec("INSERT INTO sessions (token, data, expiry) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE data = VALUES(data), expiry = VALUES(expiry)", token, b, expiry.UTC())
|
||||
if err != nil {
|
||||
@ -41,13 +88,14 @@ func (m *MySQLStore) Save(token string, b []byte, expiry time.Time) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete removes a session token and corresponding data from the MySQLStore instance.
|
||||
func (m *MySQLStore) Delete(token string) error {
|
||||
_, err := m.DB.Exec("DELETE FROM sessions WHERE token = ?", token)
|
||||
return err
|
||||
}
|
||||
|
||||
func (m *MySQLStore) startSweeper(interval time.Duration) {
|
||||
m.stopSweeper = make(chan bool)
|
||||
func (m *MySQLStore) startCleanup(interval time.Duration) {
|
||||
m.stopCleanup = make(chan bool)
|
||||
ticker := time.NewTicker(interval)
|
||||
for {
|
||||
select {
|
||||
@ -56,16 +104,41 @@ func (m *MySQLStore) startSweeper(interval time.Duration) {
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
case <-m.stopSweeper:
|
||||
case <-m.stopCleanup:
|
||||
ticker.Stop()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MySQLStore) StopSweeper() {
|
||||
if m.stopSweeper != nil {
|
||||
m.stopSweeper <- true
|
||||
// StopCleanup terminates the background cleanup goroutine for the MySQLStore instance.
|
||||
// It's rare to terminate this; generally MySQLStore instances and their cleanup
|
||||
// goroutines are intended to be long-lived and run for the lifetime of your
|
||||
// application.
|
||||
//
|
||||
// There may be occasions though when your use of the MySQLStore is transient. An
|
||||
// example is creating a new MySQLStore instance in a test function. In this scenario,
|
||||
// the cleanup goroutine (which will run forever) will prevent the MySQLStore object
|
||||
// from being garbage collected even after the test function has finished. You
|
||||
// can prevent this by manually calling StopCleanup.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestExample(t *testing.T) {
|
||||
// db, err := sql.Open("mysql", "user:pass@/db")
|
||||
// if err != nil {
|
||||
// t.Fatal(err)
|
||||
// }
|
||||
// defer db.Close()
|
||||
//
|
||||
// engine := mysqlstore.New(db, time.Second)
|
||||
// defer engine.StopCleanup()
|
||||
//
|
||||
// // Run test...
|
||||
// }
|
||||
func (m *MySQLStore) StopCleanup() {
|
||||
if m.stopCleanup != nil {
|
||||
m.stopCleanup <- true
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -7,28 +7,8 @@ import (
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/alexedwards/scs/session"
|
||||
)
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
dsn := os.Getenv("SESSION_MYSQL_TEST_DSN")
|
||||
db, err := sql.Open("mysql", dsn)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
if err = db.Ping(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
m := New(db, 0)
|
||||
_, ok := interface{}(m).(session.Engine)
|
||||
if ok == false {
|
||||
t.Fatalf("got %v: expected %v", ok, true)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFind(t *testing.T) {
|
||||
dsn := os.Getenv("SESSION_MYSQL_TEST_DSN")
|
||||
db, err := sql.Open("mysql", dsn)
|
||||
@ -229,7 +209,7 @@ func TestDelete(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSweeper(t *testing.T) {
|
||||
func TestCleanup(t *testing.T) {
|
||||
dsn := os.Getenv("SESSION_MYSQL_TEST_DSN")
|
||||
db, err := sql.Open("mysql", dsn)
|
||||
if err != nil {
|
||||
@ -245,7 +225,7 @@ func TestSweeper(t *testing.T) {
|
||||
}
|
||||
|
||||
m := New(db, 200*time.Millisecond)
|
||||
defer m.StopSweeper()
|
||||
defer m.StopCleanup()
|
||||
|
||||
err = m.Save("session_token", []byte("encoded_data"), time.Now().Add(100*time.Millisecond))
|
||||
if err != nil {
|
||||
@ -273,7 +253,7 @@ func TestSweeper(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestStopNilSweeper(t *testing.T) {
|
||||
func TestStopNilCleanup(t *testing.T) {
|
||||
dsn := os.Getenv("SESSION_MYSQL_TEST_DSN")
|
||||
db, err := sql.Open("mysql", dsn)
|
||||
if err != nil {
|
||||
@ -287,5 +267,5 @@ func TestStopNilSweeper(t *testing.T) {
|
||||
m := New(db, 0)
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
// A send to a nil channel will block forever
|
||||
m.StopSweeper()
|
||||
m.StopCleanup()
|
||||
}
|
||||
|
127
engine/pgstore/README.md
Normal file
127
engine/pgstore/README.md
Normal file
@ -0,0 +1,127 @@
|
||||
# pgstore
|
||||
[](https://godoc.org/github.com/alexedwards/scs/engine/pgstore)
|
||||
|
||||
Package pgstore is a PostgreSQL-based storage engine for the [SCS session package](https://godoc.org/github.com/alexedwards/scs/session).
|
||||
|
||||
## Usage
|
||||
|
||||
### Installation
|
||||
|
||||
Either:
|
||||
|
||||
```
|
||||
$ go get github.com/alexedwards/scs/engine/pgstore
|
||||
```
|
||||
|
||||
Or (recommended) use use [gvt](https://github.com/FiloSottile/gvt) to vendor the `engine/pgstore` and `session` sub-packages:
|
||||
|
||||
```
|
||||
$ gvt fetch github.com/alexedwards/scs/engine/pgstore
|
||||
$ gvt fetch github.com/alexedwards/scs/session
|
||||
```
|
||||
|
||||
### Setup
|
||||
|
||||
You should have a working PostgreSQL database containing a `sessions` table with the definition:
|
||||
|
||||
```sql
|
||||
CREATE TABLE sessions (
|
||||
token TEXT PRIMARY KEY,
|
||||
data BYTEA NOT NULL,
|
||||
expiry TIMESTAMPTZ NOT NULL
|
||||
);
|
||||
CREATE INDEX sessions_expiry_idx ON sessions (expiry);
|
||||
```
|
||||
|
||||
### Example
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/alexedwards/scs/engine/pgstore"
|
||||
"github.com/alexedwards/scs/session"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Establish a database/sql pool
|
||||
db, err := sql.Open("postgres", "postgres://user:pass@localhost/db")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
// Create a new PGStore instance using the existing database/sql pool,
|
||||
// with a cleanup interval of 5 minutes.
|
||||
engine := pgstore.New(db, 5*time.Minute)
|
||||
|
||||
sessionManager := session.Manage(engine)
|
||||
http.HandleFunc("/put", putHandler)
|
||||
http.HandleFunc("/get", getHandler)
|
||||
http.ListenAndServe(":4000", sessionManager(http.DefaultServeMux))
|
||||
}
|
||||
|
||||
func putHandler(w http.ResponseWriter, r *http.Request) {
|
||||
err := session.PutString(r, "message", "Hello world!")
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
}
|
||||
}
|
||||
|
||||
func getHandler(w http.ResponseWriter, r *http.Request) {
|
||||
msg, err := session.GetString(r, "message")
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
}
|
||||
io.WriteString(w, msg)
|
||||
}
|
||||
```
|
||||
|
||||
### Cleaning up expired session data
|
||||
|
||||
The pgstore package provides a background 'cleanup' goroutine to delete expired session data. This stops the database table from holding on to invalid sessions indefinitely and growing unnecessarily large.
|
||||
|
||||
You can specify how frequently to run the cleanup when creating a new PGstore instance:
|
||||
|
||||
```go
|
||||
// Run a cleanup every 30 minutes.
|
||||
pgstore.New(db, 30*time.Minute)
|
||||
|
||||
// Setting the cleanup interval to zero prevents the cleanup from being run.
|
||||
pgstore.New(db, 0)
|
||||
```
|
||||
|
||||
#### Terminating the cleanup goroutine
|
||||
|
||||
It's rare that the cleanup goroutine for a PGStore instance needs to be terminated. It is generally intended to be long-lived and run for the lifetime of your application.
|
||||
|
||||
However, there may be occasions when your use of a PGStore instance is transient. A common example would be using it in a short-lived test function. In this scenario, the cleanup goroutine (which will run forever) will prevent the PGStore object from being garbage collected even after the test function has finished. You can prevent this by manually calling `StopCleanup()`.
|
||||
|
||||
For example:
|
||||
|
||||
```go
|
||||
func TestExample(t *testing.T) {
|
||||
db, err := sql.Open("postgres", "postgres://user:pass@localhost/db")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
engine := New(db, time.Second)
|
||||
defer engine.StopCleanup()
|
||||
|
||||
// Run test...
|
||||
}
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
The pgstore package is underpinned by the excellent [pq](https://github.com/lib/pq) driver.
|
||||
|
||||
Full godoc documentation: [https://godoc.org/github.com/alexedwards/scs/engine/pgstore](https://godoc.org/github.com/alexedwards/scs/engine/pgstore).
|
@ -1,3 +1,38 @@
|
||||
// Package pgstore is a PostgreSQL-based storage engine for the SCS session package.
|
||||
//
|
||||
// A working PostgreSQL database is required, containing a sessions table with
|
||||
// the definition:
|
||||
//
|
||||
// CREATE TABLE sessions (
|
||||
// token TEXT PRIMARY KEY,
|
||||
// data BYTEA NOT NULL,
|
||||
// expiry TIMESTAMPTZ NOT NULL
|
||||
// );
|
||||
// CREATE INDEX sessions_expiry_idx ON sessions (expiry);
|
||||
//
|
||||
// The pgstore package provides a background 'cleanup' goroutine to delete expired
|
||||
// session data. This stops the database table from holding on to invalid sessions
|
||||
// indefinitely and growing unnecessarily large.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// func main() {
|
||||
// // Establish a database/sql pool
|
||||
// db, err := sql.Open("postgres", "postgres://user:pass@localhost/db")
|
||||
// if err != nil {
|
||||
// log.Fatal(err)
|
||||
// }
|
||||
// defer db.Close()
|
||||
//
|
||||
// // Create a new PGStore instance using the existing database/sql pool,
|
||||
// // with a cleanup interval of 5 minutes.
|
||||
// engine := pgstore.New(db, 5*time.Minute)
|
||||
//
|
||||
// sessionManager := session.Manage(engine)
|
||||
// http.ListenAndServe(":4000", sessionManager(http.DefaultServeMux))
|
||||
// }
|
||||
//
|
||||
// The pgstore package is underpinned by the pq driver (https://github.com/lib/pq).
|
||||
package pgstore
|
||||
|
||||
import (
|
||||
@ -5,26 +40,35 @@ import (
|
||||
"log"
|
||||
"time"
|
||||
|
||||
// Register lib/pq with database/sql
|
||||
_ "github.com/lib/pq"
|
||||
)
|
||||
|
||||
// PGStore represents the currently configured session storage engine.
|
||||
type PGStore struct {
|
||||
*sql.DB
|
||||
stopSweeper chan bool
|
||||
stopCleanup chan bool
|
||||
}
|
||||
|
||||
func New(db *sql.DB, sweepInterval time.Duration) *PGStore {
|
||||
// New returns a new PGStore instance.
|
||||
//
|
||||
// The cleanupInterval parameter controls how frequently expired session data
|
||||
// is removed by the background cleanup goroutine. Setting it to 0 prevents
|
||||
// the cleanup goroutine from running (i.e. expired sessions will not be removed).
|
||||
func New(db *sql.DB, cleanupInterval time.Duration) *PGStore {
|
||||
p := &PGStore{DB: db}
|
||||
if sweepInterval > 0 {
|
||||
go p.startSweeper(sweepInterval)
|
||||
if cleanupInterval > 0 {
|
||||
go p.startCleanup(cleanupInterval)
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
func (p *PGStore) Find(token string) ([]byte, bool, error) {
|
||||
var b []byte
|
||||
// Find returns the data for a given session token from the PGStore instance. If
|
||||
// the session token is not found or is expired, the returned exists flag will
|
||||
// be set to false.
|
||||
func (p *PGStore) Find(token string) (b []byte, exists bool, err error) {
|
||||
row := p.DB.QueryRow("SELECT data FROM sessions WHERE token = $1 AND current_timestamp < expiry", token)
|
||||
err := row.Scan(&b)
|
||||
err = row.Scan(&b)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, false, nil
|
||||
} else if err != nil {
|
||||
@ -33,6 +77,8 @@ func (p *PGStore) Find(token string) ([]byte, bool, error) {
|
||||
return b, true, nil
|
||||
}
|
||||
|
||||
// Save adds a session token and data to the PGStore instance with the given expiry time.
|
||||
// If the session token already exists then the data and expiry time are updated.
|
||||
func (p *PGStore) Save(token string, b []byte, expiry time.Time) error {
|
||||
_, err := p.DB.Exec("INSERT INTO sessions (token, data, expiry) VALUES ($1, $2, $3) ON CONFLICT (token) DO UPDATE SET data = EXCLUDED.data, expiry = EXCLUDED.expiry", token, b, expiry)
|
||||
if err != nil {
|
||||
@ -41,13 +87,14 @@ func (p *PGStore) Save(token string, b []byte, expiry time.Time) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete removes a session token and corresponding data from the PGStore instance.
|
||||
func (p *PGStore) Delete(token string) error {
|
||||
_, err := p.DB.Exec("DELETE FROM sessions WHERE token = $1", token)
|
||||
return err
|
||||
}
|
||||
|
||||
func (p *PGStore) startSweeper(interval time.Duration) {
|
||||
p.stopSweeper = make(chan bool)
|
||||
func (p *PGStore) startCleanup(interval time.Duration) {
|
||||
p.stopCleanup = make(chan bool)
|
||||
ticker := time.NewTicker(interval)
|
||||
for {
|
||||
select {
|
||||
@ -56,16 +103,41 @@ func (p *PGStore) startSweeper(interval time.Duration) {
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
case <-p.stopSweeper:
|
||||
case <-p.stopCleanup:
|
||||
ticker.Stop()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PGStore) StopSweeper() {
|
||||
if p.stopSweeper != nil {
|
||||
p.stopSweeper <- true
|
||||
// StopCleanup terminates the background cleanup goroutine for the PGStore instance.
|
||||
// It's rare to terminate this; generally PGStore instances and their cleanup
|
||||
// goroutines are intended to be long-lived and run for the lifetime of your
|
||||
// application.
|
||||
//
|
||||
// There may be occasions though when your use of the PGStore is transient. An
|
||||
// example is creating a new PGStore instance in a test function. In this scenario,
|
||||
// the cleanup goroutine (which will run forever) will prevent the PGStore object
|
||||
// from being garbage collected even after the test function has finished. You
|
||||
// can prevent this by manually calling StopCleanup.
|
||||
//
|
||||
// Example:
|
||||
//
|
||||
// func TestExample(t *testing.T) {
|
||||
// db, err := sql.Open("postgres", "postgres://user:pass@localhost/db")
|
||||
// if err != nil {
|
||||
// t.Fatal(err)
|
||||
// }
|
||||
// defer db.Close()
|
||||
//
|
||||
// engine := pgstore.New(db, time.Second)
|
||||
// defer engine.StopCleanup()
|
||||
//
|
||||
// // Run test...
|
||||
// }
|
||||
func (p *PGStore) StopCleanup() {
|
||||
if p.stopCleanup != nil {
|
||||
p.stopCleanup <- true
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -229,7 +229,7 @@ func TestDelete(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSweeper(t *testing.T) {
|
||||
func TestCleanup(t *testing.T) {
|
||||
dsn := os.Getenv("SESSION_PG_TEST_DSN")
|
||||
db, err := sql.Open("postgres", dsn)
|
||||
if err != nil {
|
||||
@ -245,7 +245,7 @@ func TestSweeper(t *testing.T) {
|
||||
}
|
||||
|
||||
p := New(db, 200*time.Millisecond)
|
||||
defer p.StopSweeper()
|
||||
defer p.StopCleanup()
|
||||
|
||||
err = p.Save("session_token", []byte("encoded_data"), time.Now().Add(100*time.Millisecond))
|
||||
if err != nil {
|
||||
@ -273,7 +273,7 @@ func TestSweeper(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestStopNilSweeper(t *testing.T) {
|
||||
func TestStopNilCleanup(t *testing.T) {
|
||||
dsn := os.Getenv("SESSION_PG_TEST_DSN")
|
||||
db, err := sql.Open("postgres", dsn)
|
||||
if err != nil {
|
||||
@ -287,5 +287,5 @@ func TestStopNilSweeper(t *testing.T) {
|
||||
p := New(db, 0)
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
// A send to a nil channel will block forever
|
||||
p.StopSweeper()
|
||||
p.StopCleanup()
|
||||
}
|
||||
|
@ -0,0 +1,83 @@
|
||||
# redisstore
|
||||
[](https://godoc.org/github.com/alexedwards/scs/engine/redisstore)
|
||||
|
||||
Package redisstore is a Redis-based storage engine for the [SCS session package](https://godoc.org/github.com/alexedwards/scs/session).
|
||||
|
||||
Warning: The redisstore API is not finalized and may change, possibly significantly. The package is fine to use as-is, but it is strongly recommended that you vendor the package to avoid compatibility problems in the future.
|
||||
|
||||
## Usage
|
||||
|
||||
### Installation
|
||||
|
||||
Either:
|
||||
|
||||
```
|
||||
$ go get github.com/alexedwards/scs/engine/redisstore
|
||||
```
|
||||
|
||||
Or (recommended) use use [gvt](https://github.com/FiloSottile/gvt) to vendor the `engine/redisstore` and `session` sub-packages:
|
||||
|
||||
```
|
||||
$ gvt fetch github.com/alexedwards/scs/engine/redisstore
|
||||
$ gvt fetch github.com/alexedwards/scs/session
|
||||
```
|
||||
|
||||
### Example
|
||||
|
||||
The redisstore package uses the popular [Redigo](https://github.com/garyburd/redigo) Redis client.
|
||||
|
||||
You should follow the Redigo instructions to [setup a connection pool](https://godoc.org/github.com/garyburd/redigo/redis#Pool), and pass the pool to `redisstore.New()` to establish the session storage engine.
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/alexedwards/scs/engine/redisstore"
|
||||
"github.com/alexedwards/scs/session"
|
||||
"github.com/garyburd/redigo/redis"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Establish a Redigo connection pool.
|
||||
pool := &redis.Pool{
|
||||
MaxIdle: 10,
|
||||
Dial: func() (redis.Conn, error) {
|
||||
return redis.Dial("tcp", "localhost:6379")
|
||||
},
|
||||
}
|
||||
|
||||
// Create a new RedisStore instance using the connection pool.
|
||||
engine := redisstore.New(pool)
|
||||
|
||||
sessionManager := session.Manage(engine)
|
||||
http.HandleFunc("/put", putHandler)
|
||||
http.HandleFunc("/get", getHandler)
|
||||
http.ListenAndServe(":4000", sessionManager(http.DefaultServeMux))
|
||||
}
|
||||
|
||||
func putHandler(w http.ResponseWriter, r *http.Request) {
|
||||
err := session.PutString(r, "message", "Hello world!")
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
}
|
||||
}
|
||||
|
||||
func getHandler(w http.ResponseWriter, r *http.Request) {
|
||||
msg, err := session.GetString(r, "message")
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), 500)
|
||||
}
|
||||
io.WriteString(w, msg)
|
||||
}
|
||||
```
|
||||
|
||||
### Cleaning up expired session data
|
||||
|
||||
Redis will [automatically remove](http://redis.io/commands/expire#how-redis-expires-keys) expired session keys.
|
||||
|
||||
## Notes
|
||||
|
||||
Full godoc documentation: [https://godoc.org/github.com/alexedwards/scs/engine/redisstore](https://godoc.org/github.com/alexedwards/scs/engine/redisstore).
|
@ -1,3 +1,30 @@
|
||||
// Package redisstore is a Redis-based storage engine for the SCS session package.
|
||||
//
|
||||
// Warning: The redisstore API is not finalized and may change, possibly significantly.
|
||||
// The package is fine to use as-is, but it is strongly recommended that you vendor
|
||||
// the package to avoid compatibility problems in the future.
|
||||
//
|
||||
// The redisstore pacakge relies on the the popular Redigo Redis client
|
||||
// (https://github.com/garyburd/redigo).
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// func main() {
|
||||
// // Establish a Redigo connection pool following instructions at
|
||||
// // https://godoc.org/github.com/garyburd/redigo/redis#Pool
|
||||
// pool := &redis.Pool{
|
||||
// MaxIdle: 10,
|
||||
// Dial: func() (redis.Conn, error) {
|
||||
// return redis.Dial("tcp", "localhost:6379")
|
||||
// },
|
||||
// }
|
||||
//
|
||||
// // Create a new RedisStore instance using the connection pool.
|
||||
// engine := redisstore.New(pool)
|
||||
//
|
||||
// sessionManager := session.Manage(engine)
|
||||
// http.ListenAndServe(":4000", sessionManager(http.DefaultServeMux))
|
||||
// }
|
||||
package redisstore
|
||||
|
||||
import (
|
||||
@ -6,21 +33,29 @@ import (
|
||||
"github.com/garyburd/redigo/redis"
|
||||
)
|
||||
|
||||
// Prefix controls the Redis key prefix. You should only need to change this if there is
|
||||
// a naming clash.
|
||||
var Prefix = "scs:session:"
|
||||
|
||||
// RedisStore represents the currently configured session storage engine. It is essentially
|
||||
// a wrapper around a Redigo connection pool.
|
||||
type RedisStore struct {
|
||||
pool *redis.Pool
|
||||
}
|
||||
|
||||
// New returns a new RedisStore instance. The pool parameter should be a pointer to a
|
||||
// Redigo connection pool. See https://godoc.org/github.com/garyburd/redigo/redis#Pool.
|
||||
func New(pool *redis.Pool) *RedisStore {
|
||||
return &RedisStore{pool}
|
||||
}
|
||||
|
||||
func (r *RedisStore) Find(token string) ([]byte, bool, error) {
|
||||
// Find returns the data for a given session token from the RedisStore instance. If the session
|
||||
// token is not found or is expired, the returned exists flag will be set to false.
|
||||
func (r *RedisStore) Find(token string) (b []byte, exists bool, err error) {
|
||||
conn := r.pool.Get()
|
||||
defer conn.Close()
|
||||
|
||||
b, err := redis.Bytes(conn.Do("GET", Prefix+token))
|
||||
b, err = redis.Bytes(conn.Do("GET", Prefix+token))
|
||||
if err == redis.ErrNil {
|
||||
return nil, false, nil
|
||||
} else if err != nil {
|
||||
@ -29,6 +64,8 @@ func (r *RedisStore) Find(token string) ([]byte, bool, error) {
|
||||
return b, true, nil
|
||||
}
|
||||
|
||||
// Save adds a session token and data to the RedisStore instance with the given expiry time.
|
||||
// If the session token already exists then the data and expiry time are updated.
|
||||
func (r *RedisStore) Save(token string, b []byte, expiry time.Time) error {
|
||||
conn := r.pool.Get()
|
||||
defer conn.Close()
|
||||
@ -49,6 +86,7 @@ func (r *RedisStore) Save(token string, b []byte, expiry time.Time) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Delete removes a session token and corresponding data from the ResisStore instance.
|
||||
func (r *RedisStore) Delete(token string) error {
|
||||
conn := r.pool.Get()
|
||||
defer conn.Close()
|
||||
|
Reference in New Issue
Block a user