一、Cookie和Session的由来

我们知道HTTP协议无连接的, 也就是不保存用户的状态信息。

早期(十几年前)的网页是静态的, 数据都是写死的, 人们访问网页只是用来查看新闻的, 没有保存用户状态的需求。

而往后出现了像论坛、博客、网购这一类需要保存用户信息的网站, 如果网站不保存用户的状态信息, 意味着用户每次访问都需要重新输入用户名和密码, 这无疑对用户的体验是极其不好的。

于是, 就出现了会话跟踪技术, 我们可以把它理解为客户端与服务端之间的一次会晤, 一次会晤包含的多次请求与响应, 每次请求都带着请求参数, 比如请求登入的请求参数是用户名和密码, 服务端就会拿着请求参数与数据库去比对, 找到相应的用户信息。

如何实现会话跟踪 : 在HTTP协议中可以使用Cookie来完成, 在Web开发中可以使用Session来完成

  • Cookie是存在浏览器中的键值对, 每次发送请求都携带者参数, 但是容易被截获, 不安全
  • 于是就出现了Session, 它是存在于服务端的键值对, key为随机字符串, 安全性提高了, 但所有的数据都存在服务器中, 服务器的压力很大
  • 之后便产生了Token的概念, 服务端签发加密后的字符串给客户端浏览器保存, 客户端每次请求携带用户名和密码, 并加上由服务端签发的用户名和密码加密的字符串, 服务端收到请求后再对用户名密码加密, 与后面携带的密文对比, 由于它也是保存在客户端浏览器上的, 所以也叫Cookie

二、Cookie简介

1. 什么是Cookie

  • Cookie是服务器保存在客户端浏览器之上的key-value键值对 : username='jarvis';password="123"
  • 它是随着服务器的响应发送给客户端, 客户端将其保存, 下一次请求时会将Cookie放在其中, 服务器通过识别Cookie就能知道是哪个客户端浏览器

2. Cookie规范

  • Cookie大小上限为4KB
  • 一个服务器最多在客户端浏览器上保存20个Cookie
  • 一个浏览器最多保存300个Cookie

上面是HTTP中Cookie的规范, 现在浏览器的竞争, 有些Cookie大小能打到8KB, 最多可以保存500个Cookie

不同浏览器之间的Cookie是不共享的

3. 安全性

  • Cookie保存在浏览器本地, 意味着很容易被窃取和篡改

4. Cookie 关键配置

在使用 Cookie 的时候,要注意“安全使用”。

  • Domain: 也就是 Cookie 可以用在什么域名下,按照最小化原则来设定。
  • Path: Cookie 可以用在什么路径下,同样按照最小化原则来设定。
  • Max-AgeExpires: 过期时间,只保留必要时间。
  • Http-Only: 设置为 true 的话,那么浏览器上的 JS 代码将无法使用这个 Cookie。永远设置为 true。
  • Secure: 只能用于 HTTPS 协议,生产环境永远设置为 true。
  • SameSite: 是否允许跨站发送 Cookie,尽量避免。

在Go语言中,标准库net/http提供了用于处理HTTP请求和响应的功能,包括处理Cookie的相关功能。可以通过http.Cookie{}查看。

对应解释如下:

type Cookie struct {
Name string // Cookie的名称,用于唯一标识一个Cookie
Value string // Cookie的值,存储在Cookie中的具体数据 Path string // Cookie的路径,指定了哪些路径下的页面可以访问该Cookie
Domain string // Cookie的域,指定了哪些域名下可以访问该Cookie
Expires time.Time // Cookie的过期时间,表示Cookie在何时之前有效
RawExpires string // 仅用于读取Cookie,保存未解析的过期时间信息 // MaxAge=0 表示没有指定'Max-Age'属性
// MaxAge<0 表示立即删除Cookie,相当于 'Max-Age: 0'
// MaxAge>0 表示'Max-Age'属性存在,并以秒为单位给出
MaxAge int Secure bool // Secure属性,指定是否只在使用HTTPS协议时才发送Cookie
HttpOnly bool // HttpOnly属性,设置为true时,禁止通过JavaScript访问该Cookie SameSite SameSite // SameSite属性,控制是否允许跨站发送Cookie Raw string // 保存未解析的原始Cookie字符串
Unparsed []string // 保存未解析的属性-值对文本
}

