Token、Session和Cookie

332 阅读18分钟

为什么使用Token验证: 在Web领域基于Token的身份验证随处可见。在大多数使用Web API的互联网公司中,tokens 是多用户下处理认证的最佳方式。 以下几点特性会让你在程序中使用基于Token的身份验证:

  • 1.无状态、可扩展
  • 2.支持移动设备
  • 3.跨程序调用
  • 4.加密,安全
    (A)Token的起源
    在介绍基于Token的身份验证的原理与优势之前,不妨先看看之前的认证都是怎么做的。
      基于服务器的验证
      HTTP协议是无状态的,这种无状态意味着程序需要验证每一次请求,从而辨别客户端的身份,在这之前,程序都是通过在服务端存储的登录信息来辨别请求的。这种方式一般都是通过存储Session来完成。随着Web,应用程序,移动端的兴起,基于服务器验证这种验证的方式逐渐暴露出了问题。尤其是在可扩展性方面。
  • 1.Seesion:每次认证用户发起请求时,服务器需要去创建一个记录来存储信息。当越来越多的用户发请求时,内存的开销也会不断增加。
  • 2.可扩展性:在服务端的内存中使用Seesion存储登录信息,无法水平扩展
  • 3.CORS(跨域资源共享):当我们需要让数据跨多台移动设备上使用时,跨域资源的共享会是一个让人头疼的问题。在使用Ajax抓取另一个域的资源进行跨域请求时,就可以会出现禁止请求的情况。
  • 4.CSRF(跨站请求伪造):用户在访问银行网站时,他们很容易受到跨站请求伪造的攻击,并且能够被利用其访问其他的网站。 在这些问题中,可扩展性是最突出的。因此我们有必要去寻求一种更有行之有效的方法。
    基于Token的验证原理
      基于Token的身份验证是无状态的,我们不将用户信息存在服务器或Session中。这种概念解决了在服务端存储信息时的许多问题NoSession意味着你的程序可以根据需要去增减机器,而不用去担心用户是否登录。 基于Token的身份验证的过程如下:
  • 1.用户通过用户名和密码发送请求。
  • 2.程序验证。
  • 3.程序返回一个签名的token 给客户端。
  • 4.客户端储存token,并且每次用于每次发送请求。
  • 5.服务端验证token并返回数据。
    Tokens的优势
    (1)无状态、可扩展
      在客户端存储的Token是无状态的,并且能够被扩展。基于这种无状态和不存储Session信息,负载均衡器能够将用户信息从一个服务器传到其他服务器上,因为tokens里负载了用户的验证信息。相反,如果我们将已验证的用户的信息保存在Session中,则每次请求都需要用户向已验证的服务器发送验证信息(称为Session亲和性),用户量大时,可能会造成拥堵,消耗内存。
      Token能够创建与其它程序共享权限的程序。使用token时,可以提供可选的权限给第三方应用程序。当用户想让另一个应用程序访问它们的数据,我们可以通过建立自己的API,得出特殊权限的token
    (2)安全性
      请求中发送token而不再是发送cookie能够防止CSRF(跨站请求伪造)。即使在客户端使用cookie存储token,cookie也仅仅是一个存储机制而不是用于认证。不将信息存储在Session中,让我们少了对session操作。token是有时效的,可以设置有效期,一段时间之后用户需要重新验证。也不一定需要等到token自动失效,token有撤回的操作,通过token revocataion可以使一个特定的token或是一组有相同认证的token无效
    (3)多平台跨域
      先来谈论一下CORS(跨域资源共享,详见上篇文章juejin.cn/post/691605…
    (B)Session
      session就是会话,这是服务端的一种操作。当你第一次访问一个web网站的时候,服务端会生成一个session,并有一个sessionid和他对应。这个session是存储到内存中的,你可以向这个session中写入信息,比如当前登录用户的信息。sessionid会被返回到客户端,客户端一般采用cookie来保存,当然这个cookie不用人为写入,通常缓存在浏览器中


当后端调用HttpServletRequest对象的getSession的方法的时候,tomcat内部会生成一个jsessonid(tomcat sessionid的叫法)。这个jsessonid会随本次请求返回给客户端。
响应头信息:
HTTP/1.1 200 OK
Set-Cookie: JSESSIONID=xxxxxxxxxxxxxxxxxxx
这个jessionid就会写到cookie中。之后jessionid就会通过cookie传递到服务端。 这里我们就会很清楚了,session的数据是存储到内存中,那问题就来了,如果我们的服务是分布式部署,有多台机器的话,可能我们第一次登陆的时候,我们把用户的信息存储到了session,但是后面的请求打到了B机器上,那B机器是获取不到用户的session的。另外就是session存储在内存中,那服务器重启,session就丢失了,这就是它的弊端。现在有一些技术,例如session共享、iphash、session持久等也可以解决上述问题
(C)Cookie
  cookie是浏览器的一种策略。上述讲到了sessionid就是存储在cookie中的。我们知道http协议是无状态的,cookie就是用来解决这个问题的。cookie中可以用来保存服务端返回的一些用户信息的,例如前文提到的token、sessionid。每一次的请求,都会携带这些cookie。服务端从请求头中取到cookie中的信息,就可以识别本次请求的来源,这样,http是不是就变成有状态的了。
说几点cookie注意事项:

  • 1.cookie存放在客户端,所以是不安全的,人为可以清除。
  • 2.cookie有过期时间设定。如果不设置过期时间,说明这个cookie就是当前浏览器的会话时间,浏览器关了,cookie就不存在了。如果有过期时间,cookie就会存储到硬盘上,浏览器关闭不影响cookie。下次打开浏览器,cookie还存在.
  • 3.cookie有大小的限制,4KB。   最后,对于一个分布式的web系统,通用的解决方案就是cookie+token。由服务端生成token,将用户信息与token进行关联,token返回给浏览器,存储到cookie(cookie无法直接跨域,可以存在header中)中。后续请求都携带cooke或者将token从cookie取出以参数传递。

(D)总结

  • 1.对于会话来说,JWT 真的算 Token 么?虽然名字中带 Token,但是对于会话来说,我觉得 JWT 其实更像是一种“客户端会话”。客户端会话虽然有一些吸引力,但是带来的问题好像比服务端会话更多,怎么还有好多人推荐使用,是我学艺不精,还是他们没搞清楚?

  • 2.服务端会话已经很成熟易用了,在前端也只需要保存一个会话 ID 而已,在传统的开发模式中这个会话 ID 基本上都是保存在 Cookie 中的。如果在前后端分离这种开发模式下,真的用不了 Cookie (比如说部分客户端不支持 Cookie 或者跨域了),那直接基于 HTTP 头部实现一种替代的会话 ID 传输方式,然后再找一个地方存储这个会话 ID,不就可以了么?我看好像也有些人这么做并且管这个叫 Token 。

  • 3.见到一些人说“先输入用户名和密码进行登陆,成功后返回一个 Token,然后后续请求都带上这个 Token,定期更换即可”。从功能上讲,这种 Token 感觉十分类似我们做“保持登陆 7 天”时的那种自动登陆 Token,是么?那这种 Token 和会话好像已经没关系了啊!   前后端分离中不使用 Cookie 的情况下,Session 和 Token 到底该怎么用?

  • (1)多个业务的情况下 Session 是单个业务维护登陆用户信息的 而 token 是多个系统共用的登陆鉴权的

  • (2)jwt 是去中心化,自包含的

  • (3)这个东西主要是分布式或者负载均衡里面用,当你同一个用户的两个请求打到不同服务实例上的时候,这两个实例都需要有能力验证它的合法性。session 的话你就得保证两个请求都进到同一个服务器里才行。或者你确定你的业务规模用不着做这些东西,那直接用 session 最方便。

  • (4)JWT 最大的问题是一旦发出去了,在它的失效时间到来之前你永远无法撤销它的认证,要是存 redis 里过滤那就太好笑了。它最大的好处就是"无状态",如果要考虑到复杂的认证 /鉴权系统,JWT 是很有优势的,因为 JWT 的灵活性远远高于 session 。如果只是简单的客户端认证,其实和前后端是否分离关系不大,就直接用自带的 session 认证系统基本就能 hold 住。否则还得设计 token 分发,刷新,过期处理等等麻烦的事情。不过,可以用 jwt 来存 session id,这样不就完美解决这个问题了!!!

  • (5)JWT 最大的特点是存在客户端,而不是服务器端,所以“无状态”,但也正因为没有存在服务端,所以服务端没办法主动让其失效,只能等其过期。无法被服务端主动撤销,以及潜在的安全问题,就注定无法替代 session ,感觉 jwt 存储 session id,服务端使用分布式 session 才是比较好的解决方案 (E)token主动失效解决方案
      由于项目中涉及到token主动失效的需求,因此记录相关使用的解决方案。由于该项目部分模块是涉及到内部商城系统,只对集团内部员工开放福利,因此用户鉴权要求较高。具体需求为:在token泄露(如Cookies被劫持)以及密码重置的场景下,商城系统需要将未过期的但是已经不安全的token主动失效
      一般而言,token 存在 Header 或者 Cookies中 ,由于HTTP 是明文传输,HTPPS 是密文传输,因此可以将HTPPS代替HTTP传输,这样可以一定程度防止token被截获。由于工作中商城系统项目涉及到的用户不仅包括大众用户,还包括内部用户,而商城中部分折扣、满减等福利只对内部用户开放,为防止内部用户token被截取,项目中采用了常用的应对方式:用户登录时,Server端将生成的token与用户登录IP(反向代理时,Server端也能从相应的消息头获取到Client端的IP)进行绑定,并将生成的token返回给Client端,最后当用户再次登录时,即使token在中途被截取,Server端通过检验token中的IP,就能发现token已泄露。同时为了增加截获成本,可以在Server端生成token时,对其进行RSA非对称加密方式进行加密或者AES对称加密。另外,对于部分敏感操作,为了安全起见,会利用短信验证码进行二次校验。
      如果Server发给Client的token还没有过期,但是变得不安全,如token泄露或者想踢掉恶意用户,Server端需要及时地主动将还在有效期但是不安全的token剔除,使其失效。Server端对token的控制比Session弱,Session可以用Server端掌控,但是token一旦签发,就脱离了Server的掌控,成了脱缰的野马。通常而言,Server端只能通过声明token的过期时间expire,使其在过期后被动失效。因此,就像上文所说,token最大的优点也是其最大的弱点。有人可能会说,为什么不把Server将签发的token都存一份到分布式缓存中,这样token就能被跟踪和标识。这种方式可以解决问题,但每份token都要保存起来,并且每次校验都要做查询。这种方式其实和分布式session并没有多大差别。显然,这种方式放弃了token占用空间小、有效性校验速度快的优点。在没有跨域资源访问的需求场景下,分布式Session的方案反而更有优势,至少可以在Session中保存敏感信息。Session是存在Server端,而不用担心在Client端被泄露。
    项目中采用的方案(不一定最优):

  • (1) 为每个token设置不太长的过期时间,token过期时间设置太久会不安全

  • (2) 在用户表user中,添加一个“token_black_white_id”字段用于保存一个随机String
    user_name | password | ... | token_black_white_id

  • (3) 在token(项目中使用JWT)的payload中负载"tokenBlackWhiteId"属性。签发token时,将用户的"token_black_white_id"字段对应的字符串写入。

   {
        "exp": 失效时间,

       "tokenBlackWhiteId": "对应的字段值随机字符串"
   }
  • (4) 将分布式缓存服务器(Redis)作为黑名单容器。若用户token泄露或者重置密码等需要剔除已经签发的token时,为该用户生成新的"token_black_white_id"存入用户登陆表并将"token_black_white_id"旧值加入黑名单即分布式缓存中。通过保存Key为"tokenBlack + userId(user表的主键user_id)",Value为上述tokenBlackWhiteId属性值的K-V结构对泄露的token进行拉黑。

  • (5) "token_black_white_id"在黑名单容器(Redis)中的有效时间是token设置的过期时间,过期后自动从黑名单中删除。

  • (6) 做token有效性校验的Server在启动时从分布式缓存下载黑名单到本地内存(通过redisTemplate.keys(prefix),其中prefix="tokenBlack*", * 号用于匹配所有黑名单中的userId)。并且订阅分布式缓存的消息推送功能以监听黑名单的增删动作,同步修改本地内存中的黑名单。

  • (7) Server端做token校验时,不仅仅校验过期时间,也要查询内存中的黑名单列表。若token的"tokenBlackWhiteId"在黑名单中,则该token失效。   【注】(a)其实,也可以直接使用分布式Session,但是分布式Session上面也分析过,每次都会查询用户token的整个集合,效率不及上述方案; (b)另外,采用专门的字段token_black_white_id是为了进一步提升查询速度,因为通常而言,token串比较长,占用存储的同时还可能会影响查询速度,不直接存储在Redis目的是防止恶意请求token导致黑名单膨胀,采用字段token_black_white_id,可以保证永远只有一个token。   
    (F)分布式session

  • 1)session复制   session复制是小型企业应用使用较多的一种服务器集群session管理机制,在真正的开发使用的并不是很多,通过对web服务器(例如Tomcat)进行搭建集群。
    缺点:
      session同步的原理是在同一个局域网里面通过发送广播来异步同步session的,一旦服务器多了,并发上来了,session需要同步的数据量就大了,需要将其他服务器上的session全部同步到本服务器上,会带来一定的网路开销,在用户量特别大的时候,会出现内存不足的情况
    优点:
      服务器之间的session信息都是同步的,任何一台服务器宕机的时候不会影响另外服务器中session的状态,配置相对简单
      Tomcat内部已经支持分布式架构开发管理机制,可以对tomcat修改配置来支持session复制,在集群中的几台服务器之间同步session对象,使每台服务器上都保存了所有用户的session信息,这样任何一台本机宕机都不会导致session数据的丢失,而服务器使用session时,也只需要在本机获取即可
    如何配置:
      在Tomcat安装目录下的config目录中的server.xml文件中,将注释打开,tomcat必须在同一个网关内,要不然收不到广播,同步不了session 在web.xml中开启session复制:<distributable/>

  • 2)session黏性(绑定)   表示从同一窗口发来的请求都将有集群中的同一个tomcat进行处理 worker.lbcontroller.sticky_session=True 粘性session的好处在不会在不同的tomcat上来回跳动处理请求,但是坏处是如果处理该session的tomcat崩溃,那么之后的请求将由其他tomcat处理,原有session失效而重新新建一个新的session,这样如果继续从session取值,会抛出nullpointer的访问异常。
    Nginx

  Nginx是一款自由的、开源的、高性能的http服务器和反向代理服务器;反向代理、负载均衡、http服务器(动静代理)、正向代理 如何使用nginx进行session绑定?
  利用nginx的反向代理和负载均衡,之前是客户端会被分配到其中一台服务器进行处理,具体分配到哪台服务器进行处理还得看服务器的负载均衡算法(轮询、随机、ip-hash、权重等),但是我们可以基于nginx的ip-hash策略,可以对客户端和服务器进行绑定,同一个客户端就只能访问该服务器,无论客户端发送多少次请求都被同一个服务器处理
