cookie、session、token联系与区别

前言

我们都知道http协议本身是一种无状态的协议,一个普通的请求大致分为三步:

  1. 客户端发送请求给服务器 ;

  2. 服务器处理该请求;

  3. 服务器将处理结果响应该客户端。

之后该客户端再次向该服务区发送请求后,服务器端并不能知道这两个请求是否是同一个浏览器或用户发出来的。所以作为web服务器必须能够采用某种方式来唯一识别同一个用户,并记录该用户的状态。而这同一个客户端与服务器在一段时间内的多次交互,我们就可以称该客户端是该服务器的一个客户端会话窗口,有了会话窗口,我们就能确定哪个请求是哪个用户发出的了,从而可以实现会话跟踪,并记录用户的行为。

概念:
会话: 可以理解为用户打开浏览器,访问该web服务器的多个资源,然后关闭浏览器,这中间的一系列过程称之为一个会话。

有状态的会话: 浏览器发送的每一次请求,每一个会话都要有唯一的标识来唯一标识自己,当浏览器发送请求的时候就带上这个标识来让服务器识别,从而实现有“状态”的会话。

javaweb中有两种实现会话的机制:

  1. Cookie机制

  2. Session机制

cookie和session是浏览器与服务器交互的一种规范,有专门的的组织对该规范进行定义,只要浏览器或服务器遵守了该规范,我们就能使用cookie和session。其他能做web开发的高级语言也有,只是实现方式不同罢了。

1、cookie

1.1 基本介绍

  1. cookie机制采用的是在客户端保持 HTTP 状态信息的方案。当浏览器访问WEB服务器的某个资源时,WEB服务器会在HTTP响应头中添加一个键值对传送给浏览器,再由浏览器将该cookie放到客户端磁盘的一个文件中,该文件可理解为cookie域(键值对的集合),往后每次访问某个网站时,都会在请求头中带着这个网站的所有cookie值。(至于怎么区分不同网站的cookie的,很简单,每个网站都给他一个唯一标识比如网址等,每次打开某网址时,就查询该网站下的所有cookie值即可)

  2. 每一个cookie都有一个key和一个value,且key是唯一的。相同名字时,后者会覆盖掉前者(类似哈希表的key的效果)。

  3. 一个WEB浏览器也可以存储多个WEB站点提供的Cookie。浏览器一般只允许存放300个Cookie,每个站点最多存放20个Cookie,每个Cookie的大小限制为4KB。

  4. HTTP Cookie 机制是 HTTP 协议无状态的一种补充和改良

1.2 分类

  1. 会话级别的cookie
    默认情况下它是一个会话级别的cookie,存储在浏览器的内存中,用户退出浏览器之后被删除,因为它没有指定ExpiresMax-Age 指令。但是,Web 浏览器可能会使用会话还原,这会使大多数会话 Cookie 保持永久状态,就像从未关闭过浏览器一样。

  2. 持久化的cookie
    若希望浏览器将该cookie存储在磁盘上,则需要设置该cookie的生命周期setMaxAge,并给出一个以秒为单位的时间。将最大时效设为0则是命令浏览器在会话结束后删除该cookie。持久性 Cookie 不会在客户端关闭时过期,而是在特定日期(Expires)特定时间长度(Max-Age)外过期。例如:

    Set-Cookie: id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT;
    

1.3 Cookie 的 Secure 和 HttpOnly 标记

安全的 Cookie 需要经过 HTTPS 协议通过加密的方式发送到服务器。即使是安全的,也不应该将敏感信息存储在cookie 中,因为它们本质上是不安全的,并且此标志不能提供真正的保护。

HttpOnly 的作用

  • 会话 Cookie 中缺少 HttpOnly 属性会导致攻击者可以通过程序(JS脚本、Applet等)获取到用户的 Cookie 信息,造成用户 Cookie 信息泄露,增加攻击者的跨站脚本攻击威胁。

  • HttpOnly 是微软对 Cookie 做的扩展,该值指定 Cookie 是否可通过客户端脚本访问。

  • 如果在 Cookie 中没有设置 HttpOnly 属性为 true,可能导致 Cookie 被窃取。窃取的 Cookie 可以包含标识站点用户的敏感信息,如 ASP.NET 会话 ID 或 Forms 身份验证票证,攻击者可以重播窃取的 Cookie,以便伪装成用户或获取敏感信息,进行跨站脚本攻击等。

