七、会话技术
7.1.1、Cookie
1. Cookie是什么
- HTTP是无状态协议,服务器不能记录浏览器的访问状态,也就是说服务器不能区分两次请求是否由同一个客户端发出
- Cookie就是解决HTTP协议无状态的方案之一,中文就是小甜饼的意思。
- Cookie实际上就是服务器保存在浏览器上的一段信息,浏览器有了Cookie之后,每次向服务器发送请求都会同时将该信息发送给服务器,服务器收到请求后,就可以根据该信息处理请求
- Cookie由服务器创建,病假发送给浏览器,最终由浏览器保存
2. 读写Cookie
// 设置Cookie
c.SetCookie("xxx","yyy")
// 获取Cookie
c.Cookie("xxx") // "yyy"
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 上启动服务
}
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时有专门的中间件,我们可以直接使用。
中间件:
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 中间件注意要点:
- session仓库其实就是一个map[interface]interface对象,所有session可以存储任意数据。
- session使用编解码器自导gob,所以存储类似:struct、map、这些对象需要先注册对象,不然会爆粗
gob: type not registered for...
- session存储引擎支持:Cookie、内存、MongoDB、Redis、postgres、memstore、memcached以及gorm支持的各类数据库(MySQL、sqlite)
- session在创建时有一个配置项,可以配置session过期时间、Cookie、domain、secure、path等参数
- 调用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数据更加安全
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")
}
}
gin.Default()
默认使用了Logger
和Recovery
中间件,其中:
-
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()
}
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()
}
}