在nginx安装目录下的conf目录中的nginx.conf文件

upstream www.sasuke.cn {//upstream的负载均衡
//  Ip_hash;可以用hash
  server 39.125.59.4:8080 weight=2;
/*也可以用权重,weight是权重,可以根据机器配置定义权重。weigth参数
  表示权值,权值越高被分配到的几率越大。*/
  Server 39.135.59.4:8081 weight=3;
}
server {
  listen 80;
  server_name www.sasuke.cn;
  #root /usr/local/nginx/html;
  #index index.html index.htm;
  location / {
    proxy_pass http:39.105.59.4;
    index index.html index.htm;
  }
}

缺点:

容易造成单点故障,如果有一台服务器宕机,那么该台服务器上的session信息将会丢失

前端不能有负载均衡,如果有,session绑定将会出问题

优点:

配置简单
  • 3)session集中式管理 优点:redis为内存数据库,读写效率高,并可在集群环境下做高可用.是常用的方式
    加入依赖:
<dependency>  
    <groupId>org.springframework.boot</groupId>  
    <artifactId>spring-boot-starter-data-redis</artifactId>  
</dependency>  
<!-- session redis 共享 -->  
<dependency>  
     <groupId>org.springframework.session</groupId>  
     <artifactId>spring-session-data-redis</artifactId>  
