七、会话技术

7.1、会话技术

在学习HTTP超文本传输协议的时候,就已经知道HTTP是一种无状态的传输协议,对于事务处理没有记忆能力。对于客户端浏览器发出的请求,Web服务器无法区分是不是源自通一个浏览器,所以,这时就需要额外的数据用于维持回话。

7.1.1、Cookie

1. Cookie是什么

  1. HTTP是无状态协议,服务器不能记录浏览器的访问状态,也就是说服务器不能区分两次请求是否由同一个客户端发出
  2. Cookie就是解决HTTP协议无状态的方案之一,中文就是小甜饼的意思。
  3. Cookie实际上就是服务器保存在浏览器上的一段信息,浏览器有了Cookie之后,每次向服务器发送请求都会同时将该信息发送给服务器,服务器收到请求后,就可以根据该信息处理请求
  4. Cookie由服务器创建,病假发送给浏览器,最终由浏览器保存

2. 读写Cookie

// 设置Cookie
c.SetCookie("xxx","yyy")
// 获取Cookie
c.Cookie("xxx") // "yyy"

(1)登录案例

package main

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

func index(c *gin.Context) {
	isLogin, _ := c.Cookie("isLogin")

	if isLogin == "true" {
		user, _ := c.Cookie("username")
		c.HTML(200, "index.html", gin.H{
			"username": user,
		})
	} else {
		c.Redirect(302, "/login")
	}
}

func login(c *gin.Context) {
	if c.Request.Method == "GET" {
		c.HTML(200, "login.html", nil)
	} else {
		user := c.PostForm("user")
		pwd := c.PostForm("pwd")
		if user == "root" && pwd == "123" {
			// 给客户端设置cookie
			// maxAge int, 单位 s
			// path cookie  所在目录
			// domain string  域名
			// secure  是否只能通过https访问
			// httponly bool  是否允许别人通过js获取自己的cookie
			c.SetCookie("isLogin", "true", 200000, "/", "127.0.0.1", false, true)
			c.SetCookie("username", user, 200000, "/", "127.0.0.1", false, true)
			//c.Redirect(302,"/index")
			c.Redirect(302, "/index")
		} else {
			c.Redirect(302, "/login")
		}
	}
}

func main() {
	r := gin.Default()
	// 返回一个html页面
	r.LoadHTMLGlob("templates/*")
	r.Any("/login", login)
	r.GET("/index", index)
	r.Run() // 监听并0.0.0.0:8000 上启动服务
}

(2)最后访问时间案例

package main

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

func index(c *gin.Context) {
	// 获取Cookie
	lastVist, _ := c.Cookie("lastVist")
	fmt.Println("lastVist", lastVist)

	// 设置Cookie
	now := time.Now().String()[:19]
	c.SetCookie("lastVist", now, 100, "/", "127.0.0.1", false, true)

	c.HTML(200, "index.html", gin.H{
		"lastVist": lastVist,
	})
}

func main() {
	r := gin.Default()
	// 返回一个HTML页面
	r.LoadHTMLGlob("templates/*")
	r.GET("/index", index)
	r.Run() // 监听并在 0.0.0.0:8080 上启动服务
}

7.1.2、session

gin框架在处理session时有专门的中间件,我们可以直接使用。

中间件:github.com/gin-contrib/sessions ,我们直接安装依赖 go get github.com/gin-contrib/sessions 即可引入使用。

我们参考官网案例:

package main

import (
        // 导入session包
	"github.com/gin-contrib/sessions"
       // 导入session存储引擎
	"github.com/gin-contrib/sessions/cookie"
        // 导入gin框架包
	"github.com/gin-gonic/gin"
)

func main() {
	r := gin.Default()

        // 创建基于cookie的存储引擎,yuan 参数是用于加密的密钥,可以随便填写
	store := cookie.NewStore([]byte("root"))

        // 设置session中间件,参数mysession,指的是session的名字,也是cookie的名字
       // store是前面创建的存储引擎
	r.Use(sessions.Sessions("mysession", store))

	r.GET("/test", func(c *gin.Context) {
                // 初始化session对象
		session := sessions.Default(c)
                
                // 通过session.Get读取session值
                // session是键值对格式数据,因此需要通过key查询数据

		if session.Get("hello") != "world" {
                        // 设置session数据,()
			                  session.Set("hello", "world")
                        // 删除session数据
                        session.Delete("tizi365")
                        // 删除整个session
                        session.Clear()
                        // 保存session数据
		                   	session.Save()
		}
                
		c.JSON(200, gin.H{"hello": session.Get("hello")})
	})
	r.Run(":8000")
}