1.4 cookie的作用域

cookie的domain和path属性定义了cookie的作用范围,即访问哪些网站或url时,会自动的带着该cookie。domain即域名,默认是当前服务器主机(不包括子域名),如果指定了Domain,则一般包含子域名。path默认是*(所有路径),即域名后面的的路径。大部分情况下我们都是使用默认的设置即可。

例如,如果设置 Domain=mozilla.org,则 Cookie 也包含在子域名中(如developer.mozilla.org)。

例如,设置 Path=/docs,则以下地址都会匹配:

  • /docs
  • /docs/Web/
  • /docs/Web/HTTP

1.5 基本原理

当一个客户端浏览器访问某web服务器时,web服务器会调用HttpServletResponse的addCookie()方法,在响应头中添加一个名叫Set-Cookie的响应字段用于将Cookie返回给浏览器,当客户端浏览器第二次访问该web服务器时会自动的将该cookie回传给服务器,来实现用户状态跟踪。

1.6 java中常用api

javax.servlet.http.Cookie类来封装Cookie信息,它包含有生成Cookie信息和提取Cookie信息的各个属性的方法。

  1. public Cookie(String name,String value)。

  2. setMaxAge(int longTime)与getMaxAge方法:设置和获取cookie的最大有效时长。setMaxAge(0) 表示删除磁盘上的某个cookie。

    注意:cookie没有提供修改方法,当name一样时,覆盖原来的就算是更新了。删除也是,setMaxAge(0),当name一样时,原来的会被覆盖掉,新建的没有生命周期,也会被立马删除。

  3. setPath与getPath方法 :设置或读取Cookie的作用范围。

  4. HttpServletResponse接口中定义了一个addCookie(Cookie cookie)方法,它用于在发送给浏览器的HTTP响应消息中增加一个Set-Cookie响应头字段。

  5. HttpServletRequest接口中定义了一个getCookies方法,它用于从HTTP请求消息的Cookie请求头字段中读取所有的Cookie项。

  6. getName方法 :获取到cookie的name。

  7. setValue(String value)与getValue方法:设置和获取cookie的value。

下面附上cookie的三个java代码示例:

//保存用户上一次访问的时间
public class CookieDemo01 extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //服务器,告诉你,你来的时间,把这个时间封装成为一个信件,你下次带来,我就知道你来了
        //中文乱码
        req.setCharacterEncoding("UTF-8");
        resp.setContentType("text/html");  //加上这个选项google才不会中文乱码
        resp.setCharacterEncoding("UTF-8");
        PrintWriter out = resp.getWriter();

        //cookie,服务器端从客户端获取
        Cookie[] cookies = req.getCookies();  //这里返回数组,说明cookie可能存在多个

        //判断cookie是否存在

        if(cookies!=null){
            //如果存在怎么办
            out.write("你上一次访问的时间是:");
            for (int i = 0; i < cookies.length; i++) {
                Cookie cookie = cookies[i];
                //获取cookie的名字
                if (cookie.getName().equals("lastLoginTime")) {
                    //获取cookie的值
                    System.out.println(cookie.getValue());
                    long lastLoginTime = Long.parseLong(cookie.getValue());
                    Date date = new Date(lastLoginTime);
                    out.write(date.toLocaleString());
                }
            }

        }else {
            out.write("这是您第一次访问本站");
        }

        //服务器给客户端响应一个cookie
        Cookie cookie = new Cookie("lastLoginTime",System.currentTimeMillis()+"");

        cookie.setMaxAge(24*60*60);

        resp.addCookie(cookie);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doGet(req, resp);
    }
}
//保存用户上一次访问的时间

