GWS升级到V2说明

GWS

Go’s web session library.


介 绍

大家新年好啊,这几天过节有空把之前写一个开源库,重构了一下,顺便写一下升级和使用说明,仓库地址: github.com/auula/gws

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")
})

上面都是基本的增删改查操作,如果你作为一名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的时候就已经考虑到了,所有都提供了。

以上示例代码目录:

  • ./example/store_example.go
  • ./example/ram_example.go
  • ./example/redis_example.go

PS: 如果你是微信公众号看到这篇文章,那估计你不能正常跳转,因为腾讯的平台就这个样子,如果对你有帮助你可以按一个star再走。

  • https://github.com/auula/gws

如果你发现了什么bug欢迎pr或者issues,我对代码质量要求比较高,gws代码是通过机器进行code review的,目前代码测试覆盖率和文档正在完善中,目前还在dev分支,可能目前大部分go get拉不到代码,过几天应该会同步到主分支上然后发布,如果你添加其他类型的存储实现,请确保你的代码有注释和测试覆盖代码,不然如果action显红并不给予合并,谢谢。

GWS升级到V2说明
code review
GWS升级到V2说明
测试覆盖率
GWS升级到V2说明
代码监控

小 结

设计一款好的api给别人用 比写普通crud要难得多,你要考虑到别人各种调,各种不按常理出牌 ,并且还要限制别人对一些标识符的访问权限,我这两天把之前写的session库重构了一下,要做一个能写出优雅代码工程师,而不是做一个糊拼烂凑的api调用操作员,在你写出代码那一刻你要为以后维护你代码那个人想一想,还要考虑这样设计api合不合理。


原文始发于微信公众号(TPaper):GWS升级到V2说明

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/23646.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!