gin框架
gin入门
gin是一个用Go语言编写的web框架。它是一个类似于martini但拥有更好性能的API框架, 由于使用了httprouter,速度提高了近40倍。 如果你是性能和高效的追求者, 你会爱上Gin。也是Go世界里最流行的Web框架,Github上有32K+
star。 基于httprouter开发的Web框架。 中文文档齐全,简单易用的轻量级框架。
下载gin
go get github.com/gin-gonic/gin
简单实例
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func rootHandler(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"msg": "这是首页",
})
}
func main() {
// 启动默认驱动
r := gin.Default()
//给/hello配置一个函数
r.GET("/hello", func(c *gin.Context) {
c.JSON(200, gin.H{
"msg": "hello gin",
})
})
r.GET("/", rootHandler)
// 启动webserver
r.Run(":9000")
}
Gin渲染
HTML
首先定义一个存放模板文件的templates
文件夹,内部放了一张linux.png。
Gin框架中使用LoadHTMLGlob()
或者LoadHTMLFiles()
方法进行HTML模板渲染。
当我们渲染的HTML文件中引用了静态文件时,我们只需要按照以下方式在渲染页面前调用gin.Static
方法即可。
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func loginHandler(c *gin.Context) {
c.HTML(http.StatusOK, "login.html", gin.H{
"msg": "index",
})
}
func main() {
// 启动默认驱动
r := gin.Default()
// 加载模板目录
r.LoadHTMLGlob("templates/*")
//给/hello配置一个函数
r.GET("/hello", func(c *gin.Context) {
c.JSON(200, gin.H{
"msg": "hello gin",
})
})
// 设置静态文件的目录
// 第一个参数是代码中使用的路径,第二个参数是实际静态文件的路径
r.Static("/png", "./statics")
r.GET("/login", loginHandler)
// 启动webserver
r.Run(":9000")
}
login.html
<!DOCTYPE html>
<html lang="cn">
<head>
<meta charset="UTF-8">
<title>login</title>
</head>
<body>
<h1>login</h1>
<div>{{ .msg }}</div>
<img src="/png/linux.png" alt="">
</body>
</html>
自定义模板函数
定义一个不转义相应内容的safe模板函数
package main
import (
"html/template"
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
// 启动默认驱动
r := gin.Default()
r.SetFuncMap(template.FuncMap{
"safe": func(str string) template.HTML{
return template.HTML(str)
},
})
r.LoadHTMLFiles("./index.tmpl")
r.GET("/index", func(c *gin.Context) {
c.HTML(http.StatusOK, "index.tmpl", "值")
})
// 启动webserver
r.Run(":9000")
}
在index.tmpl模板中使用定义好的safe模板函数
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<title>SetFuncMap</title>
</head>
<body>
<div>{{ . | safe }}</div>
</body>
</html>
JSON渲染
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func jsonHandler(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"msg": "json",
})
}
func jsonstructHandler(c *gin.Context) {
type userinfo struct{
Name string `json:"名字"`
Password string `json:"密码"`
}
u1 := userinfo{
Name: "json",
Password: "123",
}
c.JSON(http.StatusOK, u1)
}
func main() {
// 启动默认驱动
r := gin.Default()
//JSON格式
r.GET("/json", jsonHandler)
//结构体格式
r.GET("/jsonstruct", jsonstructHandler)
// 启动webserver
r.Run(":9000")
}
XML渲染
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func xmlHandler(c *gin.Context) {
c.XML(http.StatusOK, gin.H{
"msg": "xml",
})
}
func xmlstructHandler(c *gin.Context) {
type userinfo struct{
Name string `xml:"名字"`
Password string `xml:"密码"`
}
u1 := userinfo{
Name: "xml",
Password: "123",
}
c.XML(http.StatusOK, u1)
}
func main() {
// 启动默认驱动
r := gin.Default()
//xml格式
r.GET("/xml", xmlHandler)
//结构体格式
r.GET("/xmlstruct", xmlstructHandler)
// 启动webserver
r.Run(":9000")
}
YAML渲染
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func yamlHandler(c *gin.Context) {
c.YAML(http.StatusOK, gin.H{
"msg": "yaml",
})
}
func main() {
// 启动默认驱动
r := gin.Default()
//yaml格式
r.GET("/yaml", yamlHandler)
// 启动webserver
r.Run(":9000")
}
获取参数
获取querystring参数
query string指的是URL中?后面携带的参数,例如: query_string?name=哈哈哈&city=呵呵。
获取请求的querystring参数的方法如下:
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func queryStringHandler(c *gin.Context) {
nameVal := c.DefaultQuery("name", "默认值") // 查不到就用指定的默认值(第二个参数)
cityVal := c.Query("city") // 查不到默认空字符串
c.JSON(http.StatusOK, gin.H{
"name": nameVal,
"city": cityVal,
})
}
func main() {
// 启动默认驱动
r := gin.Default()
//获取query string参数
// query_string?city=呵呵 query_string?name=哈哈哈&city=呵呵 query_string?name=哈哈哈
r.GET("/query_string", queryStringHandler)
// 启动webserver
r.Run(":9000")
}
获取form参数
当前端请求的数据通过form表单提交时,例如向/form发送一个POST请求,获取请求数据的方式如下:
使用postman工具
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func formHandler(c *gin.Context) {
// 提取form表单数据
nameVal := c.DefaultPostForm("name", "默认值") // 查不到使用默认值
cityVal := c.PostForm("city") // 查不到为空字符串
c.JSON(http.StatusOK, gin.H{
"name": nameVal,
"city": cityVal,
})
}
func main() {
// 启动默认驱动
r := gin.Default()
//获取form表单参数
r.POST("/form", formHandler)
// 启动webserver
r.Run(":9000")
}
获取路径path参数
请求的参数通过URL路径传递,例如:/book/list、/book/new、/book/delete。 获取请求URL路径中的参数的方式如下:
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func pathHandler(c *gin.Context) {
// 提取路径参数
actionVal := c.Param("action")
c.JSON(http.StatusOK, gin.H{
"action": actionVal,
})
}
func main() {
// 启动默认驱动
r := gin.Default()
//获取path参数
// 请求/book/list
r.GET("/book/:action", pathHandler)
// 启动webserver
r.Run(":9000")
}
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func pathsHandler(c *gin.Context) {
// 提取路径参数
nameVal := c.Param("name")
cityVal := c.Param("city")
c.JSON(http.StatusOK, gin.H{
"name": nameVal,
"city": cityVal,
})
}
func main() {
// 启动默认驱动
r := gin.Default()
//获取path参数
// 请求/user/哈哈哈/呵呵
r.GET("/user/:name/:city", pathsHandler)
// 启动webserver
r.Run(":9000")
}
获取json参数
当前端请求的数据通过JSON提交时,例如向/json发送一个POST请求,则获取请求参数的方式如下:
package main
import (
"encoding/json"
"fmt"
"net/http"
"github.com/gin-gonic/gin"
)
func jsonHandler(c *gin.Context) {
// 提取body数据
b, err := c.GetRawData()
if err != nil {
fmt.Println(err)
return
}
// 定义map或结构体
var m map[string]interface{}
// 反序列化
_ = json.Unmarshal(b, &m)
c.JSON(http.StatusOK, m)
}
func main() {
// 启动默认驱动
r := gin.Default()
//获取json表单
r.POST("/json", jsonHandler)
// 启动webserver
r.Run(":9000")
}
参数绑定
ShouldBind强大的功能,可以基于请求的Content-Type识别请求数据类型并利用反射机制自动提取请求中querystring、form表单、JSON、XML等参数到结构体中。能够基于请求自动提取JSON、form表单和querystring类型的数据,并把值绑定到指定的结构体对象。
package main
import (
"fmt"
"net/http"
"github.com/gin-gonic/gin"
)
type Login struct {
User string `form:"user" json:"user" binding:"required"`
Password string `form:"password" json:"password" binding:"required"`
}
var login Login
func jsonHandler(c *gin.Context) {
// ShouldBind()会根据请求的Content-Type自行选择绑定器
if err := c.ShouldBind(&login); err == nil {
fmt.Printf("%#v\n", login)
c.JSON(http.StatusOK, gin.H{
"user": login.User,
"password": login.Password,
})
} else {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
}
}
func formHandler(c *gin.Context){
// ShouldBind()会根据请求的Content-Type自行选择绑定器
if err := c.ShouldBind(&login); err == nil {
c.JSON(http.StatusOK, gin.H{
"user": login.User,
"password": login.Password,
})
} else {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
}
}
func querystringHandler(c *gin.Context){
// ShouldBind()会根据请求的Content-Type自行选择绑定器
if err := c.ShouldBind(&login); err == nil {
c.JSON(http.StatusOK, gin.H{
"user": login.User,
"password": login.Password,
})
} else {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
}
}
func main() {
// 启动默认驱动
r := gin.Default()
//绑定json
r.POST("/loginJson", jsonHandler)
//绑定form表单
r.POST("/loginForm", formHandler)
//绑定querystring
r.GET("/loginForm", querystringHandler)
// 启动webserver
r.Run(":9000")
}
文件上传
单个文件上传
upload.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>文件</title>
</head>
<body>
<form action="/upload" method="post" enctype="multipart/form-data">
<input type="file" name="filename">
<input type="submit">
</form>
</body>
</html>
main.go
package main
import (
"fmt"
"net/http"
"github.com/gin-gonic/gin"
)
func uploadHandler(c *gin.Context){
// 单个文件
fileObj, err := c.FormFile("filename")
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{
"err": err,
})
return
}
// fileObj: 上传文件对象
// fileObj.filename 拿到上传文件名
filePath := fmt.Sprintf("./%s", fileObj.Filename)
// 保存文件到本地的路径
c.SaveUploadedFile(fileObj, filePath)
c.JSON(http.StatusOK, gin.H{
"msg": "OK",
})
}
func main() {
// 启动默认驱动
r := gin.Default()
// 处理multipart forms提交文件时默认的内存限制是32 MiB
// 可以通过下面的方式修改
// router.MaxMultipartMemory = 8 << 20 // 8 MiB
r.LoadHTMLFiles("./upload.html")
r.GET("/upload", func(c *gin.Context) {
c.HTML(http.StatusOK, "upload.html", nil)
})
r.POST("/upload", uploadHandler)
// 启动webserver
r.Run(":9000")
}
多个文件上传
upload.html
multiple="multiple"可选多个文件
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>文件</title>
</head>
<body>
<form action="/upload" method="post" enctype="multipart/form-data">
<input type="file" name="filename" multiple="multiple">
<input type="submit">
</form>
</body>
</html>
main.go
package main
import (
"fmt"
"log"
"net/http"
"github.com/gin-gonic/gin"
)
func uploadsHandler(c *gin.Context){
// 多个文件
form, _ := c.MultipartForm()
files := form.File["filename"]
for _, fileObj := range files {
log.Println(fileObj.Filename)
filePath := fmt.Sprintf("./%s", fileObj.Filename)
fmt.Println(filePath)
// 保存文件到本地的路径
c.SaveUploadedFile(fileObj, filePath)
}
c.JSON(http.StatusOK, gin.H{
"msg": "OK",
})
}
func main() {
// 启动默认驱动
r := gin.Default()
// 处理multipart forms提交文件时默认的内存限制是32 MiB
// 可以通过下面的方式修改
// router.MaxMultipartMemory = 8 << 20 // 8 MiB
r.LoadHTMLFiles("./upload.html")
r.GET("/upload", func(c *gin.Context) {
c.HTML(http.StatusOK, "upload.html", nil)
})
r.POST("/upload", uploadsHandler)
// 启动webserver
r.Run(":9000")
}
Gin路由
Gin框架中的路由使用的是httprouter这个库,其基本原理就是构造一个路由地址的前缀树。
普通路由
r.POST("/upload", uploadsHandler)
r.PUT("/upload", uploadsHandler)
r.GET("/upload", uploadsHandler)
//Any匹配所有请求方法
r.Any("/upload", func(c *gin.Context) {...})
处理404代码
r.NoRoute(func(c *gin.Context) {
c.HTML(http.StatusNotFound, "views/404.html", nil)
})
路由组
可以将拥有共同URL前缀的路由划分为一个路由组。通常我们将路由分组用在划分业务逻辑或划分API版本时。
s := r.Group("/shopping")
{
s.GET("/index", sIndexHandler) // /shopping/index
s.GET("/home", sHomeHandler) // /shopping/home
}
liveGroup := r.Group("/live")
{
liveGroup.GET("/index", liveIndexHandler)
liveGroup.GET("/home", liveHomeHandler)
}
// 路由组也是支持嵌套的
v1 := r.Group("/v1")
{
v1.GET("/index", sIndexHandler)// /v1/index
v1Shopping := v1.Group("/shopping")
{
v1Shopping.GET("/home", sHomeHandler) // /v1/shopping/home
}
}
Gin中间件
Gin框架允许开发者在处理请求的过程中,加入用户自己的钩子(Hook)函数。这个钩子函数就叫中间件,中间件适合处理一些公共的业务逻辑,比如登录认证、权限校验、数据分页、记录日志、耗时统计等。
定义中间件
Gin中的中间件必须是一个gin.HandlerFunc类型。
//定义一个统计请求耗时的中间件。
package main
import (
"fmt"
"net/http"
"time"
"github.com/gin-gonic/gin"
)
func shopIndexHandler(c *gin.Context){
time.Sleep(3*time.Second)
fmt.Println(c.MustGet("key").(string)) // 从上下文取值
c.JSON(http.StatusOK, gin.H{
"code": 0,
"msg": "/s/index",
})
}
func castTime(c *gin.Context){
start := time.Now()
c.Set("key", "value") // 可以通过c.Set在请求上下文中设置值,后续的处理函数能够取到该值
c.Next() // 运行下一个Handler函数
//统计耗时
cast := time.Since(start)
fmt.Println("cast", cast)
}
func main() {
// 启动默认驱动
r := gin.Default() //Default包含了两个中间件Logger(), Recovery()
r.Use(castTime())
shoppingGroup := r.Group("/s")
{
shoppingGroup.GET("/index", shopIndexHandler)
}
// 启动webserver
r.Run(":9000")
}
注册中间件
gin框架中,我们可以为每个路由添加任意数量的中间件。
全局路由注册
func main() {
// 新建一个没有任何默认中间件的路由
r := gin.New()
// 注册一个全局中间件
r.Use(castTime())
shoppingGroup := r.Group("/s")
{
shoppingGroup.GET("/index", shopIndexHandler)
}
// 启动webserver
r.Run(":9000")
}
为某个路由单独注册
func main() {
r := gin.New()
// 路由单独注册中间件(可注册多个)
r.GET("/index", CastTime(), shopIndexHandler)
// 启动webserver
r.Run(":9000")
}
为路由组注册中间件
// 写法一
func main() {
r := gin.New()
// 路由组注册中间件
shoppingGroup := r.Group("/s", castTime())
{
shoppingGroup.GET("/index", shopIndexHandler)
}
// 启动webserver
r.Run(":9000")
}
// 写法二
func main() {
r := gin.New()
shoppingGroup := r.Group("/s")
// 路由组注册中间件
shoppingGroup.Use(castTime())
{
shoppingGroup.GET("/index", shopIndexHandler)
}
// 启动webserver
r.Run(":9000")
}
中间件注意事项
gin默认中间件
gin.Default()
默认使用了Logger
和Recovery
中间件,其中:
Logger
中间件将日志写入gin.DefaultWriter
,即使配置了GIN_MODE=release
。Recovery
中间件会recover任何panic
。如果有panic的话,会写入500响应码。
如果不想使用上面两个默认的中间件,可以使用gin.New()
新建一个没有任何默认中间件的路由。
gin中间件中使用goroutine
当在中间件或handler
中启动新的goroutine
时,不能使用原始的上下文(c *gin.Context),必须使用其只读副本(c.Copy()
)。
Gin使用logrus
logrus介绍
Logrus是Go(golang)的结构化logger,与标准库logger完全API兼容。
它有以下特点:
- 完全兼容标准日志库,拥有七种日志级别:
Trace
,Debug
,Info
,Warning
,Error
,Fatal
andPanic
。 - 可扩展的Hook机制,允许使用者通过Hook的方式将日志分发到任意地方,如本地文件系统,logstash,elasticsearch或者mq等,或者通过Hook定义日志内容和格式等
- 可选的日志输出格式,内置了两种日志格式JSONFormater和TextFormatter,还可以自定义日志格式
- Field机制,通过Filed机制进行结构化的日志记录
- 线程安全
logrus安装
go get github.com/sirupsen/logrus
gin框架使用logrus
package main
import (
"fmt"
"net/http"
"os"
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
)
// use logrus in gin
var log = logrus.New() // 在该程序的所有文件都能使用
func initLogrus() (err error) {
// 初始化logrus的配置
log.Formatter = &logrus.JSONFormatter{} // 记录JSON格式的日志
// 指定日志输出
file, err := os.OpenFile("./log/xx.log", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil{
fmt.Println("open lofg file failed, err", err)
return
}
log.Out = file
// 告诉gin框架把它日志也记录到我们打开的文件中
gin.SetMode(gin.TestMode) // 上线的时候要设置为ReleaseMode
gin.DisableConsoleColor()
gin.DefaultWriter = log.Out
// gin.DefaultWriter = io.MultiWriter(log.Out)
// 设置日志级别
// Logrus有七个日志级别:Trace, Debug, Info, Warning, Error, Fataland Panic。
log.Level = logrus.DebugLevel
return
}
func shopIndexHandler(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"code": 0,
"msg": "/s/index",
})
}
func main() {
err := initLogrus()
if err != nil {
panic(err)
}
// 启动默认驱动
r := gin.Default()
shoppingGroup := r.Group("/s")
{
shoppingGroup.GET("/index", shopIndexHandler)
}
// 启动webserver
r.Run(":9000")
}
Cookie和Session
Cookie
Cookie的由来
HTTP协议存在一个问题,它是无状态的。
无状态的意思是每次请求都是独立的,它的执行情况和结果与前面的请求和之后的请求都无直接关系,它不会受前面的请求响应情况直接影响,也不会直接影响后面的请求响应情况。
状态可以理解为客户端和服务器在某次会话中产生的数据,那无状态的就以为这些数据不会被保留。会话中产生的数据又是我们需要保存的,也就是说要“保持状态”。因此Cookie就是在这样一个场景下诞生。
Cookie是什么
在 Internet 中,Cookie 实际上是指小量信息,是由 Web 服务器创建的,将信息存储在用户计算机上(客户端)浏览器的数据文件只能4k大小。一般网络用户习惯用其复数形式 Cookies,指某些网站为了辨别用户身份、进行 Session 跟踪而存储在用户本地终端上的数据,而这些数据通常会经过加密处理。
Cookie的机制
Cookie是由服务器端生成,发送给User-Agent(一般是浏览器),浏览器会将Cookie的key/value保存到某个目录下的文本文件内,下次请求同一网站时就发送该Cookie给服务器(前提是浏览器设置为启用cookie)。Cookie名称和值可以由服务器端开发自己定义,这样服务器可以知道该用户是否是合法用户以及是否需要重新登录等,服务器可以设置或读取Cookies中包含信息,借此维护用户跟服务器会话中的状态以及信息。
Cookie的特点
Cookie是保存在浏览器端的键值对(保存在客户机上的文本数据)
Cookie是标识请求的,弥补HTTP请求是无状态的! 自定义页面效果、登录、购物车
浏览器发送请求的时候,自动把携带该站点之前存储的Cookie信息。
Cookie数据可以配置过期时间,过期的Cookie数据会被系统清除,server端可以设置Cookie数据。
Cookie是针对单个域名的,不同域名之间的Cookie是独立的。
gin框架操作Cookie
package main
import (
"fmt"
"github.com/gin-gonic/gin"
"net/http"
)
type UserInfo struct {
Username string `form:"username"`
Password string `form:"password"`
}
func loginHandler(c *gin.Context){
if c.Request.Method == "POST"{
// 跳转回之前访问的url,没有就跳转回/index
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 == "test" && u.Password == "123"{
// 登录成功
// 1. 设置cookie
c.SetCookie("username", u.Username, 20, "/", "127.0.0.1", false, true)
// 跳转到index页面
c.Redirect(http.StatusFound, toPath)
} else {
// 密码错误
c.HTML(http.StatusOK, "login.html", gin.H{
"err": "用户名或者密码错误",
})
return
}
}else {
c.HTML(http.StatusOK, "login.html", nil)
}
}
func indexHandler(c *gin.Context){
c.HTML(http.StatusOK, "index.html", nil)
}
func vipHandler(c *gin.Context){
tmpUsername, ok := c.Get("username")
if !ok {
// 如果取不到值,说明前面的中间件出问题了
c.Redirect(http.StatusFound, "login")
return
}
username, ok := tmpUsername.(string) // 断言
if !ok {
// 类型断言失败
c.Redirect(http.StatusFound, "/login")
return
}
c.HTML(http.StatusOK, "vip.html", gin.H{
"username": username,
})
}
// 基于cookie实现用户登录认证的中间件
func cookieMiddleware(c *gin.Context){
// 在返回页面之前要先校验是否登录过,是否有username的cookie
// 获取cookie
username, err := c.Cookie("username")
if err != nil{
// 没用cookie直接跳转登录页
toPath := fmt.Sprintf("%s?next=%s", "login", c.Request.URL.Path)
c.Redirect(http.StatusFound, toPath)
return
}
// 用户有cookie
c.Set("username", username) // 在上下文中设置一个键值对
c.Next() // 继续后续的处理函数
}
func main() {
// 启动默认驱动
r := gin.Default()
r.LoadHTMLGlob("templates/*")
r.Any("/login", loginHandler)
r.GET("/index", indexHandler)
r.GET("/vip", cookieMiddleware, vipHandler)
// 启动webserver
r.Run(":9000")
}
html文件
index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>index</title>
</head>
<body>
<h1>index页面不需要登录就能看</h1>
</body>
</html>
vip.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>vip</title>
</head>
<body>
<h1>欢迎vip用户:{{.username}}, 登录才能看</h1>
</body>
</html>
login.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>login</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: crimson">{{ .err }}</p>
</body>
</html>
Session
Cookie虽然在一定程度上解决了“保持状态”的需求,但是由于Cookie本身最大支持4096字节,以及Cookie本身保存在客户端,可能被拦截或窃取,因此就需要有一种新的东西,它能支持更多的字节,并且他保存在服务器,有较高的安全性。这就是Session。
Session必须依赖于Cookie才能使用
用户登陆成功之后,我们在服务端为每个用户创建一个特定的session和一个唯一的标识,它们一一对应。其中:
- Session是在服务端保存的一个数据结构,用来跟踪用户的状态,这个数据可以保存在集群、数据库、文件中;
- 唯一标识通常称为Session ID会写入用户的Cookie中。
这样该用户后续再次访问时,请求会自动携带Cookie数据(其中包含了Session ID),服务器通过该Session ID就能找到与之对应Session数据,也就知道来人的数据信息。
Session和Cookie相比的优势:
- 数据量不受限
- 数据时保存在服务端,是相对安全的
- 但是需要后端维护一个Session服务
- Session是保存在服务端的键值对(内存、关系型数据库MySQL、Redis、文件)
gin框架版Session中间件
session.go 可以添加其它方式存储session
package ginsession
import (
"fmt"
"github.com/gin-gonic/gin"
)
const (
SessionCookieName = "session_id" // session_id在Cookie中对应的key
SessionContextName = "session" // session data在gin上下文中对应的key )
)
type Option struct {
MaxAge int
Path string
Domain string
Secure bool
httpOnly bool
}
type SessionData interface {
GetID() string // 返回ID
Get(key string) (value interface{}, err error)
Set(key string, value interface{})
Del(key string)
Save() // 保存
SetExpire(int) // 设置过期时间
}
// Mgr 所有类型的大仓库都应该遵循的接口
type Mgr interface {
Init(addr string, options ...string) // 所有支持的后端都必须实现Init执行连接
GetSessionData(sessionID string) (sd SessionData, err error)
CreateSession() (sd SessionData)
}
func InitMgr(name string, addr string, options ...string) {
switch name {
case "memory":
MgrObj = NewMemoryMgr()
case "redis":
MgrObj = NewRedisMgr()
}
MgrObj.Init(addr, options)
}
// 实现一个gin框架中间件
// 所有流经这个中间件的请求,它的上下文中肯定会有一个session -> session data
func SessionMiddleware(mgrObj Mgr, option *Option) gin.HandlerFunc {
if mgrObj == nil {
panic("must call InitMgr before use it.")
}
return func(c *gin.Context) {
// 1. 从请求的Cookie中获取session_id
var sd SessionData
sessionID, err := c.Cookie(SessionContextName)
fmt.Println(sessionID)
if err != nil {
// 1.1取不到session_id -> 给这个新用户创建一个新的session data,同时分配一个session_id
sd = mgrObj.CreateSession()
sessionID = sd.GetID()
fmt.Println("取不到session_id, 创建一个新的", sessionID)
} else {
// 1.2 取到session_id
// 2.根据取到的session_id去Session大仓库中取对应的session data
sd, err = mgrObj.GetSessionData(sessionID)
if err != nil {
// 2.1 根据用户传来的session_id在大仓库中取不到session data
sd = mgrObj.CreateSession()
// 2.2 更新用户cookie中保存的那个session_id
sessionID = sd.GetID()
fmt.Println("session_id取不到session data, 分配一个新的", sessionID)
}
fmt.Println("session_id未过期", sessionID)
}
sd.SetExpire(option.MaxAge) // 设置session data过期时间
// 3.如何实现让后续所有的处理请求的方法都能拿到session data
// 3.1 利用gin的c.Set("session", session data)
c.Set(SessionContextName, sd) // 保存到上下文
// 在gin框架中,要回写Cookie必须在处理请求的函数返回之前
c.SetCookie(SessionCookieName, sessionID, option.MaxAge, option.Path, option.Domain, option.Secure, option.httpOnly)
c.Next() // 执行后续的请求处理方法 c.HTML()时已经把响应头写好了
}
}
内存版
package ginsession
import (
"fmt"
"sync"
uuid "github.com/satori/go.uuid"
)
// 内存版Session服务
// 仅供练习使用
type MemSD struct {
ID string
Data map[string]interface{}
rwlock sync.RWMutex // 读写锁,锁的是上面的Data
// 过期时间
}
func (m *MemSD) GetID() string {
return m.ID
}
// 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) {
m.rwlock.Lock()
defer m.rwlock.Unlock()
delete(m.Data, key)
}
//Save 保存session data
func (m *MemSD) Save() {
return
}
func (m *MemSD) SetExpire(expirded int) {
return
}
type MemoryMgr struct {
Session map[string]SessionData
rwLock sync.RWMutex
}
// NewMemoryMgr 内存版session大仓库的构造函数
func NewMemoryMgr() Mgr {
return &MemoryMgr{
Session: make(map[string]SessionData, 1024),
}
}
func NewMemorySessionData(id string) SessionData {
return &MemSD{
ID: id,
Data: make(map[string]interface{}, 8),
}
}
func (m *MemoryMgr) Init(addr string, options ...string) {
return
}
// GetSessionData根据传进来的SessionID找到对应的SessionData
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("invalid session id")
return
}
return
}
// 创建一个session记录
func (m *MemoryMgr) CreateSession() (sd SessionData) {
// 1.造一个sessionID
uuidObj := uuid.NewV4()
// 2.对应它的sessionData
sd = NewMemorySessionData(uuidObj.String())
// 把新创建的session data保存到大仓库中
m.Session[sd.GetID()] = sd
// 3.返回SessionData
return
}
redis版
package ginsession
import (
"encoding/json"
"fmt"
"strconv"
"sync"
"time"
"github.com/go-redis/redis"
uuid "github.com/satori/go.uuid"
)
// redis版Session服务
type RedisSD struct {
ID string
Data map[string]interface{}
rwLock sync.RWMutex // 读写锁,锁上面的Data
expired int // 过期时间
client *redis.Client // redis连接池
}
func NewRedisSessionData(id string, client *redis.Client) SessionData {
return &RedisSD{
ID: id,
Data: make(map[string]interface{}, 8),
client: client,
}
}
func (r *RedisSD) GetID() string {
return r.ID
}
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() {
// 将最新的session data保存到redis中
value, err := json.Marshal(r.Data)
if err != nil {
// 序列化session data 失败
fmt.Printf("marshal session data failed, err:%v\n", err)
return
}
// 将数据保存到redis
r.client.Set(r.ID, value, time.Second*time.Duration(r.expired))
}
func (r *RedisSD) SetExpire(expired int) {
r.expired = expired
}
type RedisMgr struct {
Session map[string]SessionData
rwLock sync.RWMutex
client redis.Client // redis连接池
}
// NewRedisMgr RedisMgr的构造函数
func NewRedisMgr() Mgr {
return &RedisMgr{
Session: make(map[string]SessionData, 1024),
}
}
func (r *RedisMgr) Init(addr string, options ...string) {
// 初始化Redis连接
var (
password string
db string
)
if len(options) == 1 {
password = options[0]
} else if len(options) == 2 {
password = options[0]
db = options[1]
}
dbValue, err := strconv.Atoi(db)
if err != nil {
dbValue = 0
}
r.client = redis.NewClient(&redis.Options{
Addr: addr,
Password: password,
DB: dbValue,
})
_, err = r.client.Ping().Result()
if err != nil {
panic(err)
}
}
func (r *RedisMgr) loadFromRedis(sessionID string) (err error) {
// 1.连接redis
value, err := r.client.Get(sessionID).Result()
if err != nil {
// redis中没用该session_id对应的session data
return
}
err = json.Unmarshal([]byte(value), &r.Session)
if err != nil {
// 从redis取出的数据反序列化失败
return
}
// 2.根据sessionID找到对应的数据 GetSessionData
// 3.把数据取出来反序列化到r.data
return
}
// GetSessionData 获取sessionID对应的sessionData
func (r *RedisMgr) GetSessionData(sessionID string) (sd SessionData, err error) {
// 1. r.Session中必须已经从Redis里面加载出来数据
if r.Session == nil {
err := r.loadFromRedis(sessionID)
if err != nil {
return nil, err
}
}
r.rwLock.RLock()
defer r.rwLock.RUnlock()
// 2. r.Session[sessionID] 拿到对应的session data
sd, ok := r.Session[sessionID]
if !ok {
err = fmt.Errorf("invalid session id")
return
}
return
}
func (r *RedisMgr) CreateSession() (sd SessionData) {
// 1. 造一个sessionID
uuidObj := uuid.NewV4()
// 2.造一个和它对应的SessionData
sd = NewRedisSessionData(uuidObj.String(), r.client)
r.Session[sd.GetID()] = sd // 把新创建的session data保存收到大仓库中
return
// 3.返回SessionData
}