sessions

package
v0.0.0-...-85b7a69 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 26, 2018 License: MIT Imports: 18 Imported by: 0

README

适配gin-gonic/gin的session管理

参考以下项目,因为改动非常大,所以并非基于某一个clone的

  1. https://github.com/gorilla/sessions
  2. https://github.com/martini-contrib/sessions

gorilla/sessions依赖于https://github.com/gorilla/context,后者内部依赖一个加锁的map,不是很中意。在1.7之后,内建了context模块,可以在一定程度上优化gorilla/context的问题。

之所以一定程度是因为context库并不会改变现有的http.Request,而是返回一个新的对象,这导致一个很严重的问题,除非直接修改传入的http.Request对象,否则就无法链式的调用下去,参见如下代码

// 注意传入的next
func middleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
        userContext:=context.WithValue(context.Background(),"user","张三")
        ageContext:=context.WithValue(userContext,"age",18)
        // 这里必须递归调用
        next.ServeHTTP(rw, r.WithContext(ageContext))
    })
}

在上面的代码中,通过context包可以在http.Request对象上附加信息,但是由于会生成新的http.Request对象,所以链式调用,后续的handle并不会读取到新添加的数据,在很多场景无法使用或者会导致代码很难看。

此问题参考

  1. http://www.flysnow.org/2017/07/29/go-classic-libs-gorilla-context.html
  2. https://stackoverflow.com/questions/40199880/how-to-use-golang-1-7-context-with-http-request-for-authentication?rq=1

martini-contrib/sessions也直接依赖gorilla/sessions,所以也需要优化


之所以基于martini-contrib/sessions来修改,是因为

  1. 存在redis的store,因个人喜好,替换了一个新redis库redis.v5
  2. 适配了gin.Context对象

由于gin.Context自带context,可以直接附加数据,所以完全可以绕开https://github.com/gorilla/contexthttps://golang.org/pkg/context/

另外增加了Store的delete方法,用于删除整个cookie,而不是cookie里面的某个key。

Session

目前支持三个session后端

  1. cookie,session的内容全部序列化到cookie中返回到浏览器,Flash使用此方式
  2. file,session的内容存在本地文件中,session的id通过cookie返回到浏览器
  3. redis,session的内容存在redis数据库中,session的id通过cookie返回到浏览器

很少直接使用session

package main

import (
	"github.com/gin-gonic/gin"
	"github.com/qjw/session"
	"gopkg.in/redis.v5"
	"log"
	"net/http"
)

func main() {
	redisClient := redis.NewClient(&redis.Options{
		Addr:     "127.0.0.1:6379",
		Password: "",
		DB:       3,
	})
	if err := redisClient.Ping().Err(); err != nil {
		log.Fatal("failed to connect redis")
	}

	store, err := sessions.NewRediStore(redisClient, []byte("abcdefg"))
	if err != nil {
		log.Print(err)
	}

	r := gin.Default()
	r.GET("/ping", func(c *gin.Context) {
		// 设置session。每个session都包含若干key/value对
		session, _ := store.Get(c, "session_test")
		session.Set("key", "value")
		// 保存
		store.Save(c, session)
		// 或者 保存所有的session
		// sessions.Save(c)

		c.Redirect(http.StatusFound, "/pong")
	})

	r.GET("/pong", func(c *gin.Context) {
		// 获取session的值
		session, _ := store.Get(c, "session_test")
		value := session.Get("key")
		if value != nil {
			c.JSON(200, gin.H{
				"message": value.(string),
			})
		} else {
			c.JSON(200, gin.H{
				"message": "",
			})
		}
	})

	r.GET("/middle",
		sessions.GinSessionMiddleware(store,"session_test"),
		func(c *gin.Context) {
			// 使用中间件,自动设置session到gin.Context中,避免大量的全局变量传递
			session := c.MustGet("session").(sessions.Session)
			value := session.Get("key")
			if value != nil {
				c.JSON(200, gin.H{
					"message": value.(string),
				})
			} else {
				c.JSON(200, gin.H{
					"message": "",
				})
			}
		})
	r.Run("0.0.0.0:9090")
}

Flask

由于 gorilla/securecookie 需要一个初始密钥进行加密,所以初始化有个密钥的参数

package main

import (
	"github.com/gin-gonic/gin"
	"github.com/qjw/session"
	"net/http"
)