三、Session简介

1. 什么是Session

  • 存放在服务器上的键值对 : {'weqweq':{'username':'jarvis','password':'123'}}
  • Cookie可以保存状态, 但本身最大只能支持4069字节, 并且不安全, 于是就出现了Session
  • 它能支持更多字节, 并且保存在服务端上, 具有较高的安全性,Session基于Cookie, 本地存放服务器返回给浏览器的随机字符串
  • 客户端浏览器请求中携带随机字符串(session_id), 服务端收到后与数据库中存储的session做对比

ps : session存储的方式多种多样, 可以是数据库、缓存、硬盘等等

2. Session 安全性

Session 关键在于服务器要给浏览器一个 sess_id,也就是 Session 的 ID。

后续每一次请求都带上这个 Session ID,服务端就知道你是谁了。

但是后端服务器是“只认 ID 不认人”的。也就是说,如果攻击者拿到了你的 ID,那么服务器就会把攻击者当成你。在下图中,攻击者窃取到了 sess_id,就可以冒充是你了。

3. 如何让客户端携带 sess_id

因为 sess_id 是标识你身份的东西,所以你需要在每一次访问系统的时候都带上。

  • 最佳方式就是用 Cookie,也就是 sess_id 放到 Cookie 里面。sess_id 自身没有任何敏感信息,所以放 Cookie 也可以。
  • 也可以考虑放 Header,比如说在 Header 里面带一个 sess_id。这就需要前端的研发记得在 Header 里面带上。
  • 还可以考虑放查询参数,也就是 ?sess_id=xxx

理论上来说还可以放 body,但是基本没人这么干。在一些禁用了 Cookie 功能的浏览器上,只能考虑后两者。

在浏览器上,你可以通过插件 cookieEditor 来查看某个网站的 Cookie 信息。

四、使用 Gin 的 Session 插件

4.1 介绍

Gin框架本身并不内置对Session的支持,但你可以使用第三方的Session中间件来实现。其中比较常用的是 github.com/gin-contrib/sessions

4.2 基本使用

首先,确保你已经安装了Session中间件:

go get github.com/gin-contrib/sessions

然后在你的Gin应用程序中使用它。以下是一个基本的使用示例:

package main

import (
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/cookie"
"github.com/gin-gonic/gin"
"net/http"
"time"
) func main() {
// 创建Gin引擎
r := gin.Default() // 使用Cookie存储Session
store := cookie.NewStore([]byte("secret"))
r.Use(sessions.Sessions("mysession", store)) // 路由:设置Session
r.GET("/set", func(c *gin.Context) {
// 获取默认Session
session := sessions.Default(c) // 设置Session中的键值对
session.Set("user", "example@example.com")
session.Set("expires", time.Now().Add(24*time.Hour).Unix()) // 保存Session
session.Save() // 返回JSON响应
c.JSON(http.StatusOK, gin.H{"message": "Session set successfully"})
}) // 路由:获取Session
r.GET("/get", func(c *gin.Context) {
// 获取默认Session
session := sessions.Default(c) // 从Session中获取键值对
user := session.Get("user")
expires := session.Get("expires") // 返回JSON响应
c.JSON(http.StatusOK, gin.H{"user": user, "expires": expires})
}) // 启动Gin应用,监听端口8080
r.Run(":8080")
}

在上述示例中,我们使用github.com/gin-contrib/sessions/cookie包创建了一个基于Cookie的Session存储。路由"/set"用于设置Session,路由"/get"用于获取Session。请注意,这里的Session数据是存储在客户端的Cookie中的,因此在实际应用中需要注意安全性。

五、 session与store

在Web应用中,会话(session)是一种用于在不同请求之间存储和共享用户信息的机制。通常,会话用于跟踪用户的身份验证状态、存储用户首选项和其他与用户相关的数据。在Gin框架中,会话的管理通常通过sessionstore两个概念来完成。