使用sessions 中间件注意要点:

  1. session仓库其实就是一个map[interface]interface对象,所有session可以存储任意数据。
  2. session使用编解码器自导gob,所以存储类似:struct、map、这些对象需要先注册对象,不然会爆粗 gob: type not registered for...
  3. session存储引擎支持:Cookie、内存、MongoDB、Redis、postgres、memstore、memcached以及gorm支持的各类数据库(MySQL、sqlite)
  4. session在创建时有一个配置项,可以配置session过期时间、Cookie、domain、secure、path等参数
  5. 调用session方法:Set()、Delete()、Clear()、方法后,必须调用一次Save()方法。否则session数据不会更新

gob注册案例

type User struct {
    Name string
}

session配置项案例

// store 就是前面创建的存储引擎
store.Options(sessions.Options{
		Secure:   true,
		SameSite: 4,
		Path:     "/",
		MaxAge:   m.MaxAge,
	})

cookie和session的区别

  • Session存储数据在服务器端,Cookie在客户端
  • Session没有数据大小限制,Cookie有
  • Session数据更加安全

7.1.3、基于session的登录验证

func LoginHtml(context *gin.Context) {
	context.HTML(200, "login.html", nil)
}
func Login(context *gin.Context) {
	// 获取登陆信息
	//roleId := context.PostForm("role_id")
	account := context.PostForm("user")
	pwd := context.PostForm("pwd")
	// 数据库查询
	var user User
	DB.Where("account = ? and pwd = ?", account, pwd).Find(&user)

	if user.ID != 0 {
		userId := strconv.Itoa(user.ID)
		//  登录成功
		// 初始化session对象
		session := sessions.Default(context)
		// 设置session数据,()
		session.Set("user_id", userId)
		session.Save()

		context.Redirect(http.StatusMovedPermanently, "/")

	} else {
		// 登录失败
		context.HTML(200, "login.html", gin.H{
			"err": "用户名或者密码错误",
		})

	}
}

func Logout(context *gin.Context) {

	// 初始化session对象
	session := sessions.Default(context)
	// 设置session数据,()
	session.Delete("user_id")

	context.Redirect(http.StatusMovedPermanently, "/login")

}

7.2、中间件

7.2.1、中间件语法

package main

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

func main()  {
  // 默认使用了2个中间件Logger(), Recovery()
	app := gin.Default()
	// 全局注册中间件
	app.Use(M1())
	app.Use(M2())
	app.GET("/", func(context *gin.Context) {
		fmt.Println("Hello yuan")
		context.Writer.WriteString("Hello World")
	})
	app.Run()
}

func M1() gin.HandlerFunc {
	return func(context *gin.Context) {
		// 路由处理前执行
		fmt.Printf("M1视图函数前执行\n")
		// context.Abort()
		context.Next()
		// 路由处理后执行
		fmt.Printf("M1视图函数后执行\n")
	}
}

func M2() gin.HandlerFunc {
	return func(context *gin.Context) {
		// 路由处理前执行
		fmt.Printf("M2视图函数前执行\n")
		context.Next()
		// 路由处理后执行
		fmt.Printf("M2视图函数后执行\n")
	}
}

7.2.2、中间件应用一:CORS

gin.Default()默认使用了LoggerRecovery中间件,其中:

  • Logger中间件将日志写入gin.DefaultWriter,即使配置了GIN_MODE=release

  • Recovery中间件会recover任何panic。如果有panic的话,会写入500响应码。

如果不想使用上面两个默认的中间件,可以使用gin.New()新建一个没有任何默认中间件的路由。

注意:当在中间件或handler中启动新的goroutine时,不能使用原始的上下文(c *gin.Context),必须使用其只读副本(c.Copy())。

跨域配置中间件

// 定义全局的CORS中间件
func Cors() gin.HandlerFunc {
    return func(c *gin.Context) {
        c.Writer.Header().Add("Access-Control-Allow-Origin", "*")
        c.Next()
    }
}