public class CookieDemo02 extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        //服务器给客户端响应一个cookie
        Cookie cookie = new Cookie("lastLoginTime",System.currentTimeMillis()+"");

        cookie.setMaxAge(0);  //删除cookie

        resp.addCookie(cookie);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doGet(req, resp);
    }
}
//中文数据传递
public class CookieDemo03 extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

        req.setCharacterEncoding("UTF-8");
        resp.setContentType("text/html");  //加上这个选项google才不会中文乱码
        resp.setCharacterEncoding("UTF-8");
        PrintWriter out = resp.getWriter();
        //cookie,服务器端从客户端获取
        Cookie[] cookies = req.getCookies();  //这里返回数组,说明cookie可能存在多个

        //判断cookie是否存在

        if(cookies!=null){
            //如果存在怎么办
            out.write("你上一次访问的时间是:");
            for (int i = 0; i < cookies.length; i++) {
                Cookie cookie = cookies[i];
                //获取cookie的名字
                if (cookie.getName().equals("name")) {

                    System.out.println(cookie.getValue());
                    //用了转码这里就要进行解码操作
                    String decode = URLDecoder.decode(cookie.getValue(),"utf-8");
                    System.out.println(decode);
                    out.write(decode);

                }
            }

        }else {
            out.write("这是您第一次访问本站");
        }

        //Cookie cookie = new Cookie("name", "测试");   //这样也可以输出中文,但最好用下面的形式,进行转码操作
        Cookie cookie = new Cookie("name", URLEncoder.encode("测试","UTF-8"));

        resp.addCookie(cookie);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doGet(req, resp);
    }
}


1.7 cookie中存储中文

cookie中存储中文会出现中文乱码,需要对value进行额外的编码

  1. base64编码

    存储:Base64.getEncoder().encodeToString(content.getBytes(“utf-8”));

    读取:new String(Base64.getDecoder().decode(cookie.getValue()),“utf-8”)。

  2. URLEncoder类

    存储:Cookie cookie = new Cookie(“userName”, URLEncoder.encode(“你好世界”, “UTF-8”));

    读取:URLDecoder.decode(cookie.getValue(), “UTF-8”)。

示例代码CookieDemo03中使用的就是第二种方式,采用的是URLEncoder类。

2、session

2.1 基本介绍

session机制采用的是在服务器端保持 HTTP 状态信息的方案。为了加速session的读取和存储,web服务器中会开辟一块内存用来保存服务器端所有的session,每个session都会有一个唯一标识sessionid,根据客户端传过来的jsessionid(cookie中),找到对应的服务器端的session。为了防止服务器端的session过多导致内存溢出,web服务器默认会给每个session设置一个有效期, (30分钟)若有效期内客户端没有访问过该session,服务器就认为该客户端已离线并删除该session。

session有效期设置在web.xml中,如下:

  <!--设置session默认的失效时间  -->
  <session-config>
      <!-- 1分钟后session自动失效   -->
    <session-timeout>1</session-timeout>
  </session-config>

保存sessionID的方式

  1. cookie中

    通过一个特殊的cookie,name为JSESSIONID,value为服务器端某个session的ID,默认的方式。但是当浏览器禁用cookie后session就会失效。

  2. url重写

    当浏览器Cookie被禁时用。

    就是把session的id附加在URL路径的后面。附加的方式也有两种,一种是作为URL路径的附加信息,另一种是作为查询字符串附加在URL后面。

    做法:

    • response.encodeURL(String url)用于对表单action和超链接的url地址进行重写;

    • response.encodeRedirectURL(String url) 用于对sendRedirect方法后的url地址进行重写。

这两个方法很智能,若浏览器禁用了cookie,就默认会进行url重写(url中带上sessionid),当用户浏览器没有禁用cookie时,就不在URL后附加sessionid。用法就是代替response.sendRedirect(String url)。

2.2 基本原理

当用户发送一个请求到服务器端时,服务器会先检查请求中是否含有sessionid(存在cookie中或者在url中),

  • 如果不存在sessionid(说明是第一次请求),就会为该请求用户创建一个session对象,并将该session对象的sessionid(放到响应头的set-cookie中,格式set-cookie:sessionid,下次再请求时cookie中就会有一个name为jsessionid的cookie,value就是sessionid的值)响应给客户端。

  • 如果存在sessionid,就会在服务器端查找是否有该sessionid对应的session,如果有就使用,没有就创建一个。

所以说,服务器端的session和客户端的cookie是息息相关的,若是没有了cookie,又不做其他处理的话,服务器端的session也没了。

下面贴出一个网站请求的cookie字段,示例如下:

Cookie: Hm_lvt_2358d52bb43b2c7c42cb5a060c736de6=1652432539; SHIRO_SESSION_ID=76896642-528f-482b-b0d6-60da24f06052; pageno_cookie=1; JSESSIONID=53510451F165A8502C78A375338C001E