5.1 会话(Session)

  • 概念: 会话是在服务器端存储用户状态的一种机制。每个用户访问网站时,服务器都会为其创建一个唯一的会话标识符,该标识符存储在用户的浏览器中,通常通过Cookie来实现。服务器可以根据这个标识符来识别用户,并在多个请求之间共享用户的状态信息。
  • 作用: 主要用于存储用户的身份验证状态、用户的首选项、购物车内容等用户相关的信息,以便在用户访问不同页面或进行不同请求时能够保持一致的用户状态。

5.2 存储(Store)

  • 概念: 存储是用于实际存储和检索会话数据的地方。存储可以是内存、数据库、文件系统等,具体取决于应用程序的需求。存储负责维护会话数据的持久性和安全性。
  • 作用: 存储的主要作用是提供对会话数据的 CRUD(Create、Read、Update、Delete)操作。当用户发起请求时,存储会根据会话标识符检索相应的会话数据,服务器可以通过存储来实现会话的管理。

在Gin框架中,常用的Session中间件是 github.com/gin-contrib/sessions,而存储则可以选择不同的后端,例如使用Cookie、Redis、内存等。

在Gin应用中引入该中间件,通过创建Session对象和设定存储引擎,可以方便地进行Session的处理。以下是一个基本的使用示例:

package main

import (
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/cookie"
"github.com/gin-gonic/gin"
"net/http"
) func main() {
r := gin.Default() // 使用Cookie存储Session
store := cookie.NewStore([]byte("secret"))
r.Use(sessions.Sessions("mysession", store)) r.GET("/set", func(c *gin.Context) {
// 设置Session
session := sessions.Default(c)
session.Set("user", "example@example.com")
session.Save() c.JSON(http.StatusOK, gin.H{"message": "Session set successfully"})
}) r.GET("/get", func(c *gin.Context) {
// 获取Session
session := sessions.Default(c)
user := session.Get("user") c.JSON(http.StatusOK, gin.H{"user": user})
}) r.Run(":8080")
}
  • cookie.NewStore([]byte("secret")) 在上述示例中,使用了Cookie存储。cookie.NewStore 创建了一个基于Cookie的存储引擎,使用了一个密钥来加密Cookie中的会话数据。
  • 其他存储引擎: 除了Cookie存储,github.com/gin-contrib/sessions 还支持其他存储引擎,例如Redis、文件系统等。可以根据实际需求选择合适的存储引擎。
// 使用Redis存储Session的例子
import (
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/redis"
"github.com/gin-gonic/gin"
) func main() {
r := gin.Default() // 使用Redis存储Session
store, _ := redis.NewStore(10, "tcp", "localhost:6379", "", []byte("secret"))
r.Use(sessions.Sessions("mysession", store)) // ...
}

在实际项目中,选择存储引擎时要考虑性能、可扩展性和持久性等因素。 Cookie 存储适用于小型应用,而对于大型应用,可能需要选择支持分布式的存储引擎,如 Redis。

总体而言,使用Gin的Session中间件能够方便地管理会话,而不同的存储引擎则提供了灵活的选择,以适应不同的应用场景。

六、小黄书实践

目录结构

├── docker-compose.yaml         # Docker Compose配置文件,定义了整个应用的容器化部署
├── go.mod # Go模块文件,用于管理项目的依赖项
├── go.sum # Go模块文件,包含依赖项的版本信息
├── internal # 内部模块,包含应用程序的业务逻辑
│   ├── domain # 领域层,包含领域对象和领域逻辑
│   │   └── user.go # 用户领域对象的定义
│   ├── repository # 数据仓库层,用于与数据库交互
│   │   ├── dao # 数据访问对象,与数据库的交互接口
│   │   │   ├── init.go # 数据库初始化
│   │   │   └── user.go # 用户数据访问对象的定义
│   │   └── user.go # 用户数据仓库接口的定义
│   ├── service # 服务层,包含业务逻辑的实现
│   │   └── user.go # 用户服务的实现
│   └── web # Web层,处理HTTP请求和路由
│   ├── middleware # 中间件,用于处理HTTP请求的中间逻辑
│   │   └── login.go # 登录中间件的定义
│   ├── user.go # 用户相关的HTTP处理函数和路由定义
│   └── user_test.go # 用户相关的测试文件
├── main.go # 主程序入口,应用程序的启动文件
├── pkg # 外部可导出的包,供其他应用程序或模块使用
├── script # 脚本目录,包含各种初始化和辅助脚本
   └── mysql # MySQL初始化脚本目录
   └── init.sql # 数据库初始化SQL脚本

