gin學習筆記--session中間件


gin學習筆記--session中間件

cookie和session基礎知識點總結

HTTP請求是無狀態的,

服務端讓用戶的客戶端(瀏覽器)保存一小段數據

Cookie作用機制:

  1. 是由服務端保存在客戶端的鍵值對數據(客戶端可以阻止服務端保存Cookie)

  2. 每次客戶端發請求的時候會自動攜帶該域名下的Cookie

  3. 不用域名間的Cookie是不能共享的

Go操作Cookie:

net/http

查詢Cookie:http.Cookie("key")

設置Cookie:·http.SetCookie(w http.ResponseWriter, cookie *http.Cookie)

gin框架操作Cookie:

查詢Cookie:c.Cookie("key")

設置Cookie:c.SetCookie("key", "value", domain, path, maxAge, secure, httpOnly)

Cookie的應用場景

保存HTTP請求的狀態

  1. 保存用戶登錄的狀態
  2. 保存用戶購物車的狀態
  3. 保存用於定制化的需求

Cookie的缺點:

  1. 數據量最大4K
  2. 保存在客戶端(瀏覽器)端,不安全

Session

保存在服務端的鍵值對數據。

Session的存在必須依賴於Cookie,Cookie中保存了每個用戶Session的唯一標識。

Session的特點:

  1. 保存服務端,數據量可以存很大(只要服務器支持)
  2. 保存在服務端也相對保存在客戶端更安全
  3. 需要自己去維護一個Session服務,會提高系統的復雜度。

Seesion的工作流程

mark

源碼展示

目錄結構

mark

源碼

main.go

package main

//gin demo
import (
	"github.com/gin-gonic/gin"
	"github.com/gin_learing/session/gin_session"
	"net/http"
)

func main(){
	r:=gin.Default()
	r.LoadHTMLGlob("templates/*")
	//初始化全局的mgrobj
	gin_session.InitMgr("redis","127.0.0.1:6379")
	//session作為全局的中間件
	r.Use(gin_session.SessionMiddleware(gin_session.MgrObj))
	r.Any("/login", loginHandler)
	r.GET("/index", indexHandler)
	r.GET("/home", homeHandler)
	r.GET("/vip", AuthMiddleware, vipHandler)

	//沒有匹配到走下面
	r.NoRoute(func(c *gin.Context) {
		c.HTML(http.StatusNotFound, "404.html", nil)
	})
	r.Run()
}

handler.go

package main

import (
	"fmt"
	"github.com/gin-gonic/gin"
	"github.com/gin_learing/session/gin_session"
	"net/http"
)

//用戶信息
type UserInfo struct {
	Username string `form:"username"`
	Password  string`form:"password"`
}

// 編寫一個校驗用戶是否登錄的中間件
// 其實就是從上下文中取到session data,從session data取到isLogin
func AuthMiddleware(c *gin.Context){
	// 1. 從上下文中取到session data
	// 1. 先從上下文中獲取session data
	fmt.Println("in Auth")
	tmpSD, _ := c.Get(gin_session.SessionContextName)
	sd := tmpSD.(gin_session.SessionData)
	// 2. 從session data取到isLogin
	fmt.Printf("%#v\n", sd)
	value, err := sd.Get("isLogin")
	if err != nil {
		fmt.Println(err)
		// 取不到就是沒有登錄
		c.Redirect(http.StatusFound, "/login")
		return
	}
	fmt.Println(value)
	isLogin, ok := value.(bool)//類型斷言
	if !ok {
		fmt.Println("!ok")
		c.Redirect(http.StatusFound, "/login")
		return
	}
	fmt.Println(isLogin)
	if !isLogin{
		c.Redirect(http.StatusFound, "/login")
		return
	}
	c.Next()
}

