123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237 |
- package sessions
- import (
- "encoding/json"
- "errors"
- "fmt"
- "net/http"
- "time"
- "github.com/go-redis/redis"
- "github.com/gorilla/securecookie"
- gsessions "github.com/gorilla/sessions"
- "github.com/teris-io/shortid"
- )
- // RediStore stores sessions in a redis backend.
- type RediStore struct {
- RedisClient *redis.Client
- Codecs []securecookie.Codec
- SessionOptions *gsessions.Options // default configuration
- DefaultMaxAge int // default Redis TTL for a MaxAge == 0 session
- maxLength int
- keyPrefix string
- serializer SessionSerializer
- }
- func NewRediStore(client *redis.Client, keyPrefix string, keyPairs ...[]byte) *RediStore {
- if keyPrefix == "" {
- keyPrefix = "session_"
- }
- rs := &RediStore{
- RedisClient: client,
- Codecs: securecookie.CodecsFromPairs(keyPairs...),
- SessionOptions: &gsessions.Options{
- Path: defaultPath,
- MaxAge: defaultMaxAge,
- },
- DefaultMaxAge: 60 * 20, // 20 minutes seems like a reasonable default
- maxLength: 4096,
- keyPrefix: keyPrefix,
- serializer: JSONSerializer{},
- }
- return rs
- }
- // SessionSerializer provides an interface hook for alternative serializers
- type SessionSerializer interface {
- Deserialize(d []byte, ss *gsessions.Session) error
- Serialize(ss *gsessions.Session) ([]byte, error)
- }
- // JSONSerializer encode the session map to JSON.
- type JSONSerializer struct{}
- // Serialize to JSON. Will err if there are unmarshalable key values
- func (s JSONSerializer) Serialize(ss *gsessions.Session) ([]byte, error) {
- m := make(map[string]interface{}, len(ss.Values))
- for k, v := range ss.Values {
- ks, ok := k.(string)
- if !ok {
- err := fmt.Errorf("Non-string key value, cannot serialize session to JSON: %v", k)
- fmt.Printf("redistore.JSONSerializer.serialize() Error: %v", err)
- return nil, err
- }
- m[ks] = v
- }
- return json.Marshal(m)
- }
- // Deserialize back to map[string]interface{}
- func (s JSONSerializer) Deserialize(d []byte, ss *gsessions.Session) error {
- m := make(map[string]interface{})
- err := json.Unmarshal(d, &m)
- if err != nil {
- fmt.Printf("redistore.JSONSerializer.deserialize() Error: %v", err)
- return err
- }
- for k, v := range m {
- ss.Values[k] = v
- }
- return nil
- }
- // SetMaxLength sets RediStore.maxLength if the `l` argument is greater or equal 0
- // maxLength restricts the maximum length of new sessions to l.
- // If l is 0 there is no limit to the size of a session, use with caution.
- // The default for a new RediStore is 4096. Redis allows for max.
- // value sizes of up to 512MB (http://redis.io/topics/data-types)
- // Default: 4096,
- func (s *RediStore) SetMaxLength(l int) {
- if l >= 0 {
- s.maxLength = l
- }
- }
- func (s *RediStore) Options(options Options) {
- s.SessionOptions = &gsessions.Options{
- Path: options.Path,
- Domain: options.Domain,
- MaxAge: options.MaxAge,
- Secure: options.Secure,
- HttpOnly: options.HttpOnly,
- }
- }
- // SetSerializer sets the serializer
- func (s *RediStore) SetSerializer(ss SessionSerializer) {
- s.serializer = ss
- }
- // SetMaxAge restricts the maximum age, in seconds, of the session record
- // both in database and a browser. This is to change session storage configuration.
- // If you want just to remove session use your session `s` object and change it's
- // `Options.MaxAge` to -1, as specified in
- // http://godoc.org/github.com/gorilla/sessions#Options
- //
- // Default is the one provided by this package value - `sessionExpire`.
- // Set it to 0 for no restriction.
- // Because we use `MaxAge` also in SecureCookie crypting algorithm you should
- // use this function to change `MaxAge` value.
- func (s *RediStore) SetMaxAge(v int) {
- var c *securecookie.SecureCookie
- var ok bool
- s.SessionOptions.MaxAge = v
- for i := range s.Codecs {
- if c, ok = s.Codecs[i].(*securecookie.SecureCookie); ok {
- c.MaxAge(v)
- } else {
- fmt.Printf("Can't change MaxAge on codec %v\n", s.Codecs[i])
- }
- }
- }
- // Get returns a session for the given name after adding it to the registry.
- //
- // See gorilla/sessions FilesystemStore.Get().
- func (s *RediStore) Get(r *http.Request, name string) (*gsessions.Session, error) {
- return gsessions.GetRegistry(r).Get(s, name)
- }
- // New returns a session for the given name without adding it to the registry.
- //
- // See gorilla/sessions FilesystemStore.New().
- func (s *RediStore) New(r *http.Request, name string) (*gsessions.Session, error) {
- var (
- err error
- ok bool
- )
- session := gsessions.NewSession(s, name)
- // make a copy
- options := *s.SessionOptions
- session.Options = &options
- session.IsNew = true
- if c, errCookie := r.Cookie(name); errCookie == nil {
- session.ID = c.Value
- ok, err = s.load(session)
- session.IsNew = !(err == nil && ok)
- } else {
- session.ID = shortid.MustGenerate()
- }
- return session, err
- }
- func (s *RediStore) RenewID(r *http.Request, w http.ResponseWriter, session *gsessions.Session) error {
- _id := session.ID
- data, err := s.RedisClient.Get(s.keyPrefix + _id).Result()
- if err != nil {
- return err
- }
- session.ID = shortid.MustGenerate()
- age := session.Options.MaxAge
- if age == 0 {
- age = s.DefaultMaxAge
- }
- _, err = s.RedisClient.Set(s.keyPrefix+session.ID, data, time.Duration(age)*time.Second).Result()
- if err != nil {
- return err
- }
- http.SetCookie(w, gsessions.NewCookie(session.Name(), session.ID, session.Options))
- return nil
- }
- // Save adds a single session to the response.
- func (s *RediStore) Save(r *http.Request, w http.ResponseWriter, session *gsessions.Session) error {
- // Marked for deletion.
- if session.Options.MaxAge < 0 || len(session.Values) == 0 {
- if err := s.delete(session); err != nil {
- return err
- }
- return nil
- } else {
- // Build an alphanumeric key for the redis store.
- if session.ID == "" {
- session.ID = shortid.MustGenerate()
- }
- if err := s.save(session); err != nil {
- return err
- }
- }
- return nil
- }
- // save stores the session in redis.
- func (s *RediStore) save(session *gsessions.Session) error {
- b, err := s.serializer.Serialize(session)
- if err != nil {
- return err
- }
- if s.maxLength != 0 && len(b) > s.maxLength {
- return errors.New("SessionStore: the value to store is too big")
- }
- age := session.Options.MaxAge
- if age == 0 {
- age = s.DefaultMaxAge
- }
- _, err = s.RedisClient.Set(s.keyPrefix+session.ID, b, time.Duration(age)*time.Second).Result()
- return err
- }
- // load reads the session from redis.
- // returns true if there is a sessoin data in DB
- func (s *RediStore) load(session *gsessions.Session) (bool, error) {
- data, err := s.RedisClient.Get(s.keyPrefix + session.ID).Result()
- if err != nil {
- return false, err
- }
- if data == "" {
- return false, nil // no data was associated with this key
- }
- return true, s.serializer.Deserialize([]byte(data), session)
- }
- // delete removes keys from redis if MaxAge<0
- func (s *RediStore) delete(session *gsessions.Session) error {
- _, err := s.RedisClient.Del(s.keyPrefix + session.ID).Result()
- return err
- }
|