internal/domain/user.go 文件代码:

package domain

import "time"

// User 领域对象,是DDD 中的 entity
type User struct {
Id int64
Email string
Password string
Ctime time.Time
}

internal/repository/user.go

package repository

import (
"context"
"webook/internal/domain"
"webook/internal/repository/dao"
) var (
ErrUserDuplicateEmail = dao.ErrUserDuplicateEmail
ErrUserNotFound = dao.ErrUserNotFound
) type UserRepository struct {
dao *dao.UserDAO
} func NewUserRepository(dao *dao.UserDAO) *UserRepository {
return &UserRepository{
dao: dao,
}
} func (r *UserRepository) Create(ctx context.Context, u domain.User) error {
return r.dao.Insert(ctx, dao.User{
Email: u.Email,
Password: u.Password,
})
}
func (r *UserRepository) FindByEmail(ctx context.Context, email string) (domain.User, error) {
u, err := r.dao.FindByEmail(ctx, email)
if err != nil {
return domain.User{}, err
}
return domain.User{
Id: u.Id,
Email: u.Email,
Password: u.Password,
}, nil
}

internal/repository/dao/init.go

package dao

import "gorm.io/gorm"

func InitTable(db *gorm.DB) error {
return db.AutoMigrate(&User{})
}

internal/repository/dao/user.go

package dao

import (
"context"
"errors"
"github.com/go-sql-driver/mysql"
"gorm.io/gorm"
"time"
) var (
ErrUserDuplicateEmail = errors.New("邮箱冲突")
ErrUserNotFound = gorm.ErrRecordNotFound
) type UserDAO struct {
db *gorm.DB
} func NewUserDAO(db *gorm.DB) *UserDAO {
return &UserDAO{
db: db,
}
}
func (dao *UserDAO) Insert(ctx context.Context, u User) error {
// 存毫秒数
now := time.Now().UnixNano()
u.Ctime = now
u.Utime = now
err := dao.db.WithContext(ctx).Create(&u).Error
// 类型断言,判断是否是mysql的唯一冲突错误
if mysqlErr, ok := err.(*mysql.MySQLError); ok {
const uniqueConflictsErrNo uint16 = 1062
if mysqlErr.Number == uniqueConflictsErrNo {
// 邮箱已存在
return ErrUserDuplicateEmail
}
}
return err
}
func (dao *UserDAO) FindByEmail(ctx context.Context, email string) (User, error) {
var u User
err := dao.db.WithContext(ctx).Where("email = ?", email).First(&u).Error
return u, err
} // User 直接对应数据库中的表
// 有些人叫做entity,有些人叫做model
type User struct {
Id int64 `gorm:"primaryKey,autoIncrement"`
// 全部用户唯一
Email string `gorm:"unique"`
Password string // 创建时间,毫秒数,解决时区问题
Ctime int64
// 更新时间
Utime int64
}

internal/service/user.go

package service

import (
"context"
"errors"
"golang.org/x/crypto/bcrypt"
"webook/internal/domain"
"webook/internal/repository"
"webook/internal/repository/dao"
) var (
ErrUserDuplicateEmail = dao.ErrUserDuplicateEmail
ErrInvalidUserOrPassword = errors.New("账号/邮箱或密码不对")
) type UserService struct {
repo *repository.UserRepository
} func NewUserService(repo *repository.UserRepository) *UserService {
return &UserService{
repo: repo,
}
} func (svc *UserService) SignUp(ctx context.Context, u domain.User) error {
// 先加密密码
hash, err := bcrypt.GenerateFromPassword([]byte(u.Password), bcrypt.DefaultCost)
if err != nil {
return err
}
u.Password = string(hash)
// 然后就是,存起来
return svc.repo.Create(ctx, u)
} func (svc *UserService) Login(ctx context.Context, email, password string) (domain.User, error) {
// 先找用户
u, err := svc.repo.FindByEmail(ctx, email)
if err == repository.ErrUserNotFound {
return domain.User{}, ErrInvalidUserOrPassword
}
if err != nil {
return domain.User{}, err
}
// 比较密码了
err = bcrypt.CompareHashAndPassword([]byte(u.Password), []byte(password))
if err != nil {
// DEBUG
return domain.User{}, ErrInvalidUserOrPassword
}
return u, nil
}