//這個是最主要的,因此涉及到表單數據的提取,cookie的設置等。
func loginHandler(c *gin.Context){
	if c.Request.Method=="POST"{//判斷請求的方法,先判是否為post
		toPath := c.DefaultQuery("next", "/index")//一個路徑,用於后面的重定向
		var u UserInfo
		//綁定,並解析參數
		err:=c.ShouldBind(&u)
		if err != nil {
			c.HTML(http.StatusOK, "login.html", gin.H{
				"err": "用戶名或密碼不能為空",
			})
			return
		}
		//解析成功
		//驗證輸入的賬號密碼受否正確
		//這里再生產中應該去數據區取信息進行比對,但這里直接寫死
		if u.Username=="zhouzheng"&&u.Password=="123"{
			//接下來是核心代碼
			//驗證成功,,在當前sessiondata設置islogin=true
			// 登陸成功,在當前這個用戶的session data 保存一個鍵值對:isLogin=true
			// 1. 先從上下文中獲取session data
			tmpSD, ok := c.Get(gin_session.SessionContextName)
			if !ok{
				panic("session middleware")
			}
			sd := tmpSD.(gin_session.SessionData)
			// 2. 給session data設置isLogin = true
			sd.Set("isLogin", true)
			//調用Save,存儲到數據庫
			sd.Save()
			//跳轉到index界面
			c.Redirect(http.StatusMovedPermanently,toPath)
		}else{//驗證失敗,重新登陸
			//返回錯誤和重新登陸界面
			c.HTML(http.StatusOK, "login.html", gin.H{
				"err": "用戶名或密碼錯誤",
			})
			return
		}
	}else{//get

		c.HTML(http.StatusOK, "login.html", nil)
	}
}

func indexHandler(c *gin.Context){
	c.HTML(http.StatusOK, "index.html", nil)
}

func homeHandler(c *gin.Context){
	c.HTML(http.StatusOK, "home.html", nil)
}

func vipHandler(c *gin.Context){
	c.HTML(http.StatusOK, "vip.html", nil)
}

session.go

package gin_session

import (
	"github.com/gin-gonic/gin"
)

const (
	SessionCookieName = "session_id" // sesion_id在Cookie中對應的key
	SessionContextName = "session" // session data在gin上下文中對應的key
)

//定義一個全局的Mgr
var (
	// MgrObj 全局的Session管理對象(大倉庫)
	MgrObj Mgr
)

//構造一個Mgr
func InitMgr(name string,addr string,option...string){

	switch name{
	case "memory"://初始化一個內存版管理者
		MgrObj=NewMemory()
	case "redis":
		MgrObj=NewRedisMgr()
	}
	MgrObj.Init(addr,option...)//初始化mgr
}

//
type SessionData interface {
	GetID()string // 返回自己的ID
	Get(key string)(value interface{}, err error)
	Set(key string, value interface{})
	Del(key string)
	Save() // 保存
}

//不同版本的管理者接口
type Mgr interface {
	Init(addr string,option...string)
	GetSessionData(sessionId string)(sd SessionData,err error)
	CreatSession()(sd SessionData)
}

//gin框架中間件
func SessionMiddleware(mgrObj Mgr)gin.HandlerFunc{

	return func(c *gin.Context){

		//1.請求剛過來,從請求的cookie中獲取SessionId
		SessionID,err:=c.Cookie(SessionCookieName)
		var sd SessionData
		if err != nil {
			//1.1 第一次來,沒有sessionid,-->給用戶建一個sessiondata,分配一個sessionid
			sd=mgrObj.CreatSession()
		}else {
			//1.2  取到sessionid
			//2. 根據sessionid去大倉庫取sessiondata
			sd,err=mgrObj.GetSessionData(SessionID)
			if err != nil {
				//sessionid有誤,取不到sessiondata,可能是自己偽造的
				//重新創建一個sessiondata
				sd=mgrObj.CreatSession()
				//更新sessionid
				SessionID=sd.GetID()//這個sessionid用於回寫coookie
			}
		}
		//3. 如何實現讓后續所有請求的方法都拿到sessiondata? 讓每個用戶的dessiondata都不同
		//3.利用gin框架的c.Set("session",sessiondata)
		c.Set(SessionContextName,sd)
		//回寫cookie
		c.SetCookie(SessionCookieName,SessionID,3600,"/","127.0.0.1",false,true)
		c.Next()
	}
}

memory.go

package gin_session

import (
	"fmt"
	uuid "github.com/satori/go.uuid"
	"sync"
)

//內存版的session服務

//SessionData支持的操作

//type MemSD struct {
//	ID string
//	Data map[string]interface{}
//	rwLock sync.RWMutex // 讀寫鎖,鎖的是上面的Data
//	// 過期時間
//}

//memory的sessiondata
type MemSD struct {
	ID string
	Data map[string]interface{}
	rwLock sync.RWMutex // 讀寫鎖,鎖的是上面的Data
	// 過期時間
}

// Get 根據key獲取值
func (m *MemSD)Get(key string)(value interface{}, err error){
	// 獲取讀鎖
	m.rwLock.RLock()
	defer m.rwLock.RUnlock()
	value, ok := m.Data[key]
	if !ok{
		err = fmt.Errorf("invalid Key")
		return
	}
	return
}