2.3 java中常用api

  1. getId()方法:得到sessionid。

  2. invalidate()方法:让session立刻失效。

  3. getAttribute(String key):根据key获取该session中的value。

  4. setAttribute(String key,Object value):往session中存放key-value。

  5. removeAttribute(Stringkey):根据key删除session中的key-value。

  6. getServletContext():得到ServletContext。

  7. setMaxInactiveInterval(long timeout)/getMaxInactiveInterval:设置/获取session的最大有效时间。

  8. getCreationTime方法:获取session的创建的时间。

  9. getLastAccessedTime方法:获取session最后一次访问的时间。

  10. getSession():从HttpServletRequest中获取session。

2.4 基本应用

跨浏览器的会话跟踪

因为cookie在多个浏览器之间是共享的(但是不能跨域),所以可以将sessionid存在cookie中,再把cookie存入磁盘中,然后在其他浏览器中再次访问该服务器时,就会读取到cookie中的sessionid,从而回到上次访问的页面了。

下面附上session的三个代码示例:

public class SessionDemo01 extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

        //解决乱码问题
        req.setCharacterEncoding("UTF-8");
        resp.setContentType("text/html");  //加上这个选项google才不会中文乱码
        resp.setCharacterEncoding("UTF-8");

        //得到session
        HttpSession session = req.getSession();

        //给session中存东西
        session.setAttribute("name",new Person("测试",30));

        //获取session的id
        String id = session.getId();

        //判断session是不是新创建的
        if (session.isNew()) {
            resp.getWriter().write("session创建成功,ID"+id);
        }else {
            resp.getWriter().write("session已经在服务器中存在了,ID"+id);
        }

        //session创建的时候做了什么事情
//        Cookie cookie = new Cookie("JSESSIONID", id);
//        resp.addCookie(cookie);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doGet(req, resp);
    }
}


public class Person {
    private String name;
    private int age;

    public Person() {
    }

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}
public class SessionDemo02 extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

        //解决乱码问题
        req.setCharacterEncoding("UTF-8");
        resp.setContentType("text/html");  //加上这个选项google才不会中文乱码
        resp.setCharacterEncoding("UTF-8");

        //得到session
        HttpSession session = req.getSession();

        Person name = (Person) session.getAttribute("name");
        System.out.println(name.toString());
    }

        @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doGet(req, resp);
    }
}
public class SessionDemo03 extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        HttpSession session = req.getSession();

        session.removeAttribute("name");
        //手动注销
        session.invalidate();
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        doGet(req, resp);
    }
}

2.5 常见问题

  1. 关闭浏览器后cookie会消失吗?
    答:看情况。经过上面关于cookie的分析之后我们知道,cookie默认是存在于浏览器内存中的,若此时cookie没有持久化,浏览关闭后cookie会消失;若此时cookie进行了持久化,浏览器关闭后cookie不会消失。

  2. 关闭浏览器后session会消失吗?
    答:看起来会实际上不会。

    这个问题需要从以下两个方面考虑:

    • 从服务器端考虑 :我们知道session是存在于服务器端内存中的,和浏览器没有关系,所以浏览器关闭后,服务器端的session不会消失。(除非服务器重启或session达到了过期时间)

    • 从浏览器端考虑 :我们知道浏览器是根据cookie中的jesessionid值来唯一找到服务器端的session的,此时若cookie没有持久化,浏览器关闭后cookie也跟着消失。所以当用户再次打开浏览器后,由于没有了cookie中的jesessionid,自然也无法唯一找到服务器端的session,对用户来说,确实是浏览器关闭后再次打开就无法找到上次的会话了,误以为是关闭浏览器后服务器端的session也跟着消失,其实还在。

2.6 session的缺点

Session 机制有个缺点,比如 A 服务器存储了 Session,就是做了负载均衡后,假如一段时间内 A 的访问量激增,会转发到 B 进行访问,但是 B 服务器并没有存储 A 的 Session,会导致 Session 的失效。

此时请问该怎么办。主要有以下三种方式:

  1. session 复制

    A 生成 session 后复制到 B, C,这样每台机器都有一份 session,无论添加购物车的请求打到哪台机器,由于 session 都能找到,故不会有问题。)
    在这里插入图片描述
    这种方式虽然可行,但缺点也很明显:

    1)同一样的一份 session 保存了多份,数据冗余

    2)如果节点少还好,但如果节点多的话,特别是像阿里,微信这种由于 DAU 上亿,可能需要部署成千上万台机器,这样节点增多复制造成的性能消耗也会很大。

  2. session 粘连

    这种方式是让每个客户端请求只打到固定的一台机器上,比如浏览器登录请求打到 A 机器后,后续所有的添加购物车请求也都打到 A 机器上,Nginx 的 sticky 模块可以支持这种方式,支持按 ip 或 cookie 粘连等等,如按 ip 粘连方式如下:

    upstream tomcats {
      ip_hash;
      server 10.1.1.107:88;
      server 10.1.1.132:80;
    })
    