func main() {
	r := gin.Default()
	sessions.InitFlash([]byte("abcdefghijklmn"))

	r.GET("/ping", func(c *gin.Context) {
		sessions.AddFlash(c, "hello world")
		c.Redirect(http.StatusFound, "/pong")
	})

	r.GET("/pong", func(c *gin.Context) {
		msgs := sessions.Flashes(c)
		if len(msgs) > 0 {
			c.JSON(200, gin.H{
				"message": msgs[0].(string),
			})
		} else {
			c.JSON(200, gin.H{
				"message": "",
			})
		}
	})
	r.Run("0.0.0.0:9090")
}

输入http://127.0.0.1:9090/ping 自动跳转到http://127.0.0.1:9090/pong,并且显示ping设置的"hello world"

认证

实际情况中,只有少量接口不需要授权,所以实行白名单的方式会比较简单,但由于缺乏好的机制,这里还是传统的黑名单方式,即需要授权的接口自行添加中间件进行权限检查

所谓白名单就是做一个全局过滤(默认全部都需要授权),其中保存一个列表,在列表中请求的放开。

目前有两个难点

  1. 缺乏好的机制标示某个请求,一般使用请求url,问题是存在PATH变量的情况很麻烦
  2. 没有一种机制能够在http handle处自动注入,因为无法获取当前handle的消息,要不就统一编码白名单
package main

import (
	"github.com/gin-gonic/gin"
	"github.com/qjw/session"
	"gopkg.in/redis.v5"
	"log"
	"net/http"
)

type User struct {
	Id   int
	Name string
}

func initStore() sessions.Store{
	redisClient := redis.NewClient(&redis.Options{
		Addr:     "127.0.0.1:6379",
		Password: "",
		DB:       3,
	})
	if err := redisClient.Ping().Err(); err != nil {
		log.Fatal("failed to connect redis")
	}

	store, err := sessions.NewRediStore(redisClient, []byte("abcdefg"))
	if err != nil {
		log.Print(err)
	}
	return store
}

func main() {
	store := initStore()
	r := gin.Default()
	r.Use(sessions.GinSessionMiddleware(store, sessions.AUTH_SESSION_NAME))
	r.Use(sessions.GinAuthMiddleware(&sessions.AuthOptions{
		User:&User{},
	}))

	r.GET("/index",
		sessions.LoginRequired(),
		func(c *gin.Context) {
			// 获取登录用户
			user := sessions.LoggedUser(c).(*User)
			c.JSON(http.StatusOK, gin.H{
				"message": user.Name,
			})
		})
	r.GET("/login",
		func(c *gin.Context) {
			// 是否已经登录
			if sessions.IsAuthenticated(c){
				c.Redirect(http.StatusFound, "/index")
				return
			}
			// 登录授权
			sessions.Login(c,&User{
				Id:1,
				Name:"king",
			})
			c.Redirect(http.StatusFound, "/index")
		})
	r.GET("/logout",
		sessions.LoginRequired(),
		func(c *gin.Context) {
			// 注销登录
			sessions.Logout(c)
			c.JSON(http.StatusFound, "/logout")
		})
	r.Run("0.0.0.0:9090")
}

授权

考虑到不同的系统,权限实现有所区别(通常都使用角色来归类权限),这里做了一个简单的抽象

// 获取用户的所有权限
type UsePermissionGetter func(interface{}) (map[int]bool, error)

// 获取所有的权限
type AllPermisionsGetter func() (map[string]int, error)

权限通过string/int的map存储,在sessions.PermissionRequired("perm3")来作权限控制时,事先转换未权限的ID

在中间件处理中,调用另外一个接口获取当前用户的所有权限,并且作cache,当同一请求后续的操作中可以直接使用。

package main

import (
	"github.com/gin-gonic/gin"
	"github.com/qjw/session"
	"gopkg.in/redis.v5"
	"log"
	"net/http"
)

type User2 struct {
	Id   int
	Name string
}

func initStore2() sessions.Store {
	redisClient := redis.NewClient(&redis.Options{
		Addr:     "127.0.0.1:6379",
		Password: "",
		DB:       3,
	})
	if err := redisClient.Ping().Err(); err != nil {
		log.Fatal("failed to connect redis")
	}

	store, err := sessions.NewRediStore(redisClient, []byte("abcdefg"))
	if err != nil {
		log.Print(err)
	}
	return store
}

