{"id":22152272,"url":"https://github.com/auula/gws","last_synced_at":"2025-07-26T05:32:14.429Z","repository":{"id":43221408,"uuid":"282202467","full_name":"auula/gws","owner":"auula","description":"Go's web session library.","archived":false,"fork":false,"pushed_at":"2022-03-12T14:36:48.000Z","size":463,"stargazers_count":85,"open_issues_count":0,"forks_count":6,"subscribers_count":0,"default_branch":"master","last_synced_at":"2024-06-19T00:16:05.479Z","etag":null,"topics":["go","golang","redis","redis-cache","redis-server","redis-session","session","session-cookie","session-management","session-store","sessions","sessionstorage","web"],"latest_commit_sha":null,"homepage":"","language":"Go","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/auula.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2020-07-24T11:29:32.000Z","updated_at":"2024-05-12T17:33:40.000Z","dependencies_parsed_at":"2022-08-28T10:22:02.010Z","dependency_job_id":null,"html_url":"https://github.com/auula/gws","commit_stats":null,"previous_names":["higker/go-session","higker/sessionx"],"tags_count":18,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/auula%2Fgws","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/auula%2Fgws/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/auula%2Fgws/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/auula%2Fgws/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/auula","download_url":"https://codeload.github.com/auula/gws/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":227652567,"owners_count":17799235,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["go","golang","redis","redis-cache","redis-server","redis-session","session","session-cookie","session-management","session-store","sessions","sessionstorage","web"],"created_at":"2024-12-02T00:49:35.035Z","updated_at":"2024-12-02T00:49:35.684Z","avatar_url":"https://github.com/auula.png","language":"Go","readme":"# GWS\n\n**Go's web session library.**\n\n---\n[![Go](https://github.com/auula/gws/actions/workflows/go-test.yml/badge.svg?event=push)](https://github.com/auula/gws/actions/workflows/go-test.yml)\n[![Go Report Card](https://goreportcard.com/badge/github.com/auula/gws)](https://goreportcard.com/report/github.com/auula/gws)\n[![Release](https://img.shields.io/github/v/release/auula/gws.svg?style=flat-square)](https://github.com/auula/gws)\n[![License](https://img.shields.io/badge/license-MIT-db5149.svg)](https://github.com/auula/gws/blob/master/LICENSE)\n[![Go Reference](https://pkg.go.dev/badge/github.com/auula/gws.svg)](https://pkg.go.dev/github.com/auula/gws)\n[![codecov](https://codecov.io/gh/auula/gws/branch/dev/graph/badge.svg?token=btbed5BUUZ)](https://codecov.io/gh/auula/gws)\n[![DeepSource](https://deepsource.io/gh/auula/gws.svg/?label=active+issues\u0026token=c0sepC4oiYxaeLOxgJFLfSWP)](https://deepsource.io/gh/auula/gws/?ref=repository-badge)\n\n---\n\n\n[简体中文](./README.md) | [English](./README_EN.md)\n\n---\n### 介 绍\n\n`GWS`是一个`Go`语言实现的`WEB`会话库，支持本地会话存储，也支持`Redis`远程服务器分布式存储，并且为了可扩展存储实现，预留工厂，方便开发者自定义实现存储来保存会话数据。\n\n### 安 装\n开发者你只需要安装本库到你到项目里面，在你的项目里面执行下面命令即可安装：\n```shell\ngo get -u github.com/auula/gws\n```\n### 使用示例\n\n首先要声明一点，`gws`是支持多种存储介质保存`session`数据的，你可以自定义实现[`gws.Storage`](./storage.go)存储接口，来使用你自定义存储，接口代码如下:\n\n```go\n// Storage global session data store interface.\n// You can customize the storage medium by implementing this interface.\ntype Storage interface {\n\t// Read data from store\n\tRead(s *Session) (err error)\n\t// Write data to storage\n\tWrite(s *Session) (err error)\n\t// Remove data from storage\n\tRemove(s *Session) (err error)\n}\n```\n你只需要实现[`gws.Storage`](./storage.go)接口，就可以自定义存储会话数据，然后通过`gws.StoreFactory(opt Options, store Storage)`工厂注册自定义存储实现接口配置，例如下面我在演示代码里面一个例子:\n\n```go\npackage main\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t// 导入gws模块\n\t\"github.com/auula/gws\"\n)\n\nfunc init() {\n\t// 是否开启debug调试模式，如果开启则开发者可以在控制台看到会话链路日志\n\t// 好的开发者应该看日志去分析程序运行状态，而不是集成开发环境里面的debug功能\n\tgws.Debug(false)\n\t// 通过默认配置，并且注册自定义存储实现\n\tgws.StoreFactory(gws.NewOptions(), \u0026FileStore{})\n}\n\n// 自定义的文件存储实现\ntype FileStore struct{}\n\nfunc (fs FileStore) Read(s *gws.Session) (err error) {\n\tpanic(\"implement me\")\n}\n\nfunc (fs FileStore) Write(s *gws.Session) (err error) {\n\tpanic(\"implement me\")\n}\n\nfunc (fs FileStore) Remove(s *gws.Session) (err error) {\n\tpanic(\"implement me\")\n}\n\nfunc main() {\n\t// 测试自定义存储\n\thttp.HandleFunc(\"/panic\", func(writer http.ResponseWriter, request *http.Request) {\n\t\t// gws.GetSession 会返回本次请求的session\n\t\tsession, _ := gws.GetSession(writer, request)\n\t\t// 通过session.Values 保存需要存储会话的数据\n\t\tsession.Values[\"foo\"] = \"bar\"\n\t\t// 通过Sync方法同步数据持久化，当然这里如果是默认内存存储可以不调用\n\t\t// 如果是远程服务器或者自定义存储一定要执行此方法同步数据到其他分布式端\n\t\tsession.Sync()\n\n\t\tfmt.Fprintln(writer, \"set value successful.\")\n\t})\n\n\t_ = http.ListenAndServe(\":8080\", nil)\n\n}\n```\n以上示例代码，展示如何自定义实现一个存储，具体示例代码请查看：[./example/store_example.go](./example/store_example.go)\n\n---\n如果只是单机使用，或者是一个小体积`Web Service`应用，你可以使用默认的本地内存存储，会话存储会保存在本地服务器内存里面，这个缺点就是程序重启会话数据不能恢复。\n\n如果想支持持久化你可以自定义，也可以使用`gws`默认提供的`Redis`方案去解决会话分布式存储，`Redis`在单点故障时，只要会话没有过期，应用节点恢复正常之后，数据依旧会同步到。\n\n```go\nfunc init() {\n\t// 自定义配置选项参数，具体哪些参数可以查看go.dev上面的文档，或者看源代码吧\n\t// var opt gws.RAMOption\n\t// opt.Domain = \"www.ibyte.me\"\n\t// gws.Open(opt)\n\n\t// 你可以使用默认配置初始化，通过option function模式初始化\n\t\n\t// gws.Open(gws.NewOptions())\n\t// gws.Open(gws.NewOptions(gws.Domain(\"\"), gws.CookieName(\"\")))\n\n\t// 推荐直接默认配置\n\tgws.Open(gws.DefaultRAMOptions)\n\n\t// 这个是初始化Redis分布式存储的\n\t// gws.Open(gws.NewRDSOptions(\"127.0.0.1\", 6379, \"redis.nosql\"))\n}\n```\n\n下面的示例代码，我会演示如何通过`gws`管理你的会话数据：\n\n```go\n// 为了演示数据变化，我定义的一个UserInfo结构体\ntype UserInfo struct {\n\tUserName string `json:\"user_name,omitempty\"`\n\tEmail    string `json:\"email,omitempty\"`\n\tAge      uint8  `json:\"age,omitempty\"`\n}\n```\n我配置了一个`set`路由，如何在会话里面存储一个`user`的值，存储值直接使用`Values`字段赋值，其实就是一个`map[string]interface{}`变体结构，注意这里的`Values`不是并行安全的，其实我在开发`gws`就考虑到了这个问题，并且想设计并发安全的`api`，但是考虑到`api`太多了也不好，写`Go`要保持大道至简，并不是像`Java`那样要通过`get`和`set`各种抽象，那样只会让你的代码库变得庞大，杂乱无章。\n\n所以在文档我明确说明了如果是并发操作`Values`并且自定义加锁！！！示例代码也会在后面添加：\n```go\nhttp.HandleFunc(\"/set\", func(writer http.ResponseWriter, request *http.Request) {\n\n\tsession, _ := gws.GetSession(writer, request)\n\tsession.Values[\"user\"] = \u0026UserInfo{\n\t\tUserName: \"Leon Ding\",\n\t\tEmail:    \"ding@ibyte.me\",\n\t\tAge:      21,\n\t}\n\n\t// ram模式可以不用执行，因为是内存指针引用\n\tsession.Sync()\n\n\tfmt.Fprintln(writer, \"set value successful.\")\n})\n```\n如果要从会话里面读取数据，可以看示例代码：\n\n```go\nhttp.HandleFunc(\"/get\", func(writer http.ResponseWriter, request *http.Request) {\n\tsession, _ := gws.GetSession(writer, request)\n\n\t// 读取数据和检测map一样的操作，你如果能确保这个一定有值，你可以省去这个if操作\n\t// 直接取值也行\n\tif bytes, ok := session.Values[\"user\"]; ok {\n\t\tjsonstr, _ := json.Marshal(bytes)\n\t\tfmt.Fprintln(writer, string(jsonstr))\n\t\treturn\n\t}\n\n\tfmt.Fprintln(writer, \"no data\")\n})\n```\n删除操作及其简单，如果你是老司机开发者，我相信你已经不需要看示例代码了，如下：\n\n```go\nhttp.HandleFunc(\"/del\", func(writer http.ResponseWriter, request *http.Request) {\n\tsession, _ := gws.GetSession(writer, request)\n\tdelete(session.Values, \"user\")\n\t// 一定同步，如果是自定义存储或者是Redis分布式存储的话\n\tsession.Sync()\n\tfmt.Fprintln(writer, \"successful\")\n})\n```\n\n如果要清理这个`session`的`Values`数据，可以使用`gws.Malloc(v *gws.Values)`函数：\n\n```go\nhttp.HandleFunc(\"/clean\", func(rw http.ResponseWriter, request *http.Request) {\n\tsession, _ := gws.GetSession(rw, request)\n\t// clean session data\n\tgws.Malloc(\u0026session.Values)\n\t// sync session modify\n\tsession.Sync()\n\tfmt.Fprintf(rw, \"clean session data successful.\")\n})\n```\n\n如果废弃掉这个`session`则调用`gws.Invalidate(s *Session) error`函数：\n\n```go\nhttp.HandleFunc(\"/invalidate\", func(rw http.ResponseWriter, request *http.Request) {\n\tsession, _ := gws.GetSession(rw, request)\n\tgws.Invalidate(session)\n\tfmt.Fprintf(rw, \"set session invalidate successful.\")\n})\n```\n上面都是基本的增删改查操作，如果你作为一名`API`调用工程师或者是`API`操作员，那你看到这估计就差不多了，可以完成你日常的开发需求了，你也不需要去了解内部实现，如果要了解内部实现，我后面有空会去讲内部实现。\n\n\n## 并行安全\n由于我在设计`API`的时候，没有打算去写一个`get、set、del`，然后在里面提供一个内部锁去保证并行安全，所有调用者必须在有数据竞争的情况下自行加锁，或者你`go`写的溜，你可以自定义去包装并行安全的，下面这段代码我演示了如何并行安全的操作：\n```go\nhttp.HandleFunc(\"/race\", func(writer http.ResponseWriter, request *http.Request) {\n\tsession, _ := gws.GetSession(writer, request)\n\n\tsession.Values[\"count\"] = 0\n\tvar (\n\t\twg  sync.WaitGroup\n\t\t// 如果你是并发操作请加锁\n\t\tmux sync.Mutex\n\t)\n\tsize := 10000\n\twg.Add(size)\n\tfor i := 0; i \u003c size/2; i++ {\n\t\tgo func() {\n\t\t\ttime.Sleep(5 * time.Second)\n\t\t\tmux.Lock()\n\t\t\tif v, ok := session.Values[\"count\"].(int); ok {\n\t\t\t\tsession.Values[\"count\"] = v + 1\n\t\t\t}\n\t\t\twg.Done()\n\t\t\tmux.Unlock()\n\t\t}()\n\t\tgo func() {\n\t\t\ttime.Sleep(5 * time.Second)\n\t\t\tmux.Lock()\n\t\t\tif v, ok := session.Values[\"count\"].(int); ok {\n\t\t\t\tsession.Values[\"count\"] = v + 1\n\t\t\t}\n\t\t\twg.Done()\n\t\t\tmux.Unlock()\n\t\t}()\n\t}\n\twg.Wait()\n\tfmt.Fprintln(writer, session.Values[\"count\"].(int))\n})\n```\n在数据竞争状态下，其他的调用者，可以正常取值，但是你要保证你自定义的锁的控制范围，你要用什么类型的锁，例如读写锁还是互斥锁，这个要看你对`go`的了解程度了，或者你很强，你可以通过`channel`解决数据竞争，我在设计`API`的时候，我是保持着尽可能少量的去影响或者限制调用者一些操作体验的，上面和下面的示例是在`race`在请求的情况下，`result`不会阻塞并且还能取值的演示：\n\n```go\nhttp.HandleFunc(\"/result\", func(writer http.ResponseWriter, request *http.Request) {\n\tsession, _ := gws.GetSession(writer, request)\n\tfmt.Fprintln(writer, session.Values[\"count\"].(int))\n})\n```\n\n\n## 会话劫持\n\n会话固定攻击，这个过程，正常用户在通过浏览器访问我们编写的网站，但是这个时候有个`hack`通过`arp`欺骗，把路由器的流量劫持到他的电脑上，然后黑客通过一些特殊的软件抓包你的网络请求流量信息，在这个过程中如果你`sessionid`如果存放在`cookie`中，很有可能被黑客提取处理，如果你这个时候登录了网站，这是黑客就拿到你的登录凭证，然后在登录进行重放也就是使用你的`sessionid`，从而达到访问你账户相关的数据目的。\n\n为此我在`gws`里面添加一个`gws.Migrate(write http.ResponseWriter, old *Session) (*Session, error)`内置函数，使用示例：\n\n```go\nhttp.HandleFunc(\"/migrate\", func(writer http.ResponseWriter, request *http.Request) {\n\tvar (\n\t\tsession *gws.Session\n\t\terr     error\n\t)\n\n\tsession, _ = gws.GetSession(writer, request)\n\tlog.Printf(\"old session %p \\n\", session)\n\n\t// 迁移会话数据，并且刷新客户端会话，丢弃掉老的session\n\tif session, err = gws.Migrate(writer, session); err != nil {\n\t\tfmt.Fprintln(writer, err.Error())\n\t\treturn\n\t}\n\n\tlog.Printf(\"old session %p \\n\", session)\n\tjsonstr, _ := json.Marshal(session.Values[\"user\"])\n\tfmt.Fprintln(writer, string(jsonstr))\n})\n```\n`gws.Migrate`会帮助你迁移会话数据，你也可以配合`https`协议使用，当然该有的`API`我在设计`gws`的时候就已经考虑到了，所有都提供了。\n\n**以上示例代码目录：**\n\n- [./example/store_example.go](./example/store_example.go)\n- [./example/ram_example.go](./example/ram_example.go)\n- [./example/redis_example.go](./example/redis_example.go)\n\n如果你发现了什么`bug`欢迎`pr`或者`issues`~如果对你有帮助你可以按一个`star`再走呗，[https://github.com/auula/gws](https://github.com/auula/gws)\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fauula%2Fgws","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fauula%2Fgws","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fauula%2Fgws/lists"}