lxg před 3 roky
revize
f8c9b8b9a2
5 změnil soubory, kde provedl 869 přidání a 0 odebrání
  1. 21 0
      LICENSE
  2. 210 0
      README.md
  3. 200 0
      gormstore.go
  4. 237 0
      redistore.go
  5. 201 0
      sessions.go

+ 21 - 0
LICENSE

@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2016 Gin-Gonic
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 210 - 0
README.md

@@ -0,0 +1,210 @@
+# sessions
+
+[![Build Status](https://travis-ci.org/gin-contrib/sessions.svg)](https://travis-ci.org/gin-contrib/sessions)
+[![codecov](https://codecov.io/gh/gin-contrib/sessions/branch/master/graph/badge.svg)](https://codecov.io/gh/gin-contrib/sessions)
+[![Go Report Card](https://goreportcard.com/badge/github.com/penggy/sessions)](https://goreportcard.com/report/github.com/penggy/sessions)
+[![GoDoc](https://godoc.org/github.com/penggy/sessions?status.svg)](https://godoc.org/github.com/penggy/sessions)
+[![Join the chat at https://gitter.im/gin-gonic/gin](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/gin-gonic/gin)
+
+Gin middleware for session management with multi-backend support (currently cookie, Redis, Memcached, MongoDB, memstore).
+
+## Usage
+
+### Start using it
+
+Download and install it:
+
+```bash
+$ go get github.com/penggy/sessions
+```
+
+Import it in your code:
+
+```go
+import "github.com/penggy/sessions"
+```
+
+## Examples
+
+#### cookie-based
+
+[embedmd]:# (example_cookie/main.go go)
+```go
+package main
+
+import (
+	"github.com/penggy/sessions"
+	"github.com/penggy/sessions/cookie"
+	"github.com/gin-gonic/gin"
+)
+
+func main() {
+	r := gin.Default()
+	store := cookie.NewStore([]byte("secret"))
+	r.Use(sessions.Sessions("mysession", store))
+
+	r.GET("/incr", func(c *gin.Context) {
+		session := sessions.Default(c)
+		var count int
+		v := session.Get("count")
+		if v == nil {
+			count = 0
+		} else {
+			count = v.(int)
+			count++
+		}
+		session.Set("count", count)
+		session.Save()
+		c.JSON(200, gin.H{"count": count})
+	})
+	r.Run(":8000")
+}
+```
+
+#### Redis
+
+[embedmd]:# (example_redis/main.go go)
+```go
+package main
+
+import (
+	"github.com/penggy/sessions"
+	"github.com/penggy/sessions/redis"
+	"github.com/gin-gonic/gin"
+)
+
+func main() {
+	r := gin.Default()
+	store, _ := redis.NewStore(10, "tcp", "localhost:6379", "", []byte("secret"))
+	r.Use(sessions.Sessions("mysession", store))
+
+	r.GET("/incr", func(c *gin.Context) {
+		session := sessions.Default(c)
+		var count int
+		v := session.Get("count")
+		if v == nil {
+			count = 0
+		} else {
+			count = v.(int)
+			count++
+		}
+		session.Set("count", count)
+		session.Save()
+		c.JSON(200, gin.H{"count": count})
+	})
+	r.Run(":8000")
+}
+```
+
+#### Memcached
+
+[embedmd]:# (example_memcached/main.go go)
+```go
+package main
+
+import (
+	"github.com/bradfitz/gomemcache/memcache"
+	"github.com/penggy/sessions"
+	"github.com/penggy/sessions/memcached"
+	"github.com/gin-gonic/gin"
+)
+
+func main() {
+	r := gin.Default()
+	store := memcached.NewStore(memcache.New("localhost:11211"), "", []byte("secret"))
+	r.Use(sessions.Sessions("mysession", store))
+
+	r.GET("/incr", func(c *gin.Context) {
+		session := sessions.Default(c)
+		var count int
+		v := session.Get("count")
+		if v == nil {
+			count = 0
+		} else {
+			count = v.(int)
+			count++
+		}
+		session.Set("count", count)
+		session.Save()
+		c.JSON(200, gin.H{"count": count})
+	})
+	r.Run(":8000")
+}
+```
+
+#### MongoDB
+
+[embedmd]:# (example_mongo/main.go go)
+```go
+package main
+
+import (
+	"github.com/penggy/sessions"
+	"github.com/penggy/sessions/mongo"
+	"github.com/gin-gonic/gin"
+	"gopkg.in/mgo.v2"
+)
+
+func main() {
+	r := gin.Default()
+	session, err := mgo.Dial("localhost:27017/test")
+	if err != nil {
+		// handle err
+	}
+
+	c := session.DB("").C("sessions")
+	store := mongo.NewStore(c, 3600, true, []byte("secret"))
+	r.Use(sessions.Sessions("mysession", store))
+
+	r.GET("/incr", func(c *gin.Context) {
+		session := sessions.Default(c)
+		var count int
+		v := session.Get("count")
+		if v == nil {
+			count = 0
+		} else {
+			count = v.(int)
+			count++
+		}
+		session.Set("count", count)
+		session.Save()
+		c.JSON(200, gin.H{"count": count})
+	})
+	r.Run(":8000")
+}
+```
+
+#### memstore
+
+[embedmd]:# (example_memstore/main.go go)
+```go
+package main
+
+import (
+	"github.com/penggy/sessions"
+	"github.com/penggy/sessions/memstore"
+	"github.com/gin-gonic/gin"
+)
+
+func main() {
+	r := gin.Default()
+	store := memstore.NewStore([]byte("secret"))
+	r.Use(sessions.Sessions("mysession", store))
+
+	r.GET("/incr", func(c *gin.Context) {
+		session := sessions.Default(c)
+		var count int
+		v := session.Get("count")
+		if v == nil {
+			count = 0
+		} else {
+			count = v.(int)
+			count++
+		}
+		session.Set("count", count)
+		session.Save()
+		c.JSON(200, gin.H{"count": count})
+	})
+	r.Run(":8000")
+}
+```

+ 200 - 0
gormstore.go

@@ -0,0 +1,200 @@
+package sessions
+
+import (
+	"net/http"
+	"time"
+
+	"github.com/gorilla/context"
+	"github.com/gorilla/securecookie"
+	gsessions "github.com/gorilla/sessions"
+	"github.com/jinzhu/gorm"
+	"github.com/teris-io/shortid"
+)
+
+// Options for gormstore
+type GormStoreOptions struct {
+	TableName       string
+	SkipCreateTable bool
+}
+
+// Store represent a gormstore
+type GormStore struct {
+	db          *gorm.DB
+	opts        GormStoreOptions
+	Codecs      []securecookie.Codec
+	SessionOpts *gsessions.Options
+}
+
+type gormSession struct {
+	ID        string `sql:"unique_index"`
+	Data      string `sql:"type:text"`
+	CreatedAt time.Time
+	UpdatedAt time.Time
+	ExpiresAt time.Time `sql:"index"`
+
+	tableName string `sql:"-"` // just for convenience instead of db.Table(...)
+}
+
+// Define a type for context keys so that they can't clash with anything else stored in context
+type contextKey string
+
+func (gs *gormSession) TableName() string {
+	return gs.tableName
+}
+
+// New creates a new gormstore session
+func NewGormStore(db *gorm.DB, keyPairs ...[]byte) *GormStore {
+	return NewGormStoreWithOptions(db, GormStoreOptions{}, keyPairs...)
+}
+
+// NewOptions creates a new gormstore session with options
+func NewGormStoreWithOptions(db *gorm.DB, opts GormStoreOptions, keyPairs ...[]byte) *GormStore {
+	st := &GormStore{
+		db:     db,
+		opts:   opts,
+		Codecs: securecookie.CodecsFromPairs(keyPairs...),
+		SessionOpts: &gsessions.Options{
+			Path:   defaultPath,
+			MaxAge: defaultMaxAge,
+		},
+	}
+	if st.opts.TableName == "" {
+		st.opts.TableName = "t_sessions"
+	}
+
+	if !st.opts.SkipCreateTable {
+		st.db.AutoMigrate(&gormSession{tableName: st.opts.TableName})
+	}
+	st.Cleanup()
+	return st
+}
+
+// Get returns a session for the given name after adding it to the registry.
+func (st *GormStore) Get(r *http.Request, name string) (*gsessions.Session, error) {
+	return gsessions.GetRegistry(r).Get(st, name)
+}
+
+// New creates a session with name without adding it to the registry.
+func (st *GormStore) New(r *http.Request, name string) (*gsessions.Session, error) {
+	session := gsessions.NewSession(st, name)
+	opts := *st.SessionOpts
+	session.Options = &opts
+	session.IsNew = true
+	st.MaxAge(st.SessionOpts.MaxAge)
+
+	// try fetch from db if there is a cookie
+	if cookie, err := r.Cookie(name); err == nil {
+		session.ID = cookie.Value
+		s := &gormSession{tableName: st.opts.TableName}
+		if err := st.db.Where("id = ? AND expires_at > ?", session.ID, gorm.NowFunc()).First(s).Error; err != nil {
+			return session, nil
+		}
+		if err := securecookie.DecodeMulti(session.Name(), s.Data, &session.Values, st.Codecs...); err != nil {
+			return session, nil
+		}
+		session.IsNew = false
+		context.Set(r, contextKey(name), s)
+	} else {
+		session.ID = shortid.MustGenerate()
+	}
+
+	return session, nil
+}
+
+func (st *GormStore) RenewID(r *http.Request, w http.ResponseWriter, session *gsessions.Session) error {
+	_id := session.ID
+	session.ID = shortid.MustGenerate()
+	st.db.Exec("UPDATE "+st.opts.TableName+" SET id=? WHERE id=?", session.ID, _id)
+	http.SetCookie(w, gsessions.NewCookie(session.Name(), session.ID, session.Options))
+	return nil
+}
+
+// Save session and set cookie header
+func (st *GormStore) Save(r *http.Request, w http.ResponseWriter, session *gsessions.Session) error {
+	s, _ := context.Get(r, contextKey(session.Name())).(*gormSession)
+
+	// delete if max age is < 0
+	if session.Options.MaxAge < 0 || len(session.Values) == 0 {
+		if s != nil {
+			if err := st.db.Delete(s).Error; err != nil {
+				return err
+			}
+		}
+		return nil
+	}
+
+	data, err := securecookie.EncodeMulti(session.Name(), session.Values, st.Codecs...)
+	if err != nil {
+		return err
+	}
+	now := time.Now()
+	expire := now.Add(time.Second * time.Duration(session.Options.MaxAge))
+
+	if s == nil {
+		// generate random session ID key suitable for storage in the db
+		if session.ID == "" {
+			session.ID = shortid.MustGenerate()
+		}
+		s = &gormSession{
+			ID:        session.ID,
+			Data:      data,
+			CreatedAt: now,
+			UpdatedAt: now,
+			ExpiresAt: expire,
+			tableName: st.opts.TableName,
+		}
+		if err := st.db.Create(s).Error; err != nil {
+			return err
+		}
+		context.Set(r, contextKey(session.Name()), s)
+	} else {
+		s.Data = data
+		s.UpdatedAt = now
+		s.ExpiresAt = expire
+		if err := st.db.Save(s).Error; err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+// MaxAge sets the maximum age for the store and the underlying cookie
+// implementation. Individual sessions can be deleted by setting
+// Options.MaxAge = -1 for that session.
+func (st *GormStore) MaxAge(age int) {
+	st.SessionOpts.MaxAge = age
+	for _, codec := range st.Codecs {
+		if sc, ok := codec.(*securecookie.SecureCookie); ok {
+			sc.MaxAge(age)
+		}
+	}
+}
+
+func (st *GormStore) Options(options Options) {
+	st.SessionOpts = &gsessions.Options{
+		Path:     options.Path,
+		Domain:   options.Domain,
+		MaxAge:   options.MaxAge,
+		Secure:   options.Secure,
+		HttpOnly: options.HttpOnly,
+	}
+}
+
+// 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 is 4096 (default for securecookie)
+func (st *GormStore) MaxLength(l int) {
+	for _, c := range st.Codecs {
+		if codec, ok := c.(*securecookie.SecureCookie); ok {
+			codec.MaxLength(l)
+		}
+	}
+}
+
+// Cleanup deletes expired sessions
+func (st *GormStore) Cleanup() {
+	st.db.Delete(&gormSession{tableName: st.opts.TableName}, "expires_at <= ?", gorm.NowFunc())
+	time.AfterFunc(15*time.Second, func() {
+		st.Cleanup()
+	})
+}

+ 237 - 0
redistore.go

@@ -0,0 +1,237 @@
+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
+}

+ 201 - 0
sessions.go

@@ -0,0 +1,201 @@
+package sessions
+
+import (
+	"log"
+	"net/http"
+	"regexp"
+	"strings"
+
+	"github.com/gin-gonic/gin"
+	"github.com/gorilla/context"
+	"github.com/gorilla/sessions"
+)
+
+const (
+	DefaultKey    = "github.com/penggy/sessions"
+	errorFormat   = "[sessions] ERROR! %s\n"
+	defaultMaxAge = 60 * 60 * 24 * 30 // 30 days
+	defaultPath   = "/"
+)
+
+type Store interface {
+	sessions.Store
+	RenewID(r *http.Request, w http.ResponseWriter, gsession *sessions.Session) error
+	Options(Options)
+}
+
+// Options stores configuration for a session or session store.
+// Fields are a subset of http.Cookie fields.
+type Options struct {
+	Path   string
+	Domain string
+	// MaxAge=0 means no 'Max-Age' attribute specified.
+	// MaxAge<0 means delete cookie now, equivalently 'Max-Age: 0'.
+	// MaxAge>0 means Max-Age attribute present and given in seconds.
+	MaxAge   int
+	Secure   bool
+	HttpOnly bool
+}
+
+// Wraps thinly gorilla-session methods.
+// Session stores the values and optional configuration for a session.
+type Session interface {
+	// Get returns the session value associated to the given key.
+	Get(key interface{}) interface{}
+	// Set sets the session value associated to the given key.
+	Set(key interface{}, val interface{})
+	// Delete removes the session value associated to the given key.
+	Delete(key interface{})
+	// Clear deletes all values in the session.
+	Clear()
+	// AddFlash adds a flash message to the session.
+	// A single variadic argument is accepted, and it is optional: it defines the flash key.
+	// If not defined "_flash" is used by default.
+	AddFlash(value interface{}, vars ...string)
+	// Flashes returns a slice of flash messages from the session.
+	// A single variadic argument is accepted, and it is optional: it defines the flash key.
+	// If not defined "_flash" is used by default.
+	Flashes(vars ...string) []interface{}
+	// Options sets confuguration for a session.
+	Options(Options)
+	// Save saves all sessions used during the current request.
+	Save() error
+
+	RenewID() (string, error)
+
+	ID() string
+
+	SetMaxAge(maxAge int)
+
+	Destroy()
+}
+
+func Sessions(name string, store Store) gin.HandlerFunc {
+	return func(c *gin.Context) {
+		s := &session{name, c.Request, store, nil, false, c.Writer, false}
+		c.Set(DefaultKey, s)
+		defer context.Clear(c.Request)
+		defer s.Save()
+		http.SetCookie(s.writer, sessions.NewCookie(s.name, s.ID(), s.Session().Options))
+		c.Next()
+	}
+}
+
+func GorillaSessions(name string, store Store) gin.HandlerFunc {
+	return func(c *gin.Context) {
+		s := &session{name, c.Request, store, nil, false, c.Writer, true}
+		c.Set(DefaultKey, s)
+		defer context.Clear(c.Request)
+		c.Next()
+	}
+}
+
+type session struct {
+	name    string
+	request *http.Request
+	store   Store
+	session *sessions.Session
+	written bool
+	writer  http.ResponseWriter
+	gorilla bool
+}
+
+func (s *session) Get(key interface{}) interface{} {
+	return s.Session().Values[key]
+}
+
+func (s *session) Set(key interface{}, val interface{}) {
+	s.Session().Values[key] = val
+	s.written = true
+}
+
+func (s *session) Delete(key interface{}) {
+	delete(s.Session().Values, key)
+	s.written = true
+}
+
+func (s *session) Clear() {
+	for key := range s.Session().Values {
+		delete(s.Session().Values, key)
+	}
+	s.written = true
+}
+
+func (s *session) AddFlash(value interface{}, vars ...string) {
+	s.Session().AddFlash(value, vars...)
+}
+
+func (s *session) Flashes(vars ...string) []interface{} {
+	return s.Session().Flashes(vars...)
+}
+
+func (s *session) Options(options Options) {
+	s.Session().Options = &sessions.Options{
+		Path:     options.Path,
+		Domain:   options.Domain,
+		MaxAge:   options.MaxAge,
+		Secure:   options.Secure,
+		HttpOnly: options.HttpOnly,
+	}
+}
+
+func (s *session) Save() error {
+	if s.Written() {
+		e := s.Session().Save(s.request, s.writer)
+		if e == nil {
+			s.written = false
+		}
+		return e
+	}
+	return nil
+}
+
+func (s *session) RenewID() (string, error) {
+	e := s.store.RenewID(s.request, s.writer, s.Session())
+	return s.ID(), e
+}
+
+func (s *session) ID() string {
+	return s.Session().ID
+}
+
+func (s *session) SetMaxAge(maxAge int) {
+	s.Session().Options.MaxAge = maxAge
+	if s.gorilla {
+		s.written = true
+	} else {
+		http.SetCookie(s.writer, sessions.NewCookie(s.name, s.Session().ID, s.Session().Options))
+	}
+}
+
+func (s *session) Destroy() {
+	s.SetMaxAge(-1)
+	s.Clear()
+}
+
+func (s *session) Written() bool {
+	return s.written
+}
+
+func (s *session) Session() *sessions.Session {
+	if s.session == nil {
+		var err error
+		s.session, err = s.store.Get(s.request, s.name)
+		if err != nil {
+			log.Printf(errorFormat, err)
+		}
+	}
+	return s.session
+}
+
+func (s *session) XHR() bool {
+	if strings.EqualFold(s.request.Header.Get("x-requested-with"), "XMLHttpRequest") {
+		return true
+	}
+	return regexp.MustCompile("\\/json$").MatchString(s.request.Header.Get("accept"))
+}
+
+// shortcut to get session
+func Default(c *gin.Context) Session {
+	return c.MustGet(DefaultKey).(Session)
+}