func main() {
	store := initStore2()
	r := gin.Default()
	r.Use(sessions.GinSessionMiddleware(store, sessions.AUTH_SESSION_NAME))
	r.Use(sessions.GinAuthMiddleware(&sessions.AuthOptions{
		User: &User2{},
	}))
	sessions.InitPermission(&sessions.PermissionOptions{
		UserPermissionGetter: func(user interface{}) (map[int]bool, error) {
			ruser := user.(*User2)
			if ruser.Name == "p1" {
				return map[int]bool{
					1: true,
				}, nil
			} else if ruser.Name == "p2" {
				return map[int]bool{
					1: true,
					2: true,
				}, nil
			} else {
				return map[int]bool{}, nil
			}
		},
		AllPermisionsGetter: func() (map[string]int, error) {
			return map[string]int{
				"perm1": 1,
				"perm2": 2,
				"perm3": 3,
			}, nil
		},
	})

	r.GET("/index",
		sessions.LoginRequired(),
		func(c *gin.Context) {
			// 获取登录用户
			user := sessions.LoggedUser(c).(*User2)
			c.JSON(http.StatusOK, gin.H{
				"message": user.Name,
			})
		})
	r.GET("/perm1",
		sessions.PermissionRequired("perm1"),
		func(c *gin.Context) {
			// 获取登录用户
			user := sessions.LoggedUser(c).(*User2)
			c.JSON(http.StatusOK, gin.H{
				"message": user.Name,
			})
		})
	r.GET("/perm2",
		sessions.PermissionRequired("perm2"),
		func(c *gin.Context) {
			// 获取登录用户
			user := sessions.LoggedUser(c).(*User2)
			c.JSON(http.StatusOK, gin.H{
				"message": user.Name,
			})
		})
	r.GET("/perm3",
		sessions.PermissionRequired("perm3"),
		func(c *gin.Context) {
			// 获取登录用户
			user := sessions.LoggedUser(c).(*User2)
			c.JSON(http.StatusOK, gin.H{
				"message": user.Name,
			})
		})
	r.GET("/login",
		func(c *gin.Context) {
			// 是否已经登录
			if sessions.IsAuthenticated(c) {
				c.Redirect(http.StatusFound, "/index")
				return
			}

			// 登录授权
			sessions.Login(c, &User2{
				Id:   1,
				Name: c.DefaultQuery("name", "p1"),
			})
			c.Redirect(http.StatusFound, "/index")
		})
	r.GET("/logout",
		sessions.LoginRequired(),
		func(c *gin.Context) {
			// 注销登录
			sessions.Logout(c)
			c.JSON(http.StatusFound, "/logout")
		})
	r.Run("0.0.0.0:9090")
}

todo

  1. 多权限的and/or支持
  2. 权限层级关系

Documentation

Index

Constants

View Source
const (
	AUTH_SESSION_NAME = "session"
	AUTH_SESSION_KEY  = "_user_"
)
View Source
const (
	DefaultRegistryKey = "registrys"
	FlashKey           = "_flash"
)
View Source
const (
	PERMISSION_SESSION_KEY = "_permission_"
)

Variables

This section is empty.

Functions

func AddFlash

func AddFlash(c *kelly.Context, msg string)

添加flash消息

func AuthMiddleware

func AuthMiddleware(options *AuthOptions) kelly.HandlerFunc

自动注入登录的user信息

func Flashes

func Flashes(c *kelly.Context) []interface{}

获取所有的flask,并且清空。

func InitFlash

func InitFlash(keyPairs []byte) bool

func InitPermission

func InitPermission(options *PermissionOptions) error

初始化

func IsAuthenticated

func IsAuthenticated(c *kelly.Context) bool

是否已经登录

func LoggedUser

func LoggedUser(c *kelly.Context) interface{}

当前登录的用户

func Login

func Login(c *kelly.Context, user interface{}) error

登录

func LoginRequired

func LoginRequired() kelly.HandlerFunc

必须要登录的中间件检查

func Logout

func Logout(c *kelly.Context) error

注销

func NewCookie

func NewCookie(name, value string, options *Options) *http.Cookie

NewCookie returns an http.Cookie with the options set. It also sets the Expires field calculated based on the MaxAge value, for Internet Explorer compatibility.

func PermissionRequired

func PermissionRequired(perm string) kelly.HandlerFunc

必须要登录的中间件检查

func Save

func Save(c *kelly.Context) error

Save saves all sessions used during the current request.

func SessionMiddleware

func SessionMiddleware(store Store, key string) kelly.HandlerFunc

Types

type AllPermisionsGetter

type AllPermisionsGetter func() (map[string]int, error)

获取所有的权限

type AuthOptions

type AuthOptions struct {
	ErrorFunc    kelly.HandlerFunc
	User         interface{}
	CastUserFunc CastUser
}

Options stores configurations for a CSRF middleware.

type CastUser

type CastUser func(interface{}) interface{}

type CookieStore

type CookieStore struct {
	Codecs  []securecookie.Codec
	Options *Options // default configuration
}