// Set 根據key獲取值
func (m *MemSD)Set(key string, value interface{}){
	// 獲取寫鎖
	m.rwLock.Lock()
	defer m.rwLock.Unlock()
	m.Data[key] = value
}

// Del 刪除Key對應的鍵值對
func (m *MemSD)Del(key string){
	// 刪除key對應的鍵值對
	m.rwLock.Lock()
	defer m.rwLock.Unlock()
	delete(m.Data, key)
}

//Save方法,被動設置的,因為要照顧redis版的接口
func (m *MemSD)Save(){
	return
}

//GetID 為了拿到接口的ID數據
func (m *MemSD) GetID()string{
	return m.ID

}
//管理全局的session
type MemoryMgr struct {
	Session map[string]SessionData //存儲所有的session的一個大切片
	rwLock sync.RWMutex            //讀寫鎖,用於讀多寫少的情況,讀鎖可以重復的加,寫鎖互斥
}

//內存版初始化session倉庫
func NewMemory() (Mgr) {
	return &MemoryMgr{
		Session: make(map[string]SessionData,1024),
	}
}

//init方法
func (m *MemoryMgr)Init(addr string,option...string){
	//這里創建Init方法純屬妥協,其實memory版的並不需要初始化,前面NewMemory已經把活干完了
	//這里只是為了滿足接口的定義,因為redis里需要這個方法取去連接數據庫
	return
}
//GetSessionData 根據傳進來的SessionID找到對應Session
func (m *MemoryMgr)GetSessionData(sessionId string)(sd SessionData,err error){
	// 獲取讀鎖
	m.rwLock.RLock()
	defer m.rwLock.RUnlock()
	sd,ok:=m.Session[sessionId]
	if  !ok {
		err=fmt.Errorf("無效的sessionId")
		return
	}
	return
}

//CreatSession 創建一個session記錄
func (m *MemoryMgr)CreatSession()(sd SessionData){
	//1. 構造一個sessionID
	uuidObj:=uuid.NewV4()
	//2.創建一個sessionData
	sd= NewMemorySessionData(uuidObj.String())
	//3.創建對應關系
	m.Session[sd.GetID()]=sd
	//返回
	return
}
//NewRedisSessionData  的構造函數,用於構造sessiondata小倉庫,小紅塊
func NewMemorySessionData(id string)SessionData {
	return &MemSD{
		ID: id,
		Data: make(map[string]interface{},8),
	}
}

redis.go

package gin_session

import (
	"encoding/json"
	"fmt"
	uuid "github.com/satori/go.uuid"
	"strconv"
	"sync"
	"github.com/go-redis/redis"
	"time"
)

//NewRedisSessionData  的構造函數,用於構造sessiondata小倉庫,小紅塊
func NewRedisSessionData(id string,client *redis.Client)SessionData {
	return &RedisSD{
		ID: id,
		Data: make(map[string]interface{},8),
		client:client,
	}
}

//redis版的sessiondata的數據結構
type RedisSD struct{
	ID string
	Data map[string]interface{}
	rwLock sync.RWMutex // 讀寫鎖,鎖的是上面的Data
	expired int // 過期時間
	client *redis.Client // redis連接池
}

func (r *RedisSD) Get(key string) (value interface{}, err error) {

	// 獲取讀鎖
	r.rwLock.RLock()
	defer r.rwLock.RUnlock()
	value, ok := r.Data[key]
	if !ok{
		err = fmt.Errorf("invalid Key")
		return
	}
	return
}

func (r *RedisSD) Set(key string, value interface{}) {
	// 獲取寫鎖
	r.rwLock.Lock()
	defer r.rwLock.Unlock()
	r.Data[key] = value
}

func (r *RedisSD) Del(key string) {
	// 刪除key對應的鍵值對
	r.rwLock.Lock()
	defer r.rwLock.Unlock()
	delete(r.Data, key)

}

func (r *RedisSD) Save() {
	//將最新的sessiondata保存到redis中
	value,err:=json.Marshal(r.Data)
	if err != nil {
		 fmt.Printf("redis 序列化sessiondata失敗 err=%v\n",err)
		return
	}
	//入庫
	r.client.Set(r.ID,value,time.Duration(r.expired)*time.Second)//注意這里要用time.Duration轉換一下
}