</dependency>  

yml文件:

server:  
  port: 8080  
spring:  
  application:  
    name: manage-session  
    redis:  
    password: *****
    database: 0  
    host: 127.0.0.1  
    port: 6379 

@EnableRedisHttpSession注解启动类

    @SpringBootApplication
    @EnableRedisHttpSession 
    /* 可以通过maxInactiveIntervalInSeconds设置Session过期时间 */
    public class Application {
        public static void main(String[] args) throws Exception {
            SpringApplication.run(Application.class, args);
        }
    }

通过redis集中式管理session这种方式在使用上面对客户端是透明的,无需自己操作redis,在使用HttpSession对象的时候直接使用即可

   @RestController
   public class IndexController {
       @GetMapping("/item")
       public ResponseEntity<String> index(HttpSession httpSession) {
           httpSession.setAttribute("user", "Sasuke");
           return ResponseEntity.ok("ok");
       }

       @GetMapping("/page")
       public ResponseEntity<String> hello(HttpSession httpSession) {
           return ResponseEntity.ok(httpSession.getAttribute("user"));
       }
   }

优点:

这是企业中使用的最多的一种方式

spring为我们封装好了spring-session,直接引入依赖即可

数据保存在redis中,无缝接入,不存在任何安全隐患

redis自身可做集群,搭建主从,同时方便管理

缺点:

多了一次网络调用,web容器需要向redis访问

总结:
一般会将web容器所在的服务器和redis所在的服务器放在同一个机房,减少网络开销,走内网进行连接

  • 4)基于cookie管理 缺点:

    数据存储在客户端,存在安全隐患; cookie存储大小、类型存在限制; 数据存储在cookie中,如果一次请求cookie过大,会给网络增加更大的开销 优点:

    数据存储在cookie中,某种程度上会减轻Server端的压力

参考博文:
fairysoftware.com/token_sessi…
www.v2ex.com/amp/t/70331…