后端请求鉴权和限流方案

223 阅读7分钟

请求鉴权

鉴权就是对用户发来的一个请求进行判断:你是谁,是什么类型的用户,又拥有哪些权限。在这里我们梳理一下常见的鉴权方案。

http基本认证方式

基本认证 (Basic 认证) 是 HTTP/1.0 就定义的认证方式,主要通过用户提供用户名和密码的方式,实现对用户身份的验证。

image.png

cookie

我们最常见的就是使用账号和密码去登录网站,通过这种方式去访问资源。但我们知道HTTP本身是无状态的,每一次请求之间都是独立的。那总不能我每点击一次请求资源都要输入相应的账号和密码吧,这对用户来说显然是不可接受的。于是,如果能把用户输入的账号和密码存储在本地并每次请求的时候带上不就好了么,这就是Cookie。在HTTP的请求和响应报文中,分别有Cookie和Set-Cookie两个字段,分别代表客户端本地的缓存信息和服务端希望客户端缓存的信息。

有了Cookie存储信息,就不再需要用户重复输入了。例如直接在Cookie中存储账号密码,之后发起请求的时候直接带上即可。但实际上为了安全,一般是经过加密的。如果加密算法泄露Cookie依然会泄露。为了安全,我们可以使用Https来传输报文,这样中间人拿到的是密文,Cookie就无从得知了。

Session

cookie虽然好用,但也有一些缺点。比如:

  • cookie需要随着报文发送,大小是有限制的,为4KB
  • 浏览器对于cookie的数量也有限制