CookieStore stores sessions using secure cookies.

func NewCookieStore

func NewCookieStore(keyPairs ...[]byte) *CookieStore

NewCookieStore returns a new CookieStore.

Keys are defined in pairs to allow key rotation, but the common case is to set a single authentication key and optionally an encryption key.

The first key in a pair is used for authentication and the second for encryption. The encryption key can be set to nil or omitted in the last pair, but the authentication key is required in all pairs.

It is recommended to use an authentication key with 32 or 64 bytes. The encryption key, if set, must be either 16, 24, or 32 bytes to select AES-128, AES-192, or AES-256 modes.

Use the convenience function securecookie.GenerateRandomKey() to create strong keys.

func (*CookieStore) Delete

func (s *CookieStore) Delete(c *kelly.Context, name string) error

Save adds a single session to the response.

func (*CookieStore) Get

func (s *CookieStore) Get(c *kelly.Context, name string) (*SessionImp, error)

Get returns a session for the given name after adding it to the registry.

It returns a new session if the sessions doesn't exist. Access IsNew on the session to check if it is an existing session or a new one.

It returns a new session and an error if the session exists but could not be decoded.

func (*CookieStore) MaxAge

func (s *CookieStore) MaxAge(age int)

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 (*CookieStore) New

func (s *CookieStore) New(c *kelly.Context, name string) (*SessionImp, error)

New returns a session for the given name without adding it to the registry.

The difference between New() and Get() is that calling New() twice will decode the session data twice, while Get() registers and reuses the same decoded session after the first call.

func (*CookieStore) Save

func (s *CookieStore) Save(c *kelly.Context, session *SessionImp) error

Save adds a single session to the response.

type FilesystemStore

type FilesystemStore struct {
	Codecs  []securecookie.Codec
	Options *Options // default configuration
	// contains filtered or unexported fields
}

FilesystemStore stores sessions in the filesystem.

It also serves as a reference for custom stores.

This store is still experimental and not well tested. Feedback is welcome.

func NewFilesystemStore

func NewFilesystemStore(path string, keyPairs ...[]byte) *FilesystemStore

NewFilesystemStore returns a new FilesystemStore.

The path argument is the directory where sessions will be saved. If empty it will use os.TempDir().

See NewCookieStore() for a description of the other parameters.

func (*FilesystemStore) Delete

func (s *FilesystemStore) Delete(c *kelly.Context, name string) error

Save adds a single session to the response.

func (*FilesystemStore) Get

func (s *FilesystemStore) Get(c *kelly.Context, name string) (*SessionImp, error)

Get returns a session for the given name after adding it to the registry.

See CookieStore.Get().

func (*FilesystemStore) MaxAge

func (s *FilesystemStore) MaxAge(age int)

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 (*FilesystemStore) MaxLength

func (s *FilesystemStore) MaxLength(l int)

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 FilesystemStore is 4096.

func (*FilesystemStore) New

func (s *FilesystemStore) New(c *kelly.Context, name string) (*SessionImp, error)

New returns a session for the given name without adding it to the registry.

See CookieStore.New().

func (*FilesystemStore) Save

func (s *FilesystemStore) Save(c *kelly.Context, session *SessionImp) error

Save adds a single session to the response.

If the Options.MaxAge of the session is <= 0 then the session file will be deleted from the store path. With this process it enforces the properly session cookie handling so no need to trust in the cookie management in the web browser.

type GobSerializer

type GobSerializer struct{}

GobSerializer uses gob package to encode the session map

func (GobSerializer) Deserialize

func (s GobSerializer) Deserialize(d []byte, session *SessionImp) error

Deserialize back to map[interface{}]interface{}

func (GobSerializer) Serialize

func (s GobSerializer) Serialize(session *SessionImp) ([]byte, error)

Serialize using gob

type JSONSerializer

type JSONSerializer struct{}

JSONSerializer encode the session map to JSON.

func (JSONSerializer) Deserialize

func (s JSONSerializer) Deserialize(d []byte, session *SessionImp) error

Deserialize back to map[string]interface{}

func (JSONSerializer) Serialize

func (s JSONSerializer) Serialize(session *SessionImp) ([]byte, error)

Serialize to JSON. Will err if there are unmarshalable key values

type MultiError

type MultiError []error

MultiError stores multiple errors.

Borrowed from the App Engine SDK.

func (MultiError) Error

func (m MultiError) Error() string

type Options

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
}

Options stores configuration for a session or session store. Fields are a subset of http.Cookie fields.

type PermissionOptions