func (r *RedisSD) GetID()string{//為了拿到接口的ID數據
	return r.ID
}
//NewRedisMgr  redis版初始化session倉庫,構造函數
func NewRedisMgr()(Mgr){
	//返回一個對象實例
	return &RedisMgr{
		Session: make(map[string]SessionData,1024),
	}

}

//大倉庫
type RedisMgr struct{
	Session map[string]SessionData //存儲所有的session的一個大切片
	rwLock sync.RWMutex
	client *redis.Client//redis連接池
}
//RedisMgr初始化
func (r *RedisMgr)Init(addr string,option...string){//這里的option...代表不定參數,參數個數不確定
	//	初始化redis連接池
	var(
		passwd string
		db string
	)
	if len(option)==1{
		passwd=option[0]
	}else if len(option)==2 {
		passwd=option[0]
		db=option[1]
	}
	//轉換一下db數據類型,輸入為string,需要轉成int
	dbValue,err:=strconv.Atoi(db)
	if err != nil {
		dbValue=0//如果轉換失敗,geidb一個默認值
	}
	r.client = redis.NewClient(&redis.Options{
		Addr:     addr,
		Password: passwd, // no password set
		DB:       dbValue,  // use default DB
	})

	_, err =r.client.Ping().Result()
	if err != nil {
		panic(err)
	}
}

//加載數據庫里的數據
func (r *RedisMgr)LoadFromRedis(sessionID string) (err error) {
	//1.連接redis
	//2.根據sessioniD拿到數據
	value,err:=r.client.Get(sessionID).Result()
	if err != nil {
		//redis中wusessioinid對應的sessiondata
		fmt.Errorf("連接數據庫失敗")
		return
	}
	//3.反序列化成 r.session
	err=json.Unmarshal([]byte(value),&r.Session)
	if err != nil {
		//反序列化失敗
		fmt.Println("連接數據庫失敗")
		return
	}
	return
}

//GetSessionData 根據傳進來的SessionID找到對應Session
func (r *RedisMgr) GetSessionData(sessionId string) (sd SessionData, err error) {

	//1.r.sesion已經從redis中拿到數據
	if r.Session==nil{
		err=r.LoadFromRedis(sessionId)
		if err != nil {
			return nil, err
		}
	}
	//2.r.session[sessionID]拿到sessionData
	// 獲取讀鎖
	r.rwLock.RLock()
	defer r.rwLock.RUnlock()
	sd,ok:=r.Session[sessionId]
	if  !ok {
		err=fmt.Errorf("無效的sessionId")
		return
	}
	return
}

//CreatSession 創建一個session記錄
func (r *RedisMgr) CreatSession() (sd SessionData) {
	//1. 構造一個sessionID
	uuidObj:=uuid.NewV4()
	//2.創建一個sessionData
	sd= NewRedisSessionData(uuidObj.String(),r.client)//從連接池中拿去一個client連接傳給小紅方塊
	//3.創建對應關系
	r.Session[sd.GetID()]=sd
	//返回
	return
}

templates

//404.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <h1>{{.username}}的home頁面,登錄后才能看</h1>
</body>
</html>

-----
//home.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <h1>{{.username}}的home頁面,登錄后才能看</h1>
</body>
</html>

----
//index.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>index</title>
</head>
<body>
    <h1>index頁面不登錄也能看!</h1>
</body>
</html>

----
//login.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>登錄</title>
</head>
<body>
    <form action="" method="POST" enctype="application/x-www-form-urlencoded">
        <div>
            <label>用戶名:
                <input type="text" name="username">
            </label>
        </div>
        <div>
            <label>密碼:
                <input type="password" name="password">
            </label>
        </div>
        <div>
            <input type="submit">
        </div>
    </form>
    <p style="color: #00ffcc">{{.err}}</p>
</body>
</html>

----
//vip.html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>
<body>
    <h1>歡迎尊敬的VIP用戶:{{.username}} 光臨大腳超市,奧力給!</h1>
</body>
</html>

預期效果

本節的目的是練習gin的基本操作和cookie與session。

通過session實現一個中間件。用來做同一的登陸校驗。

如想登陸vip界面時會要求先登陸賬號密碼,然后才可以訪問vip界面。

(1)第一次訪問vip界面

mark

(2)跳轉到登陸界面,輸入賬號密碼

mark

(3)再次訪問vip界面,登陸成功

mark