在这里插入图片描述
这样的话每个 client 请求到达 Nginx 后,只要它的 ip 不变,根据 ip hash 算出来的值会打到固定的机器上,也就不存在 session 找不到的问题了,当然不难看出这种方式缺点也是很明显,对应的机器挂了怎么办?

  1. session 共享

    这种方式也是目前各大公司普遍采用的方案,将 session 保存在 redis,memcached 等中间件中,请求到来时,各个机器去这些中间件取一下 session 即可。)
    在这里插入图片描述

缺点其实也不难发现,就是每个请求都要去 redis 取一下 session,多了一次内部连接,消耗了一点性能,另外为了保证 redis 的高可用,必须做集群,当然了对于大公司来说, redis 集群基本都会部署,所以这方案可以说是大公司的首选了。

3、Token:no session!

通过上文分析我们知道通过在服务端共享 session 的方式可以完成用户的身份定位,但是不难发现也有一个小小的瑕疵:搞个校验机制我还得搭个 redis 集群?大厂确实 redis 用得比较普遍,但对于小厂来说可能它的业务量还未达到用 redis 的程度,所以有没有其他不用 server 存储 session 的用户身份校验机制呢,这就是我们今天要介绍的主角:token。

首先请求方输入自己的用户名,密码,然后 server 据此生成 token,客户端拿到 token 后会保存到本地,之后向 server 请求时在请求头带上此 token 即可。
在这里插入图片描述
相信大家看了上图会发现存在两个问题:

  1. token 只存储在浏览器中,服务端却没有存储,这样的话我随便搞个 token 传给 server 也行?

    答:server 会有一套校验机制,校验这个 token 是否合法。

  2. 怎么不像 session 那样根据 sessionId 找到 userid 呢,这样的话怎么知道是哪个用户?答:token 本身携带 uid 信息

第一个问题,如何校验 token 呢?我们可以借鉴 HTTPS 的签名机制来校验。先来看 jwt token(JSON Web Token ,简称 JWT)的组成部分:
在这里插入图片描述
可以看到 token 主要由三部分组成:

  1. header:指定了签名算法。
  2. payload:可以指定用户 id,过期时间等非敏感数据。
  3. Signature: 签名,server 根据 header 知道它该用哪种签名算法,再用密钥根据此签名算法对 head + payload 生成签名,这样一个 token 就生成了。

当 server 收到浏览器传过来的 token 时,它会首先取出 token 中的 header + payload,根据密钥生成签名,然后再与 token 中的签名比对,如果成功则说明签名是合法的,即 token 是合法的。而且你会发现 payload 中存有我们的 userId,所以拿到 token 后直接在 payload 中就可获取 userid,避免了像 session 那样要从 redis 去取的开销。

画外音:header, payload 实际上是以 base64 的形式存在的,文中为了描述方便,省去了这一步。

你会发现这种方式确实很妙,只要 server 保证密钥不泄露,那么生成的 token 就是安全的,因为如果伪造 token 的话在签名验证环节是无法通过的,就此即可判定 token 非法。

可以看到通过这种方式有效地避免了 token 必须保存在 server 的弊端,实现了分布式存储,不过需要注意的是,token 一旦由 server 生成,它就是有效的,直到过期,无法让 token 失效,除非在 server 为 token 设立一个黑名单,在校验 token 前先过一遍此黑名单,如果在黑名单里则此 token 失效,但一旦这样做的话,那就意味着黑名单就必须保存在 server,这又回到了 session 的模式,那直接用 session 不香吗。所以一般的做法是当客户端登出要让 token 失效时,直接在服务器本地移除 token 即可,下次登录重新生成 token 就好。

另外需要注意的是 token 一般是放在 header 的 Authorization 自定义头里,不是放在 Cookie 里的,这主要是为了解决跨域不能共享 Cookie 的问题。

4、Cookie 与 Token 的简单总结

4.1 Cookie 有哪些局限性

  1. Cookie 跨站是不能共享的,这样的话如果你要实现多应用(多系统)的单点登录(SSO),使用 Cookie 来做需要的话就很困难了(要用比较复杂的 trick 来实现)。

    画外音: 所谓单点登录,是指在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。

    但如果用 token 来实现 SSO 会非常简单,如下:) 在这里插入图片描述
    只要在 header 中的 authorize 字段(或其他自定义)加上 token 即可完成所有跨域站点的认证。

  2. 在移动端原生请求是没有 cookie 之说的,而 sessionid 依赖于 cookie,sessionid 就不能用 cookie 来传了,如果用 token 的话,由于它是随着 header 的 authoriize 传过来的,也就不存在此问题,换句话说token 天生支持移动平台,可扩展性好,综上所述,token 具有存储实现简单,扩展性好这些特点。

4.2 token 有哪些缺点

那有人就问了,既然 token 这么好,那为什么各个大公司几乎都采用共享 session 的方式呢,可能很多人是第一次听到 token,token 不香吗? token 有以下两点劣势:

  1. token 太长了

token 是 header, payload 编码后的样式,所以一般要比 sessionId 长很多,很有可能超出 cookie 的大小限制(cookie 一般有大小限制的,如 4kb),如果你在 token 中存储的信息越长,那么 token 本身也会越长,这样的话由于你每次请求都会带上 token,对请求来是个不小的负担。

  1. 不太安全

网上很多文章说 token 更安全,其实不然,细心的你可能发现了,我们说 token 是存在浏览器的,再细问,存在浏览器的哪里?既然它太长放在 cookie 里可能导致 cookie 超限,那就只好放在 local storage 里,这样会造成安全隐患,因为 local storage 这类的本地存储是可以被 JS 直接读取的,另外由上文也提到,token 一旦生成无法让其失效,必须等到其过期才行,这样的话如果服务端检测到了一个安全威胁,也无法使相关的 token 失效。

所以 token 更适合一次性的命令认证,设置一个比较短的有效期。

4.3 误解: Cookie 相比 token 更不安全,比如 CSRF 攻击

首先我们需要解释下 CSRF(跨站请求伪造(英语:Cross-site request forgery))攻击是怎么回事:

攻击者通过一些技术手段欺骗用户的浏览器去访问一个自己曾经认证过的网站并运行一些操作(如发邮件,发消息,甚至财产操作如转账和购买商品)。由于浏览器曾经认证过(cookie 里带来 sessionId 等身份认证的信息),所以被访问的网站会认为是真正的用户操作而去运行。

比如用户登录了某银行网站(假设为 http://www.examplebank.com/ ,并且转账地址为 http://www.examplebank.com/withdraw?amount=1000&transferTo=PayeeName ),登录后 cookie 里会包含登录用户的 sessionid,攻击者可以在另一个网站上放置如下代码:

<img src="http://www.examplebank.com/withdraw?account=Alice&amount=1000&for=Badman">

那么如果正常的用户误点了上面这张图片,由于相同域名的请求会自动带上 cookie,而 cookie 里带有正常登录用户的 sessionid,类似上面这样的转账操作在 server 就会成功,会造成极大的安全风险.
在这里插入图片描述
CSRF 攻击的根本原因在于对于同样域名的每个请求来说,它的 cookie 都会被自动带上,这个是浏览器的机制决定的,所以很多人据此认定 cookie 不安全。

使用 token 确实避免了CSRF 的问题,但正如上文所述,由于 token 保存在 local storage,它会被 JS 读取,从存储角度来看也不安全。(实际上防护 CSRF 攻击的正确方式是用 CSRF token)

所以不管是 cookie 还是 token,从存储角度来看其实都不安全,都有暴露的风险,我们所说的安全更多的是强调传输中的安全,可以用 HTTPS 协议来传输, 这样的话请求头都能被加密,也就保证了传输中的安全。

其实我们把 cookie 和 token 比较本身就不合理,一个是存储方式,一个是验证方式,正确的比较应该是 session vs token。

5、总结

session 和 token 本质上是没有区别的,都是对用户身份的认证机制,只是他们实现的校验机制不一样而已(一个保存在 server,通过在 redis 等中间件获取来校验,一个保存在 client,通过签名校验的方式来校验),多数场景上使用 session 会更合理,但如果在单点登录,一次性命令认证上使用 token 会更合适,最好在不同的业务场景中合理选型,才能达到事半功倍的效果。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值