zoukankan      html  css  js  c++  java
  • session管理机制设计与实现

    原生Go语言没有实现session管理机制,所以如果使用原生Go语言进行web编程,我们需要自己进行session管理机制的设计与实现,本文将就此进行详细介绍,并实现一个简单的session管理机制。
    session信息可以使用内存、文件或数据库等方式进行存储,考虑到对不同存储方式的兼容,我们设计的session管理机制应该能很方便的在不同存储方式之间进行切换。所以,session管理机制可以分为两部分内容:session管理器和session存储器,session管理器主要负责多种存储方式的共同操作部分,例如,cookie的读取与设置、session ID的生成,以及一些共同需要的参数设置等等。session管理器结构可设置如下:

    //session管理器
    type SessionManager struct {
    	cookieName    string          //cookie名称
    	cookieExpire  int             //cookie有效期时间(单位:分钟,0表示会话cookie)
    	sessionExpire int64           //session有效期时间(单位:分钟)
    	gcDuration    int             //垃圾回收机制运行间隔时间(单位:分钟)
    	provider      SessionProvider //session存储器
    }
    

    其创建方法为:

    //创建session管理器
    func NewManager(cookieName string, cookieExpire int, sessionExpire int64, gcDuration int, provider SessionProvider) *SessionManager {
    	return &SessionManager{
    		cookieName:    cookieName,
    		cookieExpire:  cookieExpire,
    		sessionExpire: sessionExpire,
    		gcDuration:    gcDuration,
    		provider:      provider,
    	}
    }
    

    而session存储器则对应具体的存储方式,只需负责根据session ID对session数据进行读写操作,这部分根据存储方式不同而不同,但方法签名是一致的,可以定义其接口类型为:

    //session存储器
    type SessionProvider interface {
    	create(sessionId string, data map[string]interface{}) error //创建session
    	get(sessionId, key string) (string, error)                  //读取session键值
    	getAll(sessionId string) (map[string]string, error)         //读取session所有键值对
    	set(sessionId, key string, value interface{}) error         //设置session键值
    	destroy(sessionId string) error                             //销毁session
    	gc(expire int64) error                                      //垃圾回收:删除过期session
    }
    

    接下来定义session管理器生成session ID的方法,可根据请求头信息生成,这里只是举一个例子:

    //生成session ID
    func (sm *SessionManager) createSessionId(req *http.Request) string {
    	addr := req.RemoteAddr
    	userAgent := req.Header.Get("User-Agent")
    	rand.Seed(time.Now().UnixNano())
    	n := rand.Intn(10000)
    	str := addr + "_" + userAgent + "_" + strconv.Itoa(n)
    	h := md5.New()
    	h.Write([]byte(str))
    	cipherStr := h.Sum(nil)
    	return hex.EncodeToString(cipherStr)
    }
    

    session管理器的各个方法都需要读cookie,获取session ID:

    //读cookie,获取session ID
    func (sm *SessionManager) getSessionId(req *http.Request) (string, error) {
    	c, err := req.Cookie(sm.cookieName)
    	if err != nil {
    		return "", errors.New("Reading cookie failed: " + err.Error())
    	}
    	if len(c.Value) == 0 { //尚未设置cookie
    		return "", errors.New("Cookie does not exists: " + sm.cookieName)
    	}
    	return c.Value, nil
    }
    

    最后就是session管理器创建session、读取session、写入session、销毁session、session GC等方法的定义,这些方法比较简单,只是调用session存储器对应的方法即可:

    //创建session
    func (sm *SessionManager) Create(writer *http.ResponseWriter, req *http.Request, data map[string]interface{}) error {
    	sessionId, _ := sm.getSessionId(req)
    	if len(sessionId) > 0 {
    		data, _ := sm.provider.getAll(sessionId)
    		if data != nil { //已有session,无需创建
    			return nil
    		}
    	}
    	sessionId = sm.createSessionId(req)
    	if len(sessionId) == 0 {
    		return errors.New("Length of sessionId is 0")
    	}
    	err := sm.provider.create(sessionId, data)
    	if err != nil {
    		return err
    	}
    	if sm.cookieExpire == 0 { //会话cookie
    		http.SetCookie(*writer, &http.Cookie{
    			Name:     sm.cookieName,
    			Value:    sessionId,
    			Path:     "/", //一定要设置为根目录,才能在所有页面生效
    			HttpOnly: true,
    		})
    	} else { //持久cookie
    		expire, _ := time.ParseDuration(strconv.Itoa(sm.cookieExpire) + "m")
    		http.SetCookie(*writer, &http.Cookie{
    			Name:     sm.cookieName,
    			Value:    sessionId,
    			Path:     "/", //一定要设置为根目录,才能在所有页面生效
    			Expires:  time.Now().Add(expire),
    			HttpOnly: true,
    		})
    	}
    	return nil
    }
    
    //获取session键值
    func (sm *SessionManager) Get(writer *http.ResponseWriter, req *http.Request, key string) (string, error) {
    	sessionId, _ := sm.getSessionId(req)
    	if len(sessionId) == 0 {
    		return "", errors.New("Length of sessionId is 0")
    	}
    	return sm.provider.get(sessionId, key)
    }
    
    //读取session所有键值对
    func (sm *SessionManager) GetAll(writer *http.ResponseWriter, req *http.Request) (map[string]string, error) {
    	sessionId, _ := sm.getSessionId(req)
    	if len(sessionId) == 0 {
    		return nil, errors.New("Length of sessionId is 0")
    	}
    	return sm.provider.getAll(sessionId)
    }
    
    //设置session键值
    func (sm *SessionManager) Set(writer *http.ResponseWriter, req *http.Request, key string, value interface{}) error {
    	sessionId, _ := sm.getSessionId(req)
    	if len(sessionId) == 0 {
    		return errors.New("Length of sessionId is 0")
    	}
    	return sm.provider.set(sessionId, key, value)
    }
    
    //销毁session
    func (sm *SessionManager) Destroy(req *http.Request) error {
    	sessionId, _ := sm.getSessionId(req)
    	if len(sessionId) == 0 {
    		return errors.New("Length of sessionId is 0")
    	}
    	return sm.provider.destroy(sessionId)
    }
    
    //垃圾回收:删除过期session
    func (sm *SessionManager) Gc() error {
    	err := sm.provider.gc(sm.sessionExpire)
    	duration, _ := time.ParseDuration(strconv.Itoa(sm.gcDuration) + "m")
    	time.AfterFunc(duration, func() { sm.Gc() }) //设置下次运行时间
    	return err
    }
    

    至此,我们已经实现session管理器!

    接下来,不管使用什么方式存储session信息,只要实现SessionProvider接口,关心session数据的读写操作即可。
    这里,我们实现一个文件session存储器,除了session文件保存路径,为了并发安全,每个session文件还需要对应一个读写锁,所以其结构可设计为:

    //文件session存储器
    type FileProvider struct {
    	savePath string                   //session文件保存路径
    	muxMap   map[string]*sync.RWMutex //session文件锁
    }
    

    对应的创建方法:

    //创建文件session存储器对象
    func NewFileProvider(savePath string) *FileProvider {
    	return &FileProvider{
    		savePath: savePath,
    		muxMap:   make(map[string]*sync.RWMutex),
    	}
    }
    

    所有方法都需要根据session ID得到文件路径,可定义共用方法:

    //返回session文件名称
    func (fp FileProvider) filename(sessionId string) string {
    	return fp.savePath + "/" + sessionId
    }
    

    写入session文件时,数据只能是字符串,而存入session的却不一定是字符串,所以需要一个将其他数据类型转换为字符串的共用方法:

    //将数据类型转换为字符串
    func (fp FileProvider) toString(value interface{}) (string, error) {
    	var str string
    	vType := reflect.TypeOf(value)
    	switch vType.Name() {
    	case "int":
    		i, _ := value.(int)
    		str = strconv.Itoa(i)
    	case "string":
    		str, _ = value.(string)
    	case "int64":
    		i, _ := value.(int64)
    		str = strconv.FormatInt(i, 10)
    	default:
    		return "", errors.New("Unsupported type: " + vType.Name())
    	}
    	return str, nil
    }
    

    文件session存储器的create、get、getAll、set等四个方法,本质上都是对session文件进行读写操作,可以将读和写抽取出来成为两个共用方法:

    //创建/重写session文件
    func (fp FileProvider) write(sessionId string, data map[string]string, newFile bool) error {
    	_, exist := fp.muxMap[sessionId]
    	if !exist { //内存中没有锁,先建锁
    		fp.muxMap[sessionId] = new(sync.RWMutex)
    	}
    	fp.muxMap[sessionId].Lock()
    	defer func() {
    		fp.muxMap[sessionId].Unlock()
    	}()
    	fname := fp.filename(sessionId)
    	_, err := os.Stat(fname)
    	var f *os.File
    	if newFile {
    		if err == nil { //若session文件存在,则先删除
    			os.Remove(fname)
    		}
    		f, err = os.Create(fname)
    		if err != nil {
    			return errors.New("Creating session file failed: " + err.Error())
    		}
    	} else {
    		if err != nil { //session文件不存在
    			return errors.New("Session file does not exists: " + fname)
    		}
    		f, err = os.OpenFile(fname, os.O_RDWR|os.O_TRUNC, 0644)
    		if err != nil {
    			return errors.New("Opening session file failed: " + err.Error())
    		}
    	}
    	defer func() {
    		os.Chtimes(fname, time.Now(), time.Now()) //更新文件最后访问时间
    		f.Close()
    	}()
    	for key, value := range data {
    		_, err = fmt.Fprintln(f, key+":"+value)
    		if err != nil {
    			return errors.New("Setting session key value failed: " + err.Error())
    		}
    	}
    	return nil
    }
    
    //读取session文件
    func (fp FileProvider) read(sessionId string) (map[string]string, error) {
    	fname := fp.filename(sessionId)
    	_, err := os.Stat(fname)
    	if err != nil { //session文件不存在
    		return nil, errors.New("Session file does not exists: " + fname)
    	}
    	_, exist := fp.muxMap[sessionId]
    	if !exist { //内存中没有锁,先建锁
    		fp.muxMap[sessionId] = new(sync.RWMutex)
    	}
    	fp.muxMap[sessionId].Lock()
    	defer func() {
    		fp.muxMap[sessionId].Unlock()
    	}()
    	f, err := os.Open(fname)
    	if err != nil {
    		return nil, errors.New("Opening session file failed: " + err.Error())
    	}
    	defer func() {
    		os.Chtimes(fname, time.Now(), time.Now()) //更新文件最后访问时间
    		f.Close()
    	}()
    	data := make(map[string]string)
    	scaner := bufio.NewScanner(f)
    	for scaner.Scan() {
    		kv := strings.Split(scaner.Text(), ":")
    		if len(kv) != 2 {
    			continue
    		}
    		data[kv[0]] = kv[1]
    	}
    	if len(data) == 0 {
    		return nil, errors.New("No data in session file")
    	}
    	return data, nil
    }
    

     最后,实现SessionProvider接口的6个方法:

    //创建session
    func (fp FileProvider) create(sessionId string, data map[string]interface{}) error {
    	strData := make(map[string]string)
    	for key, value := range data {
    		strValue, err := fp.toString(value)
    		if err != nil {
    			return err
    		}
    		strData[key] = strValue
    	}
    	return fp.write(sessionId, strData, true)
    }
    
    //读取session键值
    func (fp FileProvider) get(sessionId, key string) (string, error) {
    	data, err := fp.read(sessionId)
    	if err != nil {
    		return "", err
    	}
    	value, ok := data[key]
    	if !ok {
    		return "", errors.New("Session key does not exists: " + key)
    	}
    	return value, nil
    }
    
    //读取session所有键值对
    func (fp FileProvider) getAll(sessionId string) (map[string]string, error) {
    	return fp.read(sessionId)
    }
    
    //设置session键值
    func (fp FileProvider) set(sessionId, key string, value interface{}) error {
    	data, err := fp.read(sessionId)
    	if data == nil {
    		return err
    	}
    	str, err := fp.toString(value)
    	if err != nil {
    		return err
    	}
    	data[key] = str
    	return fp.write(sessionId, data, false)
    }
    
    //销毁session:删除session文件
    func (fp FileProvider) destroy(sessionId string) error {
    	fname := fp.filename(sessionId)
    	_, err := os.Stat(fname)
    	if err != nil { //session文件不存在
    		return errors.New("Session file does not exists: " + fname)
    	}
    	_, exist := fp.muxMap[sessionId]
    	if !exist { //内存中没有锁,先建锁
    		fp.muxMap[sessionId] = new(sync.RWMutex)
    	}
    	fp.muxMap[sessionId].Lock()
    	err = os.Remove(fname)
    	fp.muxMap[sessionId].Unlock()
    	if err != nil {
    		return errors.New("Removing session file failed: " + err.Error())
    	}
    	delete(fp.muxMap, sessionId)
    	return nil
    }
    
    //垃圾回收:删除过期session文件
    func (fp FileProvider) gc(expire int64) error {
    	now := time.Now().Unix()
    	for sessionId, mux := range fp.muxMap {
    		fname := fp.filename(sessionId)
    		if len(fname) == 0 {
    			continue
    		}
    		mux.Lock()
    		info, err := os.Stat(fname)
    		if err != nil {
    			mux.Unlock()
    			continue
    		}
    		modTime := info.ModTime().Unix() //文件最后访问时间
    		if modTime+expire*60 < now {     //已超出过期时间
    			err = os.Remove(fname)
    			mux.Unlock()
    			if err != nil {
    				delete(fp.muxMap, sessionId)
    			}
    		} else {
    			mux.Unlock()
    		}
    	}
    	return nil
    }
    

     这样就完成了文件session存储器的实现。

    当然我们也可以使用内存或数据库等其他方式进行session数据的存储,只需实现SessionProvider接口,并将其实例化对象赋值给session管理器创建方法的provider参数,即可实现不同存储方式的快速切换。

  • 相关阅读:
    CentOS6 配置阿里云 NTP 服务
    使用docker-compose运行nginx容器挂载时遇到的文件/目录问题
    Springboot配置文件参数使用docker-compose实现动态配置
    Dockerfile文件全面详解
    docker 生成mysql镜像启动时自动执行sql
    CentOS无法识别NTFS格式U盘完美解决方案
    网络模型与网络策略
    k8s更换网络插件:从flannel更换成calico
    数据采集实战(四)-- 线性代数习题答案下载
    leedcode 146. LRU 缓存机制(哈希+双向链表)
  • 原文地址:https://www.cnblogs.com/wujuntian/p/12070875.html
Copyright © 2011-2022 走看看