跨域配置中间件(高级请求配置)

 func(c *gin.Context) {
     method := c.Request.Method
     origin := c.GetHeader("Origin")
     c.Header("Access-Control-Allow-Origin", origin) // 注意这一行,不能配置为通配符“*”号
     c.Header("Access-Control-Allow-Credentials", "true") // 注意这一行,必须设定为 true
     c.Header("Access-Control-Allow-Headers", "Access-Control-Allow-Headers,Cookie, Origin, X-Requested-With, Content-Type, Accept, Authorization, Token, Timestamp, UserId") // 我们自定义的header字段都需要在这里声明
     c.Header("Access-Control-Allow-Methods", "POST, GET, OPTIONS,DELETE,PUT")
     c.Header("Access-Control-Expose-Headers", "Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers, Content-Type,cache-control")
 ​
     // 放行所有OPTIONS方法
     if method == "OPTIONS" {
       //c.AbortWithStatus(http.StatusNoContent)
       c.AbortWithStatus(http.StatusOK)
     }
     // 处理请求
     c.Next()
   }

7.2.3、中间件应用二:权限访问控制

package model

type User struct {
	ID      int    `gorm:"primaryKey"`
	Account string // 账号
	Pwd     string `gorm:"type:varchar(100);not null"`

	// 多对一
	RoleID int
	Role   Role
}

type Role struct {
	BaseModel
	Permissions []Permission `gorm:"many2many:role2permission;constraint:OnDelete:CASCADE;"`
}

type Permission struct {
	BaseModel
	Url string `gorm:"not null;"`
}

权限访问控制中间件:

package middlewares

import (
	. "css/database"
	. "css/model"
	"fmt"
	"github.com/gin-contrib/sessions"
	"github.com/gin-gonic/gin"
	"gorm.io/gorm/clause"
	"net/http"
	"regexp"
	"strconv"
	"strings"
)

func PermissionMD() gin.HandlerFunc {
	return func(context *gin.Context) {

		// 1. 获取请求路径
		path := context.Request.URL.Path
		fmt.Println("path:::", path)
		// 2. 校验路径是否在白名单
		validUrlSlice := []string{"/login", "/reg", "^/static/.*", "/logout"}
		for _, validUrl := range validUrlSlice {
			// 自由路径
			re := regexp.MustCompile(validUrl)
			ret := re.FindAllStringSubmatch(path, -1)
			if len(ret) != 0 {
				context.Next()
				return
			}
		}
		// 3. 校验是否登录成功
		// 初始化session对象
		session := sessions.Default(context)
		userID := session.Get("user_id")
		fmt.Println("userID::::::", userID)
		if userID == nil {
			context.Redirect(http.StatusMovedPermanently, "/login")
			return
		}
		// 4. 判断登录用户是否拥有该路径权限

		var user User
		DB.Where("ID = ?", userID).Preload("Role.Permissions").Find(&user)
		fmt.Println("user permissions:", user.Role.Permissions)

		if user.RoleID == 1 || user.RoleID == 2 {
			fmt.Println("in student role......")
			// 登录人是学生
			// 当前登陆学生的学号
			var student Student
			DB.Where("user_id = ?", user.ID).Preload(clause.Associations).Find(&student)
			snoStr := strconv.Itoa(student.Sno)
			// 权限匹配
			for _, permission := range user.Role.Permissions {
				permissionUrlReg := strings.Replace(permission.Url, "\\d+", snoStr, -1)
				fmt.Println("permissionUrlReg", permissionUrlReg, path)

				re := regexp.MustCompile("^" + permissionUrlReg + "$")
				results := re.FindAllStringSubmatch(path, -1)

				if len(results) != 0 {
					// 拥有该路径权限并放行
					context.Keys["loginUser"] = student
					context.Next()
					return
				}
			}

		} else {
			fmt.Println("in admin role......")
			// 登录人为管理员admin
			var admin Admin
			DB.Where("user_id = ?", user.ID).Preload(clause.Associations).Find(&admin)
			// 正则匹配成功即可
			for _, permission := range user.Role.Permissions {
				re := regexp.MustCompile("^" + permission.Url + "$")
				results := re.FindAllStringSubmatch(path, -1)

				if len(results) != 0 {
					context.Keys["loginUser"] = admin
					context.Next()
					return
				}
			}

		}

		context.String(http.StatusForbidden, "Forbidden!!!")
		context.Abort()

	}
}

 

posted @ 2023-07-27 22:17  xiaohaoge  阅读(12)  评论(0编辑  收藏  举报