gin學習筆記--session中間件
cookie和session基礎知識點總結
Cookie
HTTP請求是無狀態的,
服務端讓用戶的客戶端(瀏覽器)保存一小段數據
Cookie作用機制:
-
是由服務端保存在客戶端的鍵值對數據(客戶端可以阻止服務端保存Cookie)
-
每次客戶端發請求的時候會自動攜帶該域名下的Cookie
-
不用域名間的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請求的狀態
- 保存用戶登錄的狀態
- 保存用戶購物車的狀態
- 保存用於定制化的需求
Cookie的缺點:
- 數據量最大4K
- 保存在客戶端(瀏覽器)端,不安全
Session
保存在服務端的鍵值對數據。
Session的存在必須依賴於Cookie,Cookie中保存了每個用戶Session的唯一標識。
Session的特點:
- 保存服務端,數據量可以存很大(只要服務器支持)
- 保存在服務端也相對保存在客戶端更安全
- 需要自己去維護一個Session服務,會提高系統的復雜度。
Seesion的工作流程
源碼展示
目錄結構
源碼
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界面
(2)跳轉到登陸界面,輸入賬號密碼
(3)再次訪問vip界面,登陸成功
重點總結
-
gin框架的基本使用,包括路由,參數綁定,表單數據解析,模板渲染等。參數綁定真的很方便,它可以再綁定后解析各種形式的數據,JSON,form表單QueryString等。
-
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完成傳遞。 -
在session的結構上可以划分為兩部分:一個是整體的session大倉庫,里面存儲該網站所有的用戶的sessiondata,他使用一個大map來維護,他使用數據結構Mgr來進行管理;這里面的sessiondata又是一個小倉庫,里面存儲每個用戶的具體session信息,他使用數據結構SessionData來維護。
-
由於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。 -
關於操作redis,本文使用的第三方庫是
github.com/go-redis/redis
還可以使用github.com/garyburd/redigo/redis
-
在創建用戶的小倉庫sessiondata時,需要給每個用戶分配一個獨有的sessionid,這里使用了第三方庫
github.com/satori/go.uuid
,單有一些問題,就是他的uuid是字符串型的,不太方便檢索,因此可以使用snowflake
算法來生成一個全局的全局ID,具體使用方法請自行百度。 -
整理一下代碼的流程
-
程序首先根據傳入信息,選擇中間件版本memory還是redis,然后取初始化他們的管理者實例(大倉庫),在選擇redis時還要初始化一下連接池。
-
用戶訪問服務器的/vip界面
-
首先進入全局的中間件
SessionMiddleware
,目的是為了拿出用戶cookie里對應的小倉庫數據,如果用戶第一次來,那就給他創建一個專屬的小倉庫。該中間件結束時,會在用戶的context中設置一個key-value,存儲着用戶的小倉庫session數據。並且用戶拿到了一個全新的cookie(就像是一把鑰匙)。 -
接下來金進入鑒權中間件。他的作用是檢查上下文context中用戶的小倉庫內容(sessiondata),里面是否有islogin=true的字段,如果沒有,在就重定向到login界面是實現登錄。
-
在login姐買你輸入賬號密碼並驗證成功后,會在用戶小倉庫sessiondata中寫入islogin=true的字段
-
再次訪問/vip,會再次走一遍兩個中間件,並且完成驗證,最終到達
vipHandler
,給用戶返回vip登陸成功界面。
-