type PermissionOptions struct {
	ErrorFunc            kelly.HandlerFunc
	UserPermissionGetter UsePermissionGetter
	AllPermisionsGetter  AllPermisionsGetter
}

选项,对外

type RediStore

type RediStore struct {
	Pool          *redis.Client
	Codecs        []securecookie.Codec
	Options       *Options // default configuration
	DefaultMaxAge int      // default Redis TTL for a MaxAge == 0 session
	// contains filtered or unexported fields
}

RediStore stores sessions in a redis backend.

func NewRediStore

func NewRediStore(redis *redis.Client, keyPairs ...[]byte) (*RediStore, error)

NewRediStore returns a new RediStore. size: maximum number of idle connections.

func NewRediStoreWithPool

func NewRediStoreWithPool(redis *redis.Client, keyPairs ...[]byte) (*RediStore, error)

NewRediStoreWithPool instantiates a RediStore with a *redis.Pool passed in.

func (*RediStore) Delete

func (s *RediStore) Delete(c *kelly.Context, name string) error

Save adds a single session to the response.

func (*RediStore) Get

func (s *RediStore) Get(c *kelly.Context, name string) (*SessionImp, error)

Get returns a session for the given name after adding it to the registry.

See gorilla/sessions FilesystemStore.Get().

func (*RediStore) New

func (s *RediStore) New(c *kelly.Context, name string) (*SessionImp, error)

New returns a session for the given name without adding it to the registry.

See gorilla/sessions FilesystemStore.New().

func (*RediStore) Save

func (s *RediStore) Save(c *kelly.Context, session *SessionImp) error

Save adds a single session to the response.

func (*RediStore) SetKeyPrefix

func (s *RediStore) SetKeyPrefix(p string)

SetKeyPrefix set the prefix

func (*RediStore) SetMaxAge

func (s *RediStore) SetMaxAge(v int)

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 (*RediStore) SetMaxLength

func (s *RediStore) SetMaxLength(l int)

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 (*RediStore) SetSerializer

func (s *RediStore) SetSerializer(ss SessionSerializer)

SetSerializer sets the serializer

type Registry

type Registry struct {
	// contains filtered or unexported fields
}

Registry stores sessions used during a request.

func GetRegistry

func GetRegistry(c *kelly.Context) *Registry

func (*Registry) Get

func (s *Registry) Get(store Store, name string) (session *SessionImp, err error)

Get registers and returns a session for the given name and session store.

It returns a new session if there are no sessions registered for the name.

func (*Registry) Save

func (s *Registry) Save(w http.ResponseWriter) error

Save saves all sessions registered for the current request.

type 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
}

Wraps thinly gorilla-session methods. Session stores the values and optional configuration for a session.

type SessionImp

type SessionImp struct {
	// The ID of the session, generated by stores. It should not be used for
	// user data.
	ID string

	// Values contains the user-data for the session.
	Values  map[interface{}]interface{}
	Options *Options
	IsNew   bool
	// contains filtered or unexported fields
}

func NewSession

func NewSession(store Store, name string) *SessionImp

NewSession is called by session stores to create a new session instance.

func (*SessionImp) AddFlash

func (s *SessionImp) AddFlash(value interface{}, vars ...string)

func (*SessionImp) Clear

func (s *SessionImp) Clear()

func (*SessionImp) Delete

func (s *SessionImp) Delete(key interface{})

func (*SessionImp) Flashes

func (s *SessionImp) Flashes(vars ...string) []interface{}

func (*SessionImp) Get

func (s *SessionImp) Get(key interface{}) interface{}

func (*SessionImp) Save

func (s *SessionImp) Save() error

func (*SessionImp) Set

func (s *SessionImp) Set(key interface{}, val interface{})

func (*SessionImp) Written

func (s *SessionImp) Written() bool

type SessionSerializer

type SessionSerializer interface {
	Deserialize(d []byte, session *SessionImp) error
	Serialize(session *SessionImp) ([]byte, error)
}

SessionSerializer provides an interface hook for alternative serializers

type Store

type Store interface {
	// Get should return a cached session.
	Get(c *kelly.Context, name string) (*SessionImp, error)

	// New should create and return a new session.
	//
	// Note that New should never return a nil session, even in the case of
	// an error if using the Registry infrastructure to cache the session.
	New(c *kelly.Context, name string) (*SessionImp, error)

	// Save should persist session to the underlying store implementation.
	Save(c *kelly.Context, s *SessionImp) error

	// 删除cookie
	Delete(c *kelly.Context, name string) error
}

type UsePermissionGetter

type UsePermissionGetter func(interface{}) (map[int]bool, error)

获取用户的所有权限

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL