zoukankan      html  css  js  c++  java
  • Cookie、Session、Token那点事儿和前后端分离之JWT用户认证

    (两篇文章转自:https://www.jianshu.com/p/bd1be47a16c1https://www.jianshu.com/p/180a870a308a

    什么是Cookie?

    Cookie 技术产生源于 HTTP 协议在互联网上的急速发展。随着互联网时代的策马奔腾,带宽等限制不存在了,人们需要更复杂的互联网交互活动,就必须同服务器保持活动状态(简称:保活)。于是,在浏览器发展初期,为了适应用户的需求技术上推出了各种保持 Web 浏览状态的手段,其中就包括了 Cookie 技术。Cookie 在计算机中是个存储在浏览器目录中的文本文件,当浏览器运行时,存储在 RAM 中发挥作用 (此种 Cookies 称作 Session Cookies),一旦用户从该网站或服务器退出,Cookie 可存储在用户本地的硬盘上 (此种 Cookies 称作 Persistent Cookies)。

    Cookie 起源:1993 年,网景公司雇员 Lou Montulli 为了让用户在访问某网站时,进一步提高访问速度,同时也为了进一步实现个人化网络,发明了今天广泛使用的 Cookie。(所以,适当的偷懒也会促进人类计算机发展史的一小步~)

    Cookie时效性:目前有些 Cookie 是临时的,有些则是持续的。临时的 Cookie 只在浏览器上保存一段规定的时间,一旦超过规定的时间,该 Cookie 就会被系统清除。

    Cookie使用限制:Cookie 必须在 HTML 文件的内容输出之前设置;不同的浏览器 (Netscape Navigator、Internet Explorer) 对 Cookie 的处理不一致,使用时一定要考虑;客户端用户如果设置禁止 Cookie,则 Cookie 不能建立。 并且在客户端,一个浏览器能创建的 Cookie 数量最多为 300 个,并且每个不能超过 4KB,每个 Web 站点能设置的 Cookie 总数不能超过 20 个。

    执行流程:

    A:首先,客户端会发送一个http请求到服务器端。

    B: 服务器端接受客户端请求后,发送一个http响应到客户端,这个响应头,其中就包含Set-Cookie头部。

    C:在客户端发起的第二次请求(注意:如果服务器需要我们带上Cookie,我们就需要在B步骤上面拿到这个Cookie然后作为请求头一起发起第二次请求),提供给了服务器端可以用来唯一标识客户端身份的信息。这时,服务器端也就可以判断客户端是否启用了cookies。尽管,用户可能在和应用程序交互的过程中突然禁用cookies的使用,但是,这个情况基本是不太可能发生的,所以可以不加以考虑,这在实践中也被证明是对的。

    为了方便理解,可以先看下这张流程执行图加深概念

     

    客户端与服务端的Cookie流程图

    那么,在浏览器上面的请求头和Cookie在那?下图给大家截取了其中一种。

     

    请求头上面的Cookie

    那么,上面都是谈浏览器上的Cookie,那么在Android开发中,我们该如何去管理和使用Cookie?Okhttp作为经典到爆的网络框架,它的API(本文是基于Okhttp3.0版本以上,3.0以下的版本API有所不同)是通过OkhttpClient中的CookieJar或者拦截器去管理Cookie的。理论上,我们只需在构建单例OkhttpClient的时候,设置cookiejar或者拦截器,然后具体的操作(具体的操作也就是保存Cookie,取Cookie),Okhttp框架就会帮我们自动管理Cookie。如下图:

     

    OKhttp的管理策略

    这是其中一种通过集合的增查特性,就可以简单有效的帮我们管理Cookie。但我们还是要通过源代码去一探究竟。首先,CookieJar是一个接口。

     

    CookieJar

    英文注释翻译过来就是(对应段落翻译):

    CookieJar这个接口为HTTP cookies提供了强大的支持和相关策略。

    这种策略的实现作用会负责选择接受和拒绝那些cookie。一个合理的策略是拒绝所有的cookie,尽管这样会干扰需要cookie的基于会话的自身身份验证方案。

    作为Cookie的持久性,该接口的实现也必须要提供Cookie的存储。一种简单的实现可以将cookie存储在内存中;复杂的系统可以使用文件系统用于保存已接受的cookie的数据库。这里的链接指定cookie存储模型更新和过期的cookie的策略。

    所以,Okhttp的源码告知我们可以将cookie存储在内存中;复杂的系统可以使用文件系统用于保存已接受的cookie的数据库。因此,我们就可以通过Map去简单的管理和使用。

    继续分析CookieJar接口里面的方法,依旧上源码

     

    CookieJar里面的方法

    里面有方法一个是saveFromResponse(HttpUrl url, List cookies)、loadForRequest(HttpUrl url)

    saveFromResponse方法翻译:根据这个jar的方法,可以将cookie从一个HTTP响应保存到这里。请注意,如果响应,此方法可能被称为第二次HTTP响应,包括一个追踪。对于这个隐蔽的HTTP特性,这里的cookie只包含其追踪的cookie。简单点理解就是如果我们使用了这个方法,就会进行追踪(说白了就是客户端请求成功以后,在响应头里面去存cookie)

    loadForRequest方法翻译:将cookie从这个方法加载到一个HTTP请求到指定的url。这个方法从网络上返回的结果可能是一个空集合。简单的实现将返回尚未过期的已接受的cookie去进行匹配。(说白了就是加载url的时候在请求头带上cookie)。

     

    通过CookieJar打印Cookie信息

    这样,我们通过以上代码就可以完成了Cookie的非持久化。什么,非持久化,这又是神马?

    继续给大家科普,在上面说道,Cookie是具有时效性的,所以,Cookie的管理又分为持久化Cookie和非持久化Cookie。非持久化Cookie存储在内存中,也就意味着,其生命周期基本和app保持一致,app关闭后,Cookie丢失。而持久化Cookie则是存储在本地磁盘中,app关闭后不丢失。那么,如果我们要使用Cookie的持久化策略,思想可以参考上面的非持久化策略,只需要将存储方式改一下即可:

    A:通过响应拦截器从response取出cookie并保存到本地,通过请求拦截器从本地取出cookie并添加到请求中

    B:自定义CookieJar,在saveFromResponse()中保存cookie到本地,在loadForRequest()从本地取出cookie。

    那么在这里主要介绍如何通过Okhttp逼格值较高的拦截器去进行持久化cookie操作。(拦截器源码见文章末尾处

     
    保存cookie拦截器-1
     

    保存cookie拦截器-2

    这个SaveCookiesInterceptor拦截器的实现,是首先从response获取set-cookie字段的值,然后通过SharedPreferences保存在本地。

     

    将Cookie添加到请求头

    AddCookiesInterceptor请求拦截器,这个拦截的作用就是判断如果该请求存在cookie,则为其添加到Header的Cookie中。

    写好这两个拦截器之后,我们只需要将实例对象放进OkhttpClient里面即可快速的完成Cookie持久化操作。(PS:这两个拦截器在同步Cookie的时候也是超级好用)。

     

    Okhttp使用cookie拦截器

    拓展:如何通过客户端的cookie与H5上面的cookie进行同步,针对这个问题,给大家推荐笔者的另外一篇文章

    客户端与H5同步Cookie

    Session :

    Session是对于服务端来说的,客户端是没有Session一说的。Session是服务器在和客户端建立连接时添加客户端连接标志,最终会在服务器软件(Apache、Tomcat、JBoss)转化为一个临时Cookie发送给给客户端,当客户端第一请求时服务器会检查是否携带了这个Session(临时Cookie),如果没有则会添加Session,如果有就拿出这个Session来做相关操作。

    在这里引用别人家的一个小故事来加深印象:

            在说session是啥之前,我们先来说说为什么会出现session会话,它出现的机理是什么?    我们知道,我们用浏览器打开一个网页,用到的是HTTP协议,了解计算机的应该都知道这个协议,它是无状态的,什么是无状态呢?就是说这一次请求和上一次请求是没有任何关系的,互不认识的,没有关联的。但是这种无状态的的好处是快速。所以就会带来一个问题就是,我希望几个请求的页面要有关联,比如:我在www.a.com/login.php里面登陆了,我在www.a.com/index.php 也希望是登陆状态,但是,这是2个不同的页面,也就是2个不同的HTTP请求,这2个HTTP请求是无状态的,也就是无关联的,所以无法单纯的在index.php中读取到它在login.php中已经登陆了!        那咋搞呢?我不可能这2个页面我都去登陆一遍吧。或者用笨方法这2个页面都去查询数据库,如果有登陆状态,就判断是登陆的了。这种查询数据库的方案虽然可行,但是每次都要去查询数据库不是个事,会造成数据库的压力。 所以正是这种诉求,这个时候,一个新的客户端存储数据方式出现了:cookie。cookie是把少量的信息存储在用户自己的电脑上,它在一个域名下是一个全局的,只要设置它的存储路径在域名www.a.com下 ,那么当用户用浏览器访问时,php就可以从这个域名的任意页面读取cookie中的信息。所以就很好的解决了我在www.a.com/login.php页面登陆了,我也可以在www.a.com/index.php获取到这个登陆信息了。同时又不用反复去查询数据库。 虽然这种方案很不错,也很快速方便,但是由于cookie 是存在用户端,而且它本身存储的尺寸大小也有限,最关键是用户可以是可见的,并可以随意的修改,很不安全。那如何又要安全,又可以方便的全局读取信息呢?于是,这个时候,一种新的存储会话机制:session 诞生了。      Session 就是在一次会话中解决2次HTTP的请求的关联,让它们产生联系,让2两个页面都能读取到找个这个全局的session信息。session信息存在于服务器端,所以也就很好的解决了安全问题。

    Token :

    token是用户身份的验证方式,我们通常叫它:令牌。最简单的token组成:uid(用户唯一的身份标识)、time(当前时间的时间戳)、sign(签名,由token的前几位+盐以哈希算法压缩成一定长的十六进制字符串,可以防止恶意第三方拼接token请求服务器)。还可以把不变的参数也放进token,避免多次查库。

    应用场景:

    A:当用户首次登录成功(注册也是一种可以适用的场景)之后, 服务器端就会生成一个 token 值,这个值,会在服务器保存token值(保存在数据库中),再将这个token值返回给客户端.

    B:客户端拿到 token 值之后,进行本地保存。(SP存储是大家能够比较支持和易于理解操作的存储)

    C:当客户端再次发送网络请求(一般不是登录请求)的时候,就会将这个 token 值附带到参数中发送给服务器.

    D:服务器接收到客户端的请求之后,会取出token值与保存在本地(数据库)中的token值做对比

    对比一:如果两个 token 值相同, 说明用户登录成功过!当前用户处于登录状态!

    对比二:如果没有这个 token 值, 则说明没有登录成功.

    对比三:如果 token 值不同: 说明原来的登录信息已经失效,让用户重新登录.

     

    Cookie和Session的区别:

    1、cookie数据存放在客户的浏览器上,session数据放在服务器上。

    2、cookie不是很安全,别人可以分析存放在本地的cookie并进行cookie欺骗,考虑到安全应当使用session。

    3、session会在一定时间内保存在服务器上。当访问增多,会比较占用你服务器的性能,考虑到减轻服务器性能方面,应当使用cookie。

    4、单个cookie保存的数据不能超过4K,很多浏览器都限制一个站点最多保存20个cookie。

    5、所以个人建议:

    将登陆信息等重要信息存放为session

    其他信息如果需要保留,可以放在cookie中

    Token 和 Session 的区别:

    session和 token并不矛盾,作为身份认证token安全性比session好,因为每个请求都有签名还能防止监听以及重放攻击,而session就必须靠链路层来保障通讯安全了。如上所说,如果你需要实现有状态的会话,仍然可以增加session来在服务器端保存一些状态

    App通常用restful api跟server打交道。Rest是stateless的,也就是app不需要像browser那样用cookie来保存session,因此用session token来标示自己就够了,session/state由api server的逻辑处理。如果你的后端不是stateless的rest api,那么你可能需要在app里保存session.可以在app里嵌入webkit,用一个隐藏的browser来管理cookie session.

    Session是一种HTTP存储机制,目的是为无状态的HTTP提供的持久机制。所谓Session认证只是简单的把User信息存储到Session里,因为SID的不可预测性,暂且认为是安全的。这是一种认证手段。而Token,如果指的是OAuth Token或类似的机制的话,提供的是 认证 和 授权 ,认证是针对用户,授权是针对App。其目的是让 某App有权利访问 某用户 的信息。这里的Token是唯一的。不可以转移到其它App上,也不可以转到其它 用户 上。转过来说Session。Session只提供一种简单的认证,即有此SID,即认为有此User的全部权利。是需要严格保密的,这个数据应该只保存在站方,不应该共享给其它网站或者第三方App。所以简单来说,如果你的用户数据可能需要和第三方共享,或者允许第三方调用API接口,用Token。如果永远只是自己的网站,自己的App,用什么就无所谓了。

    token就是令牌,比如你授权(登录)一个程序时,他就是个依据,判断你是否已经授权该软件;cookie就是写在客户端的一个txt文件,里面包括你登录信息之类的,这样你下次在登录某个网站,就会自动调用cookie自动登录用户名;session和cookie差不多,只是session是写在服务器端的文件,也需要在客户端写入cookie文件,但是文件里是你的浏览器编号.Session的状态是存储在服务器端,客户端只有session id;而Token的状态是存储在客户端。


    在前后端分离开发时为什么需要用户认证呢?原因是由于HTTP协定是不储存状态的(stateless),这意味着当我们透过帐号密码验证一个使用者时,当下一个request请求时它就把刚刚的资料忘了。于是我们的程序就不知道谁是谁,就要再验证一次。所以为了保证系统安全,我们就需要验证用户否处于登录状态。

    传统方式

    前后端分离通过Restful API进行数据交互时,如何验证用户的登录信息及权限。在原来的项目中,使用的是最传统也是最简单的方式,前端登录,后端根据用户信息生成一个token,并保存这个 token 和对应的用户id到数据库或Session中,接着把 token 传给用户,存入浏览器 cookie,之后浏览器请求带上这个cookie,后端根据这个cookie值来查询用户,验证是否过期。

    但这样做问题就很多,如果我们的页面出现了 XSS 漏洞,由于 cookie 可以被 JavaScript 读取,XSS 漏洞会导致用户 token 泄露,而作为后端识别用户的标识,cookie 的泄露意味着用户信息不再安全。尽管我们通过转义输出内容,使用 CDN 等可以尽量避免 XSS 注入,但谁也不能保证在大型的项目中不会出现这个问题。

    在设置 cookie 的时候,其实你还可以设置 httpOnly 以及 secure 项。设置 httpOnly 后 cookie 将不能被 JS 读取,浏览器会自动的把它加在请求的 header 当中,设置 secure 的话,cookie 就只允许通过 HTTPS 传输。secure 选项可以过滤掉一些使用 HTTP 协议的 XSS 注入,但并不能完全阻止。

    httpOnly 选项使得 JS 不能读取到 cookie,那么 XSS 注入的问题也基本不用担心了。但设置 httpOnly 就带来了另一个问题,就是很容易的被 XSRF,即跨站请求伪造。当你浏览器开着这个页面的时候,另一个页面可以很容易的跨站请求这个页面的内容。因为 cookie 默认被发了出去。

    另外,如果将验证信息保存在数据库中,后端每次都需要根据token查出用户id,这就增加了数据库的查询和存储开销。若把验证信息保存在session中,有加大了服务器端的存储压力。那我们可不可以不要服务器去查询呢?如果我们生成token遵循一定的规律,比如我们使用对称加密算法来加密用户id形成token,那么服务端以后其实只要解密该token就可以知道用户的id是什么了。不过呢,我只是举个例子而已,要是真这么做,只要你的对称加密算法泄露了,其他人可以通过这种加密方式进行伪造token,那么所有用户信息都不再安全了。恩,那用非对称加密算法来做呢,其实现在有个规范就是这样做的,就是我们接下来要介绍的 JWT。

    Json Web Token(JWT)

    JWT 是一个开放标准(RFC 7519),它定义了一种用于简洁,自包含的用于通信双方之间以 JSON 对象的形式安全传递信息的方法。JWT 可以使用 HMAC 算法或者是 RSA 的公钥密钥对进行签名。它具备两个特点:

    • 简洁(Compact)

      可以通过URL, POST 参数或者在 HTTP header 发送,因为数据量小,传输速度快

    • 自包含(Self-contained)

      负载中包含了所有用户所需要的信息,避免了多次查询数据库

    JWT 组成

     
    • Header 头部

    头部包含了两部分,token 类型和采用的加密算法

    { "alg": "HS256", "typ": "JWT" }

    它会使用 Base64 编码组成 JWT 结构的第一部分,如果你使用Node.js,可以用Node.js的包base64url来得到这个字符串。

    Base64是一种编码,也就是说,它是可以被翻译回原来的样子来的。它并不是一种加密过程。

    • Payload 负载

    这部分就是我们存放信息的地方了,你可以把用户 ID 等信息放在这里,JWT 规范里面对这部分有进行了比较详细的介绍,常用的由 iss(签发者),exp(过期时间),sub(面向的用户),aud(接收方),iat(签发时间)。

    { "iss": "lion1ou JWT", "iat": 1441593502, "exp": 1441594722, "aud": "www.example.com", "sub": "lion1ou@163.com" }

    同样的,它会使用 Base64 编码组成 JWT 结构的第二部分

    • Signature 签名

    前面两部分都是使用 Base64 进行编码的,即前端可以解开知道里面的信息。Signature 需要使用编码后的 header 和 payload 以及我们提供的一个密钥,然后使用 header 中指定的签名算法(HS256)进行签名。签名的作用是保证 JWT 没有被篡改过。

    三个部分通过.连接在一起就是我们的 JWT 了,它可能长这个样子,长度貌似和你的加密算法和私钥有关系。

    eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjU3ZmVmMTY0ZTU0YWY2NGZmYzUzZGJkNSIsInhzcmYiOiI0ZWE1YzUwOGE2NTY2ZTc2MjQwNTQzZjhmZWIwNmZkNDU3Nzc3YmUzOTU0OWM0MDE2NDM2YWZkYTY1ZDIzMzBlIiwiaWF0IjoxNDc2NDI3OTMzfQ.PA3QjeyZSUh7H0GfE0vJaKW4LjKJuC3dVLQiY4hii8s

    其实到这一步可能就有人会想了,HTTP 请求总会带上 token,这样这个 token 传来传去占用不必要的带宽啊。如果你这么想了,那你可以去了解下 HTTP2,HTTP2 对头部进行了压缩,相信也解决了这个问题。

    • 签名的目的

    最后一步签名的过程,实际上是对头部以及负载内容进行签名,防止内容被窜改。如果有人对头部以及负载的内容解码之后进行修改,再进行编码,最后加上之前的签名组合形成新的JWT的话,那么服务器端会判断出新的头部和负载形成的签名和JWT附带上的签名是不一样的。如果要对新的头部和负载进行签名,在不知道服务器加密时用的密钥的话,得出来的签名也是不一样的。

    • 信息暴露

    在这里大家一定会问一个问题:Base64是一种编码,是可逆的,那么我的信息不就被暴露了吗?

    是的。所以,在JWT中,不应该在负载里面加入任何敏感的数据。在上面的例子中,我们传输的是用户的User ID。这个值实际上不是什么敏感内容,一般情况下被知道也是安全的。但是像密码这样的内容就不能被放在JWT中了。如果将用户的密码放在了JWT中,那么怀有恶意的第三方通过Base64解码就能很快地知道你的密码了。

    因此JWT适合用于向Web应用传递一些非敏感信息。JWT还经常用于设计用户认证和授权系统,甚至实现Web应用的单点登录。

    JWT 使用

     
    1. 首先,前端通过Web表单将自己的用户名和密码发送到后端的接口。这一过程一般是一个HTTP POST请求。建议的方式是通过SSL加密的传输(https协议),从而避免敏感信息被嗅探。

    2. 后端核对用户名和密码成功后,将用户的id等其他信息作为JWT Payload(负载),将其与头部分别进行Base64编码拼接后签名,形成一个JWT。形成的JWT就是一个形同lll.zzz.xxx的字符串。

    3. 后端将JWT字符串作为登录成功的返回结果返回给前端。前端可以将返回的结果保存在localStorage或sessionStorage上,退出登录时前端删除保存的JWT即可。

    4. 前端在每次请求时将JWT放入HTTP Header中的Authorization位。(解决XSS和XSRF问题)

    5. 后端检查是否存在,如存在验证JWT的有效性。例如,检查签名是否正确;检查Token是否过期;检查Token的接收方是否是自己(可选)。

    6. 验证通过后后端使用JWT中包含的用户信息进行其他逻辑操作,返回相应结果。

    和Session方式存储id的差异

    Session方式存储用户id的最大弊病在于Session是存储在服务器端的,所以需要占用大量服务器内存,对于较大型应用而言可能还要保存许多的状态。一般而言,大型应用还需要借助一些KV数据库和一系列缓存机制来实现Session的存储。

    而JWT方式将用户状态分散到了客户端中,可以明显减轻服务端的内存压力。除了用户id之外,还可以存储其他的和用户相关的信息,例如该用户是否是管理员、用户所在的分组等。虽说JWT方式让服务器有一些计算压力(例如加密、编码和解码),但是这些压力相比磁盘存储而言可能就不算什么了。具体是否采用,需要在不同场景下用数据说话。

    • 单点登录

    Session方式来存储用户id,一开始用户的Session只会存储在一台服务器上。对于有多个子域名的站点,每个子域名至少会对应一台不同的服务器,例如:www.taobao.comnv.taobao.comnz.taobao.comlogin.taobao.com。所以如果要实现在login.taobao.com登录后,在其他的子域名下依然可以取到Session,这要求我们在多台服务器上同步Session。使用JWT的方式则没有这个问题的存在,因为用户的状态已经被传送到了客户端。

    总结

    JWT的主要作用在于(一)可附带用户信息,后端直接通过JWT获取相关信息。(二)使用本地保存,通过HTTP Header中的Authorization位提交验证。但其实关于JWT存放到哪里一直有很多讨论,有人说存放到本地存储,有人说存 cookie。个人偏向于放在本地存储,如果你有什么意见和看法欢迎提出。


    综上所述:
    1、Cookie:保存在客户端,不是很安全;
    2、Session:保存在服务器端,并生成一个Session id保存在客户端。访问过多时会占用服务器的内存和性能;
    3、Token:首次登陆后,服务器生成Token值,保存在数据库中,再将这个Token值返回给客户端。增加数据库的存储和查询压力;
    4、JWT(Json Web Token):前后端分离项目中使用。根据算法生成,保存在本地。消耗服务器的计算压力。
     
     
  • 相关阅读:
    Symfony Component HttpKernel Exception AccessDeniedHttpException This action is unauthorized.
    AngularJs ng-repeat中使用ng-model
    JS数组排序sort()方法同时按照两种方式排序的用法
    MongoDB
    Node基本学习
    小程序 五 组件使用
    小程序 四 事件类型
    小程序 二 template模板(代码复用)
    小程序 wxs 脚本语言(2种使用方式)
    小程序初体验 wx:for
  • 原文地址:https://www.cnblogs.com/linyuhong/p/9968056.html
Copyright © 2011-2022 走看看