如果我们希望客户端和服务端交互的时候提供更多的用户会话信息,但客户端又存储不了这么多该怎么办呢?Session的出现就是为了解决这个问题。Session是存储在服务端的状态信息,且不存储在客户端。那么之后的交互中又怎么去验证信息呢?所以还是需要通过Cookie来存储服务端Session的唯一标识sessionid。(所以session也没什么神奇的,它就是为了应对cookie容量和数量的缺点,把会话信息存储在服务端中,但实际上客户端不可能什么都不存储的,还是需要借助cookie完成信息的关联

image.png

实际使用的时候,存储Session往往存储在分布式系统中,如redis。以<key, value>的形式把<sessionid, session>存储在相应的分布式系统中,并且通过设置过期时间来实现登录有效期。

但session也有一些缺点:

  • 它依赖cookie,禁用cookie的情况下无法使用
  • 大量session存储在服务端,会增加服务端的开销

token

上述介绍中,我们知道了 Session-Cookie 的一些缺点,及 Session 的维护给服务端造成很大困扰,必须找地方存放它,又要考虑分布式的问题,所以 Token 方案就出来了。它是一种服务端无需记录任何状态,且可跨域的鉴权方案。

Token称作令牌,是一种服务端无状态的认证方式,将关键的信息拼接成字符串并进行加密。一般组成Token的信息可以是uid+timestamp+sign+[固定参数]。它的过程其实和cookie类似,只不过服务端并不会存储相应的token,每次都是拿到token后实时计算验证。

image.png

jwt

上述几种方案都有一个问题,就是不管是cookie还是token,实际只包含了uid信息,需要更多的登录信息和其它上下文数据的话需要查询数据库。每次都查询数据库的话难免开销会更大,JWT是一种对Json进行加密签名的方式来实现授权验证的,它的特点是自包含的。用户信息和认证信息是在一起的,它既不像cookie/token那样需要查询数据库,也不像cookie-session那样需要session服务器,这就是JWT的最大优点。

请求限流

流量限制的手段有很多,最常见的:漏桶、令牌桶两种:

  1. 漏桶是指我们有一个一直装满了水的桶,每过固定的一段时间即向外漏一滴水。如果你接到了这滴水,那么你就可以继续服务请求,如果没有接到,那么就需要等待下一滴水。
  2. 令牌桶则是指匀速向桶中添加令牌,服务请求时需要从桶中获取令牌,令牌的数目可以按照需要消耗的资源进行相应的调整。如果没有令牌,可以选择等待,或者放弃。

两种方法看起来类似,但漏桶流出的速率是固定的,对于超出并发限制的请求无法通过的。而令牌桶一方面允许产生令牌,另外一方面消耗令牌。若开始令牌桶不为空,则并发量是会超过令牌生产速度的。也就是说:令牌桶是运行一定的并发波动的,当令牌桶为空便退化为漏桶;从这个角度来看,令牌桶无非就是多了一个缓冲

从功能上来看,令牌桶模型就是对全局计数的加减法操作过程,但使用计数需要我们自己加读写锁,我们可以使用 buffered channel 来完成简单的加令牌取令牌操作:

package main

import (
    "fmt"
    "time"
)

func main() {
    var capacity = 100
    var fillInterval = time.Second / time.Duration(capacity)
    var tokenBucket = make(chan struct{}, capacity)

    // 每秒向桶里放出100个token
    fillToken := func() {
       ticker := time.NewTicker(fillInterval)
       for {
          <-ticker.C
          tokenBucket <- struct{}{}
          fmt.Println("current token cnt:", len(tokenBucket), time.Now())
       }
    }

    // 从令牌桶里拿出N个令牌
    takeN := func(n uint32) bool {
       for i := 0; i < int(n); i++ {
          <-tokenBucket
       }
       return true
    }

    go fillToken()
    go takeN(35)
    time.Sleep(time.Second * 2)
}

以上我们利用chan实现了一个简易的令牌桶。一个函数定时向channel放入令牌,需要的时候再去取用。但实际的实现远比这个更加高效,我们压根不需要有个goroutine去定时放入数据。只需要记录某个时刻桶中令牌的数量和时间,当取令牌的时候通过计算时间差和令牌生成速率以及初始的令牌数,就可以得出令牌的数量了。

我们可以参考下面这个开源库的实现github.com/juju/rateli…

type Bucket struct {
	clock Clock

	// startTime holds the moment when the bucket was
	// first created and ticks began.
	startTime time.Time

	// capacity holds the overall capacity of the bucket.
	capacity int64

	// quantum holds how many tokens are added on
	// each tick.
	quantum int64

	// fillInterval holds the interval between each tick.
	fillInterval time.Duration

	// mu guards the fields below it.
	mu sync.Mutex

	// availableTokens holds the number of available
	// tokens as of the associated latestTick.
	// It will be negative when there are consumers
	// waiting for tokens.
	availableTokens int64

	// latestTick holds the latest tick for which
	// we know the number of tokens in the bucket.
	latestTick int64
}

可以看到,它并没有一个周期性的时钟去生成相应的令牌,而是记录了lastestTick、availableTokens等变量以及quantum、fillInterval。我们可以查看它获取令牌的方法。

// take is the internal version of Take - it takes the current time as
// an argument to enable easy testing.
// take方法只传入了当前时间和想要取走的令牌数目count
func (tb *Bucket) take(now time.Time, count int64, maxWait time.Duration) (time.Duration, bool) {
	if count <= 0 {
		return 0, true
	}

	tick := tb.currentTick(now)
  // adjustavailableTokens则是根据时间差和速率来调整令牌数
	tb.adjustavailableTokens(tick)
	avail := tb.availableTokens - count
	if avail >= 0 {
		tb.availableTokens = avail
		return 0, true
	}
	// Round up the missing tokens to the nearest multiple
	// of quantum - the tokens won't be available until
	// that tick.

	// endTick holds the tick when all the requested tokens will
	// become available.
  // 如果令牌数目不够,就需要计算需要等待多久,也就是阻塞时间
	endTick := tick + (-avail+tb.quantum-1)/tb.quantum
	endTime := tb.startTime.Add(time.Duration(endTick) * tb.fillInterval)
	waitTime := endTime.Sub(now)
	if waitTime > maxWait {
		return 0, false
	}
	tb.availableTokens = avail
	return waitTime, true
}

我们接着查看它调整令牌数目的方法。

// adjustavailableTokens adjusts the current number of tokens
// available in the bucket at the given time, which must
// be in the future (positive) with respect to tb.latestTick.
func (tb *Bucket) adjustavailableTokens(tick int64) {
	lastTick := tb.latestTick
	tb.latestTick = tick
	if tb.availableTokens >= tb.capacity {
		return
	}
  //根据时间差和生产速度来增加令牌数目
	tb.availableTokens += (tick - lastTick) * tb.quantum
	if tb.availableTokens > tb.capacity {
		tb.availableTokens = tb.capacity
	}
	return
}