internal/web/user.go

package web

import (
regexp "github.com/dlclark/regexp2"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
"net/http"
"webook/internal/domain"
"webook/internal/service"
) type UserHandler struct {
svc *service.UserService
emailExp *regexp.Regexp
passwordExp *regexp.Regexp
} func NewUserHandler(svc *service.UserService) *UserHandler {
const (
emailRegexPattern = "^\\w+([-+.]\\w+)*@\\w+([-.]\\w+)*\\.\\w+([-.]\\w+)*$"
passwordRegexPattern = `^(?=.*[A-Za-z])(?=.*\d)(?=.*[$@$!%*#?&])[A-Za-z\d$@$!%*#?&]{8,}$`
)
emailExp := regexp.MustCompile(emailRegexPattern, regexp.None)
passwordExp := regexp.MustCompile(passwordRegexPattern, regexp.None)
return &UserHandler{
svc: svc,
emailExp: emailExp,
passwordExp: passwordExp,
}
} func (u *UserHandler) RegisterRoutes(server *gin.Engine) {
ug := server.Group("/user") //ug is user group
ug.GET("/profile", u.Profile) // 查询用户信息接口
ug.POST("/signup", u.SignUp) // 注册接口
ug.POST("/login", u.Login) // 登录接口
ug.POST("/logout", u.Logout) // 登出接口
ug.POST("/edit", u.Edit) // 修改用户信息接口 } func (u *UserHandler) RegisterRoutesV1(ug *gin.RouterGroup) {
ug.GET("/profile", u.Profile) // 查询用户信息接口
ug.POST("/signup", u.SignUp) // 注册接口
ug.POST("/login", u.Login) // 登录接口
ug.POST("/logout", u.Logout) // 登出接口
ug.POST("/edit", u.Edit) // 修改用户信息接口 }
func (u *UserHandler) Profile(ctx *gin.Context) {
}
func (u *UserHandler) SignUp(ctx *gin.Context) {
type SignUpRequest struct {
Email string `json:"email"`
Password string `json:"password"`
ConfirmPassword string `json:"confirmPassword"`
}
var request SignUpRequest
// 如果 Bind 方法发现输入有问题,它就会直接返回一 个错误响应到前端。
if err := ctx.Bind(&request); err != nil {
return
} ok, err := u.emailExp.MatchString(request.Email)
if err != nil {
ctx.String(http.StatusOK, "系统错误")
return
}
if !ok {
ctx.String(http.StatusOK, "邮箱格式错误")
return
}
ok, err = u.passwordExp.MatchString(request.Password)
if err != nil {
ctx.String(http.StatusOK, "系统错误")
return
}
if !ok {
ctx.String(http.StatusOK, "密码必须包含至少一个数字、一个字母、一个特殊字符,并且长度至少为8位")
return
}
if request.Password != request.ConfirmPassword {
ctx.String(http.StatusOK, "两次密码不一致")
return
}
// 调用以一下svc 的方法
err = u.svc.SignUp(ctx, domain.User{
Email: request.Email,
Password: request.Password,
})
if err == service.ErrUserDuplicateEmail {
ctx.String(http.StatusOK, "邮箱冲突")
return
}
if err != nil {
ctx.String(http.StatusOK, "系统错误")
return }
ctx.String(http.StatusOK, "注册成功")
}
func (u *UserHandler) Login(ctx *gin.Context) {
type LoginRequest struct {
Email string `json:"email"`
Password string `json:"password"`
}
var request LoginRequest
if err := ctx.Bind(&request); err != nil {
return
}
// 调用以一下svc 的方法
user, err := u.svc.Login(ctx, request.Email, request.Password) if err == service.ErrInvalidUserOrPassword {
ctx.String(http.StatusOK, "账号或密码错误")
return
}
if err != nil {
ctx.String(http.StatusOK, "系统错误")
return
}
// 登录成功设置session
sess := sessions.Default(ctx)
// 设置session的key和value
sess.Set("userId", user.Id)
sess.Save()
ctx.String(http.StatusOK, "登录成功")
return
} func (u *UserHandler) Logout(ctx *gin.Context) { }
func (u *UserHandler) Edit(ctx *gin.Context) { }

main.go

package main

import (
"github.com/gin-contrib/cors"
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/cookie"
"github.com/gin-gonic/gin"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"strings"
"time"
"webook/internal/repository"
"webook/internal/repository/dao"
"webook/internal/service"
"webook/internal/web"
"webook/internal/web/middleware"
) func main() {
db := initDB()
ser := initWebServer()
u := initUser(db)
u.RegisterRoutes(ser)
ser.Run(":8080") } func initWebServer() *gin.Engine {
ser := gin.Default()
ser.Use(func(ctx *gin.Context) {
println("这是一个中间件")
})
ser.Use(func(ctx *gin.Context) {
println("这是二个中间件")
})
ser.Use(cors.New(cors.Config{
//AllowOrigins: []string{"*"},
//AllowMethods: []string{"POST", "GET"},
AllowHeaders: []string{"Content-Type", "Authorization"},
//ExposeHeaders: []string{"x-jwt-token"},
// 是否允许你带 cookie 之类的东西
AllowCredentials: true,
AllowOriginFunc: func(origin string) bool {
if strings.HasPrefix(origin, "http://localhost") {
// 你的开发环境
return true
}
return strings.Contains(origin, "yourcompany.com")
},
MaxAge: 12 * time.Hour,
}))
// 用 cookie 存储 session
store := cookie.NewStore([]byte("secret"))
// 使用 session 中间件
ser.Use(sessions.Sessions("mysession", store))
//ser.Use(middleware.NewLoginMiddlewareBuilder().Build())
//ser.Use(middleware.NewLoginMiddlewareBuilder().
// IgnorePaths("/users/signup").
// IgnorePaths("/users/login").Build())
// v1
middleware.IgnorePaths = []string{"sss"}
ser.Use(middleware.CheckLogin()) // 不能忽略sss这条路径
server1 := gin.Default()
server1.Use(middleware.CheckLogin())
return ser
} func initUser(db *gorm.DB) *web.UserHandler {
ud := dao.NewUserDAO(db)
repo := repository.NewUserRepository(ud)
svc := service.NewUserService(repo)
u := web.NewUserHandler(svc)
return u
}
func initDB() *gorm.DB {
db, err := gorm.Open(mysql.Open("root:root@tcp(localhost:13316)/webook"))
if err != nil {
// panic 相当于 整个 goroutine 结束
panic("failed to connect database")
}
err = dao.InitTable(db)
if err != nil {
panic("failed to init table")
}
return db
}

Gin 框架之Cookie与Session的更多相关文章

  1. Django框架 之 Cookie、Session整理补充

    Django框架 之 Cookie.Session整理补充 浏览目录 Django实现的Cookie Django实现的Session 一.Django实现的Cookie 1.获取Cookie 1 2 ...

  2. Django框架 之 Cookie和Session初识

    Django框架 之 Cookie和Session初识 浏览目录 Cookie介绍 Django中的Cookie Session 一.Cookie介绍 1.Cookie产生的意义 众所周知,HTTP协 ...

  3. Django框架07 /cookie和session

    Django框架07 /cookie和session 目录 Django框架07 /cookie和session 1. django请求生命周期 2. cookie 3. session 4. 总结 ...

  4. Python 之 Django框架( Cookie和Session、Django中间件、AJAX、Django序列化)

    12.4 Cookie和Session 12.41 cookie Cookie具体指的是一段小信息,它是服务器发送出来存储在浏览器上的一组组键值对,下次访问服务器时浏览器会自动携带这些键值对,以便服务 ...

  5. Go语言之高级篇beego框架之cookie与session

    1.cookie的用法 this.Ctx.SetCookie("name", name, maxage, "/") this.Ctx.SetCookie(&qu ...

  6. 4. Beego 框架之cookie与session

    what is cookie? cookie是存储在客户端的,用于标识客户身份的! what is session session 是存储在服务端,也是用于客户身份标识,用于跟踪用户会话. BeeGo ...

  7. Django框架之cookie和session及开发登录功能

    1.cookie是什么? Web应用程序是使用HTTP协议传输数据的.HTTP协议是无状态的协议.一旦数据交换完毕,客户端与服务器端的连接就会关闭,再次交换数据需要建立新的连接.这就意味着服务器无法从 ...

  8. Go语言基础之Cookie和Session

    Cookie和Session Cookie和Session是Web开发绕不开的一个环节,本文介绍了Cookie和Session的原理及在Go语言中如何操作Cookie. Cookie Cookie的由 ...

  9. Go之Cookie和Session

    文章引用自 Cookie和Session Cookie和Session是Web开发绕不开的一个环节,本文介绍了Cookie和Session的原理及在Go语言中如何操作Cookie. Cookie Co ...

  10. cookie和session的那些事

    对于经常网购的朋友来说,经常会遇到一种情况: 打开淘宝或京东商城的首页,输入个人账号和密码进行登陆,然后进行购物,支付等操作都不需要用户再次输入用户名和密码 但是如果用户换一个浏览器或者等几个小时后再 ...

随机推荐

  1. 限时促销,火山引擎 ByteHouse 为企业带来一波数智升级福利!

    更多技术交流.求职机会,欢迎关注字节跳动数据平台微信公众号,回复[1]进入官方交流群 面对庞杂的海量数据,稳定高速的实时数据处理能力,成为了当下企业数智升级过程中备受关注的点. ByteHouse 是 ...

  2. PPT 商务报告,如何去表现客户LOGO

    PPT 商务报告,如何去表现客户LOGO LOGO 如何下载 LOGO 如何展示 矩阵排列 删除背景,变成白色 删除背景 设置透明度 AI 软件做成矢量图 LOGO 转色法

  3. BBS项目(二):首页导航条搭建 修改密码功能 首页数据展示 个人站点页面

    目录 首页导航条搭建 session判断用户是否登录 更换验证码 导航条更多功能 退出系统 修改密码前端页面 使用大模态框 编写模态框 前端表单编写 修改密码后端逻辑 接受参数 验证参数 修改密码 首 ...

  4. 神经网络优化篇:详解动量梯度下降法(Gradient descent with Momentum)

    动量梯度下降法 还有一种算法叫做Momentum,或者叫做动量梯度下降法,运行速度几乎总是快于标准的梯度下降算法,简而言之,基本的想法就是计算梯度的指数加权平均数,并利用该梯度更新的权重. 例如,如果 ...

  5. 微信公众号短链实时阅读量、点赞数爬虫(不会Hook可用)

    众所周知,微信分享的公众号分享出的一般都是短链,在这个锻炼下使用浏览器打开并不能获取微信公众的阅读量点赞数等这些信息,如图1所示. 但是实际拥有详细信息的则是这个链接下面,提取链接所需要提交的信息包括 ...

  6. 42 干货系列从零用Rust编写负载均衡及代理,wmproxy中配置tcp转websocket

    wmproxy wmproxy已用Rust实现http/https代理, socks5代理, 反向代理, 静态文件服务器,四层TCP/UDP转发,七层负载均衡,内网穿透,后续将实现websocket代 ...

  7. uniapp#实现自定义省市区三级联动

    uni-APP中的三级联动(省市区)---数据前端写死 https://blog.csdn.net/lwaner/article/details/107150805 uniapp#实现自定义省市区三级 ...

  8. 假如有一个需求,我们要在一个页面中 ul 标签里渲染 **十万** 个 li 标签

    1 // 插入十万条数据 2 const total = 100000; 3 let ul = document.querySelector('ul'); // 拿到 ul 4 5 // 懒加载的思路 ...

  9. d3过滤

    1 <!DOCTYPE html> 2 <html lang="en"> 3 <head> 4 <meta charset="U ...

  10. 如何通过canvas实现电子签名

    想要实现一个电子签名,可以支持鼠标签名,还能类似书法效果线条有粗有细,同时可以导出成图片. 一.实现连贯的划线 1)首先需要注册鼠标下压.鼠标放开.鼠标移出和鼠标移动事件,通过鼠标下压赋值downFl ...