重點總結

  1. gin框架的基本使用,包括路由,參數綁定,表單數據解析,模板渲染等。參數綁定真的很方便,它可以再綁定后解析各種形式的數據,JSON,form表單QueryString等。

  2. gin中間件的使用,本文使用了兩個中間件,一個是全局的SessionMiddleware中間件r.Use(gin_session.SessionMiddleware(gin_session.MgrObj))他用來進行session的校驗和創建,首先判斷用戶的上下文中是否有cookie信息,SessionID,err:=c.Cookie(SessionCookieName),如果沒有則創建一個用戶的session倉庫(用戶的專屬小倉庫),如果有則去該倉庫中取出session數據用於之后的驗證判斷,這里其實還分兩種情況,一種就是用戶cookie里的sessionid過期了,所以要重新創建;另外一中就是成功取到了sessiondata。取到sessiondata后就涉及到一個問題,即使如何將sessiondata傳遞給后面的鑒權中間件AuthMiddleware(用戶判斷用戶是否登陸:sessiondata中islogin字段是否等於true),這里采用的c.Set(SessionContextName,sd),這是在上下文context中存儲一個key-value,再之后的AuthMiddleware中間件中可以使用c.Get(gin_session.SessionContextName)取出sessiondata完成傳遞。

  3. 在session的結構上可以划分為兩部分:一個是整體的session大倉庫,里面存儲該網站所有的用戶的sessiondata,他使用一個大map來維護,他使用數據結構Mgr來進行管理;這里面的sessiondata又是一個小倉庫,里面存儲每個用戶的具體session信息,他使用數據結構SessionData來維護。

  4. 由於session中間件要實現不同的版本,因此這里涉及到面向接口的編程。如本例中大倉庫和先倉庫均要實現memory版本和redis版本,因此他們都要設計成接口的形式。大倉庫Mgr接口要實現的方法有

    type Mgr interface {
    	Init(addr string,option...string)
    	GetSessionData(sessionId string)(sd SessionData,err error)
    	CreatSession()(sd SessionData)
    }
    

    小倉庫sessiondata要實現的方法有

    type SessionData interface {
       GetID()string // 返回自己的ID
       Get(key string)(value interface{}, err error)
       Set(key string, value interface{})
       Del(key string)
       Save() // 保存
    }
    

    大倉庫和小倉庫在使用之前都需要初始化,那么他們各自在哪里初始化呢?大倉庫在在運行開始就需要初始化,根據傳入的參數時“memory”還是“redis”決定初始化什么版本的大倉庫(用構造函數,返回一個對象實例),之后又調用了一個初始化函數MgrObj.Init(addr,option...)//初始化mgr,這個初始化和函數其實是為redis單獨創建的,目的是初始化redis連接池,這里其實也可以把他們放進構造函數NewRedisMgr中;小倉庫sessiondata實在session中間件中用sd=mgrObj.CreatSession()創建的。注意這里初始化了里面維護的map,大倉庫真正寫入數據是在創建了sessiondata后,小倉庫寫入數據是在登陸成功后的設置的islogin=true。

  5. 關於操作redis,本文使用的第三方庫是github.com/go-redis/redis還可以使用github.com/garyburd/redigo/redis

  6. 在創建用戶的小倉庫sessiondata時,需要給每個用戶分配一個獨有的sessionid,這里使用了第三方庫github.com/satori/go.uuid,單有一些問題,就是他的uuid是字符串型的,不太方便檢索,因此可以使用snowflake算法來生成一個全局的全局ID,具體使用方法請自行百度。

  7. 整理一下代碼的流程

    1. 程序首先根據傳入信息,選擇中間件版本memory還是redis,然后取初始化他們的管理者實例(大倉庫),在選擇redis時還要初始化一下連接池。

    2. 用戶訪問服務器的/vip界面

    3. 首先進入全局的中間件SessionMiddleware,目的是為了拿出用戶cookie里對應的小倉庫數據,如果用戶第一次來,那就給他創建一個專屬的小倉庫。該中間件結束時,會在用戶的context中設置一個key-value,存儲着用戶的小倉庫session數據。並且用戶拿到了一個全新的cookie(就像是一把鑰匙)。

    4. 接下來金進入鑒權中間件。他的作用是檢查上下文context中用戶的小倉庫內容(sessiondata),里面是否有islogin=true的字段,如果沒有,在就重定向到login界面是實現登錄。

    5. 在login姐買你輸入賬號密碼並驗證成功后,會在用戶小倉庫sessiondata中寫入islogin=true的字段

    6. 再次訪問/vip,會再次走一遍兩個中間件,並且完成驗證,最終到達vipHandler,給用戶返回vip登陸成功界面。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM