GO语言登录模块-Cookie与Session

851 阅读8分钟

该系列是专门为新人入门准备的文章,高手就没有必要再看了。

登录校验、授权与凭证

登录校验

常见的方式:

  1. 用户名与密码
  2. 邮箱回调链接
  3. 手机号或者邮箱接收验证码
  4. 扫码登录(共享登录态)

授权

常见的授权场景:

  1. 新安装的APP会需要授权(摄像头,网络,广告追踪等权限)
  2. 使用微信小程序时,需要授权微信的一些个人信息
  3. QQ 微信 支付宝等扫码登录某些网站时,会需要进行授权。

凭证

确认并且记录了用户的某些信息,并确保不会被盗用。比如:身份证,社保卡,驾照等等。

在互联网中常见的凭证主要是用来记录用户登录态,并且采集一些用户的行为记录。常用的方案有: cookie session token oauth

Cookie

我们先讲下 HTTP是无状态协议的具体问题。HTTP无状态是说,客户端和服务端双方都不会保存之前的请求,完全不考虑双方的身份问题。即服务端对于涌过来的请求根本不分辨也没有必要分辨是哪个客户端,哪个用户,哪个机器发过来的。

HTTP协议一开始就是为了访问静态资源而设计的,有没有状态关系不大。这样的好处是结构简单,性能可靠;缺点就是在某些场景里,无法满足业务的需要(如果从软件开发的角度看,这应该不算缺点)。因此,搞出了一个通用方案,也可以认为是大家公认的补充协议 cookie

我们常用的邮件协议 SMTP 是有状态协议,大家可以看下它的请求流程。网络分层中大多数协议都是有状态的。 另外这里还要区分下http1.Xhttp2协议的区别。http2 理论上是可以有状态的。

工作流程

  1. Client发送HTTP请求给 Server;
  2. Server响应,并附带Set-Cookie头部信息
  3. Client保存Cookie,之后请求Server会附带Cookie的头部信息
  4. Server从Cookie中获取相关信息,进而确定Client的身份。

字段说明

//Gin 框架中设置Cookie的方法
func (c *Context) SetCookie(name, value string, maxAge int, path, domain string, secure, httpOnly bool) {
	if path == "" {
		path = "/"
	}
	http.SetCookie(c.Writer, &http.Cookie{
		Name:     name,
		Value:    url.QueryEscape(value),
		MaxAge:   maxAge,
		Path:     path,
		Domain:   domain,
		SameSite: c.sameSite,
		Secure:   secure,
		HttpOnly: httpOnly,
	})
}
  1. NameValue键值对,设置Cookie的名称及相对应的值,都必须是字符串类型,会对具体的值进行编码。
  2. MaxAge 失效的时间,单位秒,默认为 -1。具体含义:
    • 整数,则该Cookie在maxAge秒后失效。
    • 负数,该Cookie为临时Cookie,关闭浏览器后立即失效。
    • 0,表示删除该Cookie。
  3. Path 指定Cookie在哪个路径(路由)下生效。默认是”/“,意思是当前Domain下的所有路径。 如果设置为/index,则只有/index下的路由可以获取到该Cookie。比如:/index/title。
  4. Domain 指定Cookie所属域名,默认是当前域名。注意:”localhost“ 和 ”127.0.0.1“是完全不同的,你在”localhost“下设置的Cookie,不会被”127.0.0.1“访问到。
  5. Secure 该Cookie是否仅被使用安全协议传输。当Secure值为true时,Cookie在Http中是无效,在Https中才有效。引申:Http和Https的区别。
  6. HttpOnly 默认为False。 如果设置为True,则无法通过JS获取Cookie的值,但仍然可以通过Application手动修改Cookie。一定程度上能防止XSS攻击。引申,什么是XSS攻击。
  7. SameSite 可以防止第三方站点使用Cookie。详情可以查下官方说明,或者自己实现下。

特性

有哪些毛病

  1. Cookie存储在客户端。很显然,不太安全,而且没法存很多东西。
  2. Cookie不可跨域。一级域名(juejin.com)和二级域名(info.juejin.com)之间是允许共享使用的。

引申,可以了解一下什么是跨域,以及跨域有啥问题。

  1. 有一个非常尴尬的问题:授权。一般情况下,网站需要先申请操作Cookie的权限,才能使用。或者有些用户干脆在浏览器上直接禁用Cookie。
  2. Cookie会随着Http请求一并发给客户端,如果CooKie里装的东西太多,会增加网络开销。

使用时的注意事项

  1. 客户端有可能会篡改个别数据,因此服务端需要对关键字段进行校验。
  2. 千万不要存一些敏感数据
  3. Cookie最多存储4K的数据。浏览器对于同时存储的Cookie也有数量上的限制。一般是300个。
  4. 无法跨域,这是非常麻烦的问题。
  5. 移动端对于Cookie支持很差。一般都是使用JWT。

Session(会话)

Session也是用来存储客户端状态的一种方法。它依赖Cookie,又区别于Cookie

工作流程

未命名文件.png

  1. 客户端发起登录请求时,服务端先校验用户的信息,然后生成一个唯一的SessionID,并将SessionID和用户信息一起保存起来(通常是本地文件的形式。目前大型服务都是存在Redis服务中)
  2. 通过SetCookie方法,将SessionID返回给客户端。
  3. 客户端收到SessionID后,会按照Cookie相关的规则,将数据存在本地。作用域,有效期等等。
  4. 客户端后续的请求都会携带Cookie,也就是把SessionID传递给了服务端。服务端拿到之后,去查下该SessionID是否存在,如果没有就说明没有登录。如果有,就找到了这个用户的信息,可以继续后续的操作。

因此,我们说Session的实现需要依赖Cookie,大部分服务都是使用这一套方式来实现登录和验签的。

Session和Cookie的区别

最大的区别:Session存在服务器上,Cookie直接把数据存在客户端上。带来的不同点有:

  1. Session相对安全点,数据不会被篡改。SessionID校验失败就直接返回了。
  2. Session可以存各种各样类型的数据。Cookie只能存字符串。Go服务中多数是直接存一个结构体。
  3. Cookie有存储空间和数量上限。Session理论上没有任何上限。
  4. Session可以持久化到很多地方。比如数据库,缓存。

使用时的注意事项

  1. 用户数量非常多的时候,保存大量Session信息是风险非常高的一种行为。它会占用大量的内存空间,一旦出问题,会影响大量用户。不能把鸡蛋都放在一个篮子里。
  2. 如果Session将信息保存在本机,那么就无法实现集群部署。目前几乎不会这样做,大家更倾向将数据存储在一个专用的验签服务上。
  3. Cookie的一些毛病,Session也都有,比如:无法跨域,移动端支持不好。因此Session必须要使用其他的办法来实现跨域。

引申,如果用户禁用了Cookie,那么Session该怎么办?

在GO语言中使用Session

Gin框架中没有集成Session模块。可以使用 gin-contrib/sessions 这个三方包来实现。

有时间可以看下他的底层源码,核心代码很少,很简单。

go get github.com/gin-contrib/sessions

具体的使用可以参考官方文档: github.com/gin-contrib…

顺便提一句,这个Session是可以直接作为中间件塞到Gin框架里的。

引申,如何实现用户登录后自动续期?

本来Session的有效期只有12个小时。如何让用户不必频繁登录?

分布式服务下的Session服务

这里只是一个引申。目前大多数公司不会选择过分依赖Session服务,成本有点高,大家更倾向于成本低,效率好的JWT。只有内部服务,或者后台管理系统普遍还在使用。

分布式服务,可以简单理解为服务集群,即有多个单台服务组成的一个服务集群。此时常用的策略有三种,实际使用的只有一种:

  1. 同步Session。单台服务Session变动,同步给其他所有机器,这是分布式服务一个经典的数据一致性问题。Session不适用这个场景,有更好用的实现方式。一定不会用的方案
  2. 通过Nginx进行代理,把用户的请求通过IP进行分类,保证某个IP的请求固定落在一台服务器上。经典加一层的思想。很显然,这样缺乏容错性,一台挂了,那么这个机器上的所有Session都会失效。
  3. 独立的Session服务,将Session存放在一个分布式缓存服务中,这是最常用的方案。本质上也是加一层,这个加一层,带来的优势更明显:跨平台共享,随业务变化扩缩容,不受服务器抖动影响。