Skip to content

auula/gws

Repository files navigation

GWS

Go's web session library.


Go Go Report Card Release License Go Reference codecov DeepSource


简体中文 | English


介 绍

GWS是一个Go语言实现的WEB会话库,支持本地会话存储,也支持Redis远程服务器分布式存储,并且为了可扩展存储实现,预留工厂,方便开发者自定义实现存储来保存会话数据。

安 装

开发者你只需要安装本库到你到项目里面,在你的项目里面执行下面命令即可安装:

go get -u github.com/auula/gws

使用示例

首先要声明一点,gws是支持多种存储介质保存session数据的,你可以自定义实现gws.Storage存储接口,来使用你自定义存储,接口代码如下:

// Storage global session data store interface.
// You can customize the storage medium by implementing this interface.
type Storage interface {
	// Read data from store
	Read(s *Session) (err error)
	// Write data to storage
	Write(s *Session) (err error)
	// Remove data from storage
	Remove(s *Session) (err error)
}

你只需要实现gws.Storage接口,就可以自定义存储会话数据,然后通过gws.StoreFactory(opt Options, store Storage)工厂注册自定义存储实现接口配置,例如下面我在演示代码里面一个例子:

package main

import (
	"fmt"
	"net/http"
	// 导入gws模块
	"github.com/auula/gws"
)

func init() {
	// 是否开启debug调试模式,如果开启则开发者可以在控制台看到会话链路日志
	// 好的开发者应该看日志去分析程序运行状态,而不是集成开发环境里面的debug功能
	gws.Debug(false)
	// 通过默认配置,并且注册自定义存储实现
	gws.StoreFactory(gws.NewOptions(), &FileStore{})
}

// 自定义的文件存储实现
type FileStore struct{}

func (fs FileStore) Read(s *gws.Session) (err error) {
	panic("implement me")
}

func (fs FileStore) Write(s *gws.Session) (err error) {
	panic("implement me")
}

func (fs FileStore) Remove(s *gws.Session) (err error) {
	panic("implement me")
}

func main() {
	// 测试自定义存储
	http.HandleFunc("/panic", func(writer http.ResponseWriter, request *http.Request) {
		// gws.GetSession 会返回本次请求的session
		session, _ := gws.GetSession(writer, request)
		// 通过session.Values 保存需要存储会话的数据
		session.Values["foo"] = "bar"
		// 通过Sync方法同步数据持久化,当然这里如果是默认内存存储可以不调用
		// 如果是远程服务器或者自定义存储一定要执行此方法同步数据到其他分布式端
		session.Sync()

		fmt.Fprintln(writer, "set value successful.")
	})

	_ = http.ListenAndServe(":8080", nil)

}

以上示例代码,展示如何自定义实现一个存储,具体示例代码请查看:./example/store_example.go


如果只是单机使用,或者是一个小体积Web Service应用,你可以使用默认的本地内存存储,会话存储会保存在本地服务器内存里面,这个缺点就是程序重启会话数据不能恢复。

如果想支持持久化你可以自定义,也可以使用gws默认提供的Redis方案去解决会话分布式存储,Redis在单点故障时,只要会话没有过期,应用节点恢复正常之后,数据依旧会同步到。

func init() {
	// 自定义配置选项参数,具体哪些参数可以查看go.dev上面的文档,或者看源代码吧
	// var opt gws.RAMOption
	// opt.Domain = "www.ibyte.me"
	// gws.Open(opt)

	// 你可以使用默认配置初始化,通过option function模式初始化
	
	// gws.Open(gws.NewOptions())
	// gws.Open(gws.NewOptions(gws.Domain(""), gws.CookieName("")))

	// 推荐直接默认配置
	gws.Open(gws.DefaultRAMOptions)

	// 这个是初始化Redis分布式存储的
	// gws.Open(gws.NewRDSOptions("127.0.0.1", 6379, "redis.nosql"))
}

下面的示例代码,我会演示如何通过gws管理你的会话数据:

// 为了演示数据变化,我定义的一个UserInfo结构体
type UserInfo struct {
	UserName string `json:"user_name,omitempty"`
	Email    string `json:"email,omitempty"`
	Age      uint8  `json:"age,omitempty"`
}

我配置了一个set路由,如何在会话里面存储一个user的值,存储值直接使用Values字段赋值,其实就是一个map[string]interface{}变体结构,注意这里的Values不是并行安全的,其实我在开发gws就考虑到了这个问题,并且想设计并发安全的api,但是考虑到api太多了也不好,写Go要保持大道至简,并不是像Java那样要通过getset各种抽象,那样只会让你的代码库变得庞大,杂乱无章。

所以在文档我明确说明了如果是并发操作Values并且自定义加锁!!!示例代码也会在后面添加:

http.HandleFunc("/set", func(writer http.ResponseWriter, request *http.Request) {

	session, _ := gws.GetSession(writer, request)
	session.Values["user"] = &UserInfo{
		UserName: "Leon Ding",
		Email:    "ding@ibyte.me",
		Age:      21,
	}

	// ram模式可以不用执行,因为是内存指针引用
	session.Sync()

	fmt.Fprintln(writer, "set value successful.")
})

如果要从会话里面读取数据,可以看示例代码:

http.HandleFunc("/get", func(writer http.ResponseWriter, request *http.Request) {
	session, _ := gws.GetSession(writer, request)

	// 读取数据和检测map一样的操作,你如果能确保这个一定有值,你可以省去这个if操作
	// 直接取值也行
	if bytes, ok := session.Values["user"]; ok {
		jsonstr, _ := json.Marshal(bytes)
		fmt.Fprintln(writer, string(jsonstr))
		return
	}

	fmt.Fprintln(writer, "no data")
})

删除操作及其简单,如果你是老司机开发者,我相信你已经不需要看示例代码了,如下:

http.HandleFunc("/del", func(writer http.ResponseWriter, request *http.Request) {
	session, _ := gws.GetSession(writer, request)
	delete(session.Values, "user")
	// 一定同步,如果是自定义存储或者是Redis分布式存储的话
	session.Sync()
	fmt.Fprintln(writer, "successful")
})

如果要清理这个sessionValues数据,可以使用gws.Malloc(v *gws.Values)函数:

http.HandleFunc("/clean", func(rw http.ResponseWriter, request *http.Request) {
	session, _ := gws.GetSession(rw, request)
	// clean session data
	gws.Malloc(&session.Values)
	// sync session modify
	session.Sync()
	fmt.Fprintf(rw, "clean session data successful.")
})

如果废弃掉这个session则调用gws.Invalidate(s *Session) error函数:

http.HandleFunc("/invalidate", func(rw http.ResponseWriter, request *http.Request) {
	session, _ := gws.GetSession(rw, request)
	gws.Invalidate(session)
	fmt.Fprintf(rw, "set session invalidate successful.")
})

上面都是基本的增删改查操作,如果你作为一名API调用工程师或者是API操作员,那你看到这估计就差不多了,可以完成你日常的开发需求了,你也不需要去了解内部实现,如果要了解内部实现,我后面有空会去讲内部实现。

并行安全

由于我在设计API的时候,没有打算去写一个get、set、del,然后在里面提供一个内部锁去保证并行安全,所有调用者必须在有数据竞争的情况下自行加锁,或者你go写的溜,你可以自定义去包装并行安全的,下面这段代码我演示了如何并行安全的操作:

http.HandleFunc("/race", func(writer http.ResponseWriter, request *http.Request) {
	session, _ := gws.GetSession(writer, request)

	session.Values["count"] = 0
	var (
		wg  sync.WaitGroup
		// 如果你是并发操作请加锁
		mux sync.Mutex
	)
	size := 10000
	wg.Add(size)
	for i := 0; i < size/2; i++ {
		go func() {
			time.Sleep(5 * time.Second)
			mux.Lock()
			if v, ok := session.Values["count"].(int); ok {
				session.Values["count"] = v + 1
			}
			wg.Done()
			mux.Unlock()
		}()
		go func() {
			time.Sleep(5 * time.Second)
			mux.Lock()
			if v, ok := session.Values["count"].(int); ok {
				session.Values["count"] = v + 1
			}
			wg.Done()
			mux.Unlock()
		}()
	}
	wg.Wait()
	fmt.Fprintln(writer, session.Values["count"].(int))
})

在数据竞争状态下,其他的调用者,可以正常取值,但是你要保证你自定义的锁的控制范围,你要用什么类型的锁,例如读写锁还是互斥锁,这个要看你对go的了解程度了,或者你很强,你可以通过channel解决数据竞争,我在设计API的时候,我是保持着尽可能少量的去影响或者限制调用者一些操作体验的,上面和下面的示例是在race在请求的情况下,result不会阻塞并且还能取值的演示:

http.HandleFunc("/result", func(writer http.ResponseWriter, request *http.Request) {
	session, _ := gws.GetSession(writer, request)
	fmt.Fprintln(writer, session.Values["count"].(int))
})

会话劫持

会话固定攻击,这个过程,正常用户在通过浏览器访问我们编写的网站,但是这个时候有个hack通过arp欺骗,把路由器的流量劫持到他的电脑上,然后黑客通过一些特殊的软件抓包你的网络请求流量信息,在这个过程中如果你sessionid如果存放在cookie中,很有可能被黑客提取处理,如果你这个时候登录了网站,这是黑客就拿到你的登录凭证,然后在登录进行重放也就是使用你的sessionid,从而达到访问你账户相关的数据目的。

为此我在gws里面添加一个gws.Migrate(write http.ResponseWriter, old *Session) (*Session, error)内置函数,使用示例:

http.HandleFunc("/migrate", func(writer http.ResponseWriter, request *http.Request) {
	var (
		session *gws.Session
		err     error
	)

	session, _ = gws.GetSession(writer, request)
	log.Printf("old session %p \n", session)

	// 迁移会话数据,并且刷新客户端会话,丢弃掉老的session
	if session, err = gws.Migrate(writer, session); err != nil {
		fmt.Fprintln(writer, err.Error())
		return
	}

	log.Printf("old session %p \n", session)
	jsonstr, _ := json.Marshal(session.Values["user"])
	fmt.Fprintln(writer, string(jsonstr))
})

gws.Migrate会帮助你迁移会话数据,你也可以配合https协议使用,当然该有的API我在设计gws的时候就已经考虑到了,所有都提供了。

以上示例代码目录:

如果你发现了什么bug欢迎pr或者issues~如果对你有帮助你可以按一个star再走呗,https://github.com/auula/gws