十年网站开发经验 + 多家企业客户 + 靠谱的建站团队
量身定制 + 运营维护+专业推广+无忧售后,网站问题一站解决
在现代web开发中,系统鉴权服务已是基本标配模块,有些开发框架甚至内置了鉴权模块的实现,或者提供一些鉴权的工具类,然而鉴权的方式也分为多种,了解各种鉴权方式的特点及使用场景可以帮助我们构建更健壮的web系统。以下列出四种常见的鉴权方式,我们来认识一下:
成都创新互联2013年至今,是专业互联网技术服务公司,拥有项目网站设计、网站制作网站策划,项目实施与项目整合能力。我们以让每一个梦想脱颖而出为使命,1280元项城做网站,已为上家服务,为项城各地企业和个人服务,联系电话:028-86922220
HTTP 基本身份验证,允许客户端在标准的 HTTP 头中发送用户名和密码。服务端可以验证这些信息,并确认客户端是否有权访问服务。这样做的好处在于,这是一种非常容易理解且得到广泛支持的协议。问题在于,通过 HTTP 有很高的风险,因为用户名和密码并没有以安全的方式发送。任何中间方都可以看到 HTTP 头的信息并读取里面的数据。因此,HTTP 基本身份验证通常应该通过 HTTPS 进行通信。
当使用 HTTPS 时,客户端获得强有力的保证,它所通信的服务端就是客户端想要通信的服务端。它给予我们额外的保护,避免人们窃听客户端和服务端之间的通信,或篡改有效负载。
服务端需要管理自己的SSL证书,当需要管理多台机器时会出现问题。一些组织自己承担签发证书的过程,这是一个额外的行政和运营负担。管理这方面的自动化工具远不够成熟,使用它们后你会发现,需要自己处理的事情就不止证书签发了。自签名证书不容易撤销,因此需要对灾难情景有更多的考虑。看看你是否能够避免自签名,以避开所有的这些工作。
SSL 之上的流量不能被反向代理服务器(比如 Varnish 或 Squid)所缓存,这是使用 HTTPS 的另一个缺点。这意味着,如果你需要缓存信息,就不得不在服务端或客户端内部实现。你可以在负载均衡中把 Https 的请求转成 Http 的请求,然后在负载均衡之后就可以使用缓存了。
还需要考虑,如果我们已经在使用现成的 SSO 方案(比如包含用户名密码信息的 SAML),该怎么办。我们想要基本身份验证使用同一套认证信息,然后在同一个进程里颁发和撤销吗?让服务与实现 SSO 所使用的那个目录服务进行通信即可做到这一点。或者,我们可以在服务内部存储用户名和密码,但需要承担存在重复行为的风险。
注意:使用这种方法,服务器只知道客户端有用户名和密码。我们不知道这个信息是否来自我们期望的机器;它可能来自网络中的其他人。
HTTP 基本身份验证是一种简单但不那么安全的认证方式,不太建议用于公开的商业应用,在此便不再展开,我们关注以下几种认证方式。
http协议是一种无状态的协议,如果没有任何认证机制,服务端对任何客户端的请求都是无差别的。在Web2.0时代,为了加强B/S交互的安全性,衍生出了Session-Cookie鉴权机制,通过在服务端开启会话,客户端存储SessionID,在每次请求时通过cookie传输SessionID的形式实现服务端基本鉴权。
cookie是保存在本地终端的数据。cookie由服务器生成,发送给浏览器,浏览器把cookie以kv形式保存到某个目录下的文本文件内,下一次请求同一网站时会把该cookie发送给服务器。由于cookie是存在客户端上的,所以浏览器加入了一些限制确保cookie不会被恶意使用,同时不会占据太多磁盘空间,所以每个域的cookie数量是有限的。
cookie的组成有:名称(key)、值(value)、有效域(domain)、路径(域的路径,一般设置为全局:"")、失效时间、安全标志(指定后,cookie只有在使用SSL连接时才发送到服务器(https))。
Session的中文翻译是“会话”,当用户打开某个web应用时,便与web服务器产生一次session。服务器使用session把用户的信息临时保存在了服务器上,用户离开网站后session会被销毁。这种用户信息存储方式相对cookie来说更安全,可是session有一个缺陷:如果web服务器做了负载均衡,那么下一个操作请求到了另一台服务器的时候session会丢失。
当程序需要为某个客户端的请求创建一个session时,服务器首先检查这个客户端的请求里是否已包含了一个session标识(称为SessionID),如果已包含则说明以前已经为此客户端创建过Session,服务器就按照SessionID把这个Session检索出来使用(检索不到,会新建一个),如果客户端请求不包含SessionID,则为此客户端创建一个Session并且生成一个与此Session相关联的SessionID,SessionID的值应该是一个既不会重复,又不容易被找到规律以仿造的字符串,这个SessionID将被在本次响应中返回给客户端保存。
保存这个SessionID的方式可以采用Cookie,这样在交互过程中浏览器可以自动的按照规则把这个标识发挥给服务器。一般这个Cookie的名字都是类似于SEEESIONID。但Cookie可以被人为的禁止,则必须有其他机制以便在Cookie被禁止时仍然能够把SessionID传递回服务器。
客户端第一次发送请求给服务器,此时服务器启动Session会话,产生一个唯一的SessionID,并通过Response的SetCookie返回给客户端,保存于客户端(一般为浏览器),并与一个浏览器窗口对应着,由于HTTP协议的特性,这一次Request-Response
后连接就断开了。以后此客户端再发送请求给服务器的时候,就会在请求Request头中携带cookie,由于cookie中带有Key为sessionID的数据,所以服务器就知道这是刚才那个客户端。
正如我们前面所讨论的,如果担心用户名和密码被泄露,HTTP基本身份验证使用普通 HTTP 并不是非常明智的。传统的替代方式是使用HTTPS路由通信,但也有一些缺点。除了需要管理证书,HTTPS通信的开销使得服务器压力增加,而且通信难以被轻松地缓存。另外Session-Cookie机制也会有被客户限制的隐患,如果用户禁用Cookie则必须由其它方式实现鉴权。
所谓Token,即令牌。客户端需要鉴权访问私人信息时,会首次向服务端发送身份验证信息(如用户名、密码),服务端校验正确后会根据一定的加密算法生成Token令牌发放给客户端,此后客户端只需通过Token,服务端只需验证Token就可识别客户并进行交互,Token可存放于HTTP Header也可存放与Cookie。
以上为一个简单的Token鉴权过程。
关于Token机制,业界有一种叫JWT(JsonWebToken)的实现机制,下面我们来了解JWT。
JWT.io 对JSON Web Tokens进行了很好的介绍,
国内阮一峰的 《JSON Web Token 入门教程》 也讲得非常好懂,可以出门右拐了解一下。
简而言之,它是一个签名的JSON对象,可以执行一些有用的操作(例如,身份验证)。它是一组字串,分Header(头部)、Payload(负载)、Signature(签名)三部分,由'.'号连接,看起来就像下面这样:
用户发送认证信息给服务端后,服务端通过JWT生成规则,生成JWT字串作为Token发放给用户,用户以后每次访问都在HTTP Header携带JWT字串,已达到鉴权目的。由于其内部携带用户信息,部分使用者已经发现其安全隐患,但其安全度不至于太过容易破解,在移动应用中的鉴权机制使用较多,除此之外,一些分布式的微服务应用也通过JWT进行模块间的鉴权,还是有一定的使用场景的。
Go开源社区已有比较成熟的JWT包实现: jwt-go ,内附有JWT编解码的使用用例,还是很好懂的,感兴趣的可get来使用。在另一篇中也做了Go 使用JWT鉴权的示例: 《Go 鉴权(三):JWT》 ,感兴趣可阅读以下,自己也在项目中实践一下。
OAUTH协议为用户资源的授权提供了一个安全的、开放而又简易的标准。同时,任何第三方都可以使用OAUTH认证服务,任何服务提供商都可以实现自身的OAUTH认证服务,因而OAUTH是开放的。
OpenID Connect 是 OAuth 2.0 具体实现中的一个标准。它使用简单的 REST 调用,因为提高了其易用性。对于一个面向公众的网站,你或许可以使用Google、Facebook、Github等作为提供者,国内可以使用QQ、微信、淘宝等作为提供者。但对于内部系统,或对于数据需要有更多控制权的系统而言,你会希望有自己的内部身份提供者。
OAuth2.0有四种授权模式,具体可看阮一峰的 《理解OAuth2.0》 ,其内容非常详细且好理解。
我们这里说一下最完整的授权码模式:
以上为OAuth2.0的认证过程。
各大厂都有提供基于OAuth2.0的三方授权服务,如QQ、微信、淘宝等等,有需要可移步到各自的开放平台查看文档,大都有提供Go的接口实现;另你也可参考使用Go官方提供实现的包 ,里面包含多数热门的OAuth客户端。
推荐使用 这个开源项目,帮助你构建自己的OAuth服务
OAuth2是一个关于授权的开放标准,核心思路是通过各类认证手段(具体什么手段OAuth2不关心)认证用户身份,并颁发token(令牌),使得第三方应用可以使用该令牌在 限定时间 、 限定范围 访问指定资源。主要涉及的RFC规范有 RFC6749 (整体授权框架), RFC6750 (令牌使用), RFC6819 (威胁模型)这几个,一般我们需要了解的就是 RFC6749 。获取令牌的方式主要有四种,分别是 授权码模式 , 简单模式 , 密码模式 和 客户端模式 ,如何获取token不在本篇文章的讨论范围,我们这里假定客户端已经通过某种方式获取到了access_token,想了解具体的oauth2授权步骤可以移步阮一峰老师的 理解OAuth 2.0 ,里面有非常详细的说明。
这里要先明确几个OAuth2中的几个重要概念:
明确概念后,就可以看OAuth2的协议握手流程,摘自RFC6749
Spring Security是一套安全框架,可以基于RBAC(基于角色的权限控制)对用户的访问权限进行控制,核心思想是通过一系列的filter chain来进行拦截过滤,以下是ss中默认的内置过滤器列表,当然你也可以通过 custom-filter 来自定义扩展filter chain列表
这里面最核心的就是 FILTER_SECURITY_INTERCEPTOR ,通过 FilterInvocationSecurityMetadataSource 来进行资源权限的匹配, AccessDecisionManager 来执行访问策略。
一般意义来说的应用访问安全性,都是围绕认证(Authentication)和授权(Authorization)这两个核心概念来展开的。即首先需要确定用户身份,在确定这个用户是否有访问指定资源的权限。认证这块的解决方案很多,主流的有 CAS 、 SAML2 、 OAUTH2 等(不巧这几个都用过-_-),我们常说的单点登录方案(SSO)说的就是这块,授权的话主流的就是spring security和shiro。shiro我没用过,据说是比较轻量级,相比较而言spring security确实架构比较复杂。
将OAuth2和Spring Security集成,就可以得到一套完整的安全解决方案。
为了便于理解,现在假设有一个名叫“脸盆网”的社交网站,用户在首次登陆时会要求导入用户在facebook的好友列表,以便于快速建立社交关系。具体的授权流程如下:
不难看出,这个假设的场景中,脸盆网就是第三方应用(client),而facebook既充当了认证服务器,又充当了资源服务器。这个流程里面有几个比较重要的关键点,我需要重点说一下,而这也是其他的涉及spring security与OAuth2整合的文章中很少提及的,很容易云里雾里的地方。
细心的同学应该发现了,其实在标准的OAuth2授权过程中,5、6、8这几步都不是必须的,从上面贴的 RFC6749 规范来看,只要有1、2、3、4、7这几步,就完成了被保护资源访问的整个过程。事实上, RFC6749 协议规范本身也并不关心用户身份的部分,它只关心token如何颁发,如何续签,如何用token访问被保护资源(facebook只要保证返回给脸盆网的就是当前用户的好友,至于当前用户是谁脸盆网不需要关心)。那为什么spring security还要做5、6这两步呢?这是因为spring security是一套完整的安全框架,它必须关心用户身份!在实际的使用场景中,OAuth2一般不仅仅用来进行被保护资源的访问,还会被用来做单点登陆(SSO)。在SSO的场景中,用户身份无疑就是核心,而token本身是不携带用户信息的,这样client就没法知道认证服务器发的token到底对应的是哪个用户。设想一下这个场景,脸盆网不想自建用户体系了,想直接用facebook的用户体系,facebook的用户和脸盆网的用户一一对应(其实在很多中小网站现在都是这种模式,可以选择使用微信、QQ、微博等网站的用户直接登陆),这种情况下,脸盆网在通过OAuth2的认证后,就希望拿到用户信息了。所以现在一般主流的OAuth2认证实现,都会预留一个用户信息获取接口,就是上面提到的 (虽然这不是OAuth2授权流程中必须的),这样client在拿到token后,就可以携带token通过这个接口获取用户信息,完成SSO的整个过程。另外从用户体验的角度来说,如果获取不到用户信息,则意味者每次要从脸盆网访问facebook的资源,都需要重定向一次进行认证,用户体验也不好。
首先要明确一点, OAuth2并不是一个SSO框架,但可以实现SSO功能 。以下是一个使用github作为OAuth2认证服务器的配置文件
可以看到 accessTokenUri 和 userAuthorizationUri 都是为了完成OAuth2的授权流程所必须的配置,而 userInfoUri 则是spring security框架为了完成SSO所必须要的。所以总结一下就是: 通过将用户信息这个资源设置为被保护资源,可以使用OAuth2技术实现单点登陆(SSO),而Spring Security OAuth2就是这种OAuth2 SSO方案的一个实现。
Spring Security在调用user接口成功后,会构造一个 OAuth2Authentication 对象,这个对象是我们通常使用的 UsernamePasswordAuthenticationToken 对象的一个超集,里面封装了一个标准的 UsernamePasswordAuthenticationToken ,同时在 detail 中还携带了OAuth2认证中需要用到的一些关键信息(比如 tokenValue , tokenType 等),这时候就完成了SSO的登陆认证过程。后续用户如果再想访问被保护资源,spring security只需要从principal中取出这个用户的token,再去访问资源服务器就行了,而不需要每次进行用户授权。这里要注意的一点是 此时浏览器与client之间仍然是通过传统的cookie-session机制来保持会话,而非通过token。实际上在SSO的过程中,使用到token访问的只有client与resource server之间获取user信息那一次,token的信息是保存在client的session中的,而不是在用户本地 。这也是之前我没搞清楚的地方,以为浏览器和client之间也是使用token,绕了不少弯路,对于Spring Security来说, 不管是用cas、saml2还是Oauth2来实现SSO,最后和用户建立会话保持的方式都是一样的 。
根据前面所说,大家不难看出,OAuth2的SSO方案和CAS、SAML2这样的纯SSO框架是有本质区别的。在CAS和SAML2中,没有资源服务器的概念,只有认证客户端(需要验证客户信息的应用)和认证服务器(提供认证服务的应用)的概念。在CAS中这叫做 cas-client 和 cas-server ,SAML2中这叫做 Service Providers 和 Identity Provider ,可以看出CAS、SAML2规范天生就是为SSO设计的,在报文结构上都考虑到了用户信息的问题(SAML2规范甚至还带了权限信息),而OAuth2本身不是专门为SSO设计的,主要是为了解决资源第三方授权访问的问题,所以在用户信息方面,还需要额外提供一个接口。
脸盆网的这个例子中,我们看到资源服务器和认证服务器是在一起的(都是facebook),在互联网场景下一般你很难找到一个独立的、权威的、第三方的认证中心(你很难想像腾讯的QQ空间通过支付宝的认证中心去授权,也很难想像使用谷歌服务要通过亚马逊去授权)。但是如果是在公司内部,这种场景其实是很多的,尤其在微服务架构下,有大量服务会对外提供资源访问,他们都需要做权限控制。那么最合理的当然就是建立一个统一的认证中心,而不是每个服务都做一个认证中心。我们前面也介绍了,token本身是不携带用户信息的,在分离后resouce server在收到请求后,如何检验token的真实性?又如何从token中获取对应的用户信息?这部分的介绍网上其实非常少,幸好我们可以直接从官方文档获取相关的蛛丝马迹,官方文档对于resouce server的配置是这样描述的:
寥寥数语,但已经足够我们分析了。从这个配置可以看出,client在访问resource server的被保护资源时,如果没有携带token,则资源服务器直接返回一个401未认证的错误
如果携带了token,则资源服务器会使用这个token向认证服务器发起一个用户查询的请求,若token错误或已经失效,则会返回
若token验证成功,则认证服务器向资源服务器返回对应的用户信息,此时resource server的spring security安全框架就可以按照标准的授权流程进行访问权限控制了。
从这个流程中我们可以看出,通过OAuth2进行SSO认证,有一个好处是做到了 认证与授权的解耦 。从日常的使用场景来说,认证比较容易做到统一和抽象,毕竟你就是你,走到哪里都是你,但是你在不同系统里面的角色,却可能千差万别(家里你是父亲,单位里你是员工,父母那里你是子女)。同时角色的设计,又是和资源服务器的设计强相关的。从前面的配置中不难发现,如果希望获得为不同资源服务器设计的角色,你只需要替换 这个配置就行了,这为我们的权限控制带来了更大的灵活性,而这是传统的比如SAML2这样的SSO框架做不到的。
终于来到了著名的JWT部分了,JWT全称为Json Web Token,最近随着微服务架构的流行而越来越火,号称新一代的认证技术。今天我们就来看一下,jwt的本质到底是什么。
我们先来看一下OAuth2的token技术有没有什么痛点,相信从之前的介绍中你也发现了,token技术最大的问题是 不携带用户信息 ,且资源服务器无法进行本地验证,每次对于资源的访问,资源服务器都需要向认证服务器发起请求,一是验证token的有效性,二是获取token对应的用户信息。如果有大量的此类请求,无疑处理效率是很低的,且认证服务器会变成一个中心节点,对于SLA和处理性能等均有很高的要求,这在分布式架构下是很要命的。
JWT就是在这样的背景下诞生的,从本质上来说,jwt就是一种 特殊格式 的token。普通的oauth2颁发的就是一串随机hash字符串,本身无意义,而jwt格式的token是有特定含义的,分为三部分:
这三部分均用base64进行编码,当中用 . 进行分隔,一个典型的jwt格式的token类似 xxxxx.yyyyy.zzzzz 。关于jwt格式的更多具体说明,不是本文讨论的重点,大家可以直接去官网查看 官方文档 ,这里不过多赘述。
相信看到签名大家都很熟悉了,没错,jwt其实并不是什么高深莫测的技术,相反非常简单。认证服务器通过对称或非对称的加密方式利用 payload 生成 signature ,并在 header 中申明签名方式,仅此而已。通过这种本质上极其传统的方式,jwt可以实现 分布式的token验证功能 ,即资源服务器通过事先维护好的对称或者非对称密钥(非对称的话就是认证服务器提供的公钥),直接在本地验证token,这种去中心化的验证机制无疑很对现在分布式架构的胃口。jwt相对于传统的token来说,解决以下两个痛点:
在上面的那个资源服务器和认证服务器分离的例子中,如果认证服务器颁发的是jwt格式的token,那么资源服务器就可以直接自己验证token的有效性并绑定用户,这无疑大大提升了处理效率且减少了单点隐患。
就像布鲁克斯在《人月神话》中所说的名言一样:“没有银弹”。JWT的使用上现在也有一种误区,认为传统的认证方式都应该被jwt取代。事实上,jwt也不能解决一切问题,它也有适用场景和不适用场景。
适用场景:
这些场景能充分发挥jwt无状态以及分布式验证的优势
不适用的场景:
不要试图用jwt去代替session。 这种模式下其实传统的session+cookie机制工作的更好,jwt因为其无状态和分布式,事实上只要在有效期内,是无法作废的,用户的签退更多是一个客户端的签退,服务端token仍然有效,你只要使用这个token,仍然可以登陆系统。另外一个问题是续签问题,使用token,无疑令续签变得十分麻烦,当然你也可以通过redis去记录token状态,并在用户访问后更新这个状态,但这就是硬生生把jwt的无状态搞成有状态了,而这些在传统的session+cookie机制中都是不需要去考虑的。这种场景下,考虑高可用,我更加推荐采用分布式的session机制,现在已经有很多的成熟框架可供选择了(比如spring session)。
这个库已经没人维护了,他们现在新搞了一个,具体可以去github上看
jwt(json web token)是一种用于前后端身份认证的方法,一个jwt由header,payload,和signature组成。
1.Claims
claims是一个实现了Valid方法的interface,Valid方法用于判断该claim是否合法
2.Keyfunc
Keyfunc在使用时一般都是返回secret密钥,可以根据Token的种类不同返回不同的密钥.
官方文档:This allows you to use properties in the Header of the token (such as 'kid') to identify which key to use.
3.Mapclaims
一个用于放decode出来的claim的map,有Vaild和一系列VerifyXXX的方法
4.Parser
用来将tokenstr转换成token
5.SigningMethod
签名方法的接口,可以通过实现这个接口自定义签名方法,jwt-go内置一些实现了SigningMethod的结构体
6.StandardClaims
jwt官方规定的一些预定义的payload:
7.Token
Token的结构体
8.ValidationError
定义解析Token时遇到的一些错误
首先我们先来看Parse()
实际上是调用了ParseWithClaims,第二个参数就是一个map[string]interface,这个函数的源码如下,这个函数内部调用的ParseUnverified,我们先来看看这个函数
官方的解释是,这个函数不校验签名的有效性,只单纯负责把tokenStr变成Token对象,而之后的事情就是交给ParseWithClaims来做啦
可以看到,ParseUnverified这个方法真的只是单纯的解码Header段和Claim段,然后检查一下用的alg是不是合法,就返回了,让我们继续往下看验证的逻辑
ok,关于解析token的主要方法我们已经看完了,接下来我们来看看如何生成一个token,其实就是反着操作一遍
先看New函数,选择一种SigningMethod,新建一个token,内部调用NewWithClaims
再看NewWithClaims,发现就是简单的给JwtToken的三个部分赋值
最后是SignedString,即使用alg的算法给token加密,生成最终的tokenStr,内部调用了SigningString,所以先看SigningString
发现SigningString就是把token的头部先变成json然后base64url编码,但是没有生成jwtToken的最后一个部分
所以SignedString作用就是用给定的加密方法和你的SecretKey对前面两部分加密,添在token的最后一段,至此token生成完毕
HTTP无状态协议 ,是指协议对于事务处理没有记忆 能力 ( 浏览器对于事物的处理没有记忆功能 )。缺少状态意味着如果后续处理需要前面的信息,则它必须重传,这样可能导致每次连接传送的数据量增大
为什么说HTTP是无状态的协议呢?
因为它的每个请求都是完全独立的,每个请求包含了处理这个请求所需的完整的数据,发送请求不涉及到状态变更。
即使在HTTP/1.1上,同一个连接允许传输多个HTTP请求的情况下,如果第一个请求出错了,后面的请求一般也能够继续处理(当然,如果导致协议解析失败、消息分片错误之类的自然是要除外的)可以看出,这种协议的结构是要比有状态的协议更简单的,一般来说实现起来也更简单,不需要使用状态机,一个循环就行了
优点
HTTP这样的无状态协议,使用元数据(如Cookies头)来维护会话,使得会话与连接本身独立起来,这样即使连接断开了,会话状态也不会受到严重伤害,保持会话也不需要保持连接本身。
另外,无状态的优点还在于对中间件友好,中间件无需完全理解通信双方的交互过程,只需要能正确分片消息即可,而且中间件可以很方便地将消息在不同的连接上传输而不影响正确性,这就方便了负载均衡等组件的设计
缺点
单个请求需要的所有信息都必须要包含在请求中一次发送到服务端,这导致单个消息的结构需要比较复杂,必须能够支持大量元数据,因此HTTP消息的解析要比其他许多协议都要复杂得多。
同时,这也导致了相同的数据在多个请求上往往需要反复传输,例如同一个连接上的每个请求都需要传输Host、Authentication、Cookies、Server等往往是完全重复的元数据,一定程度上降低了协议的效率。
HTTP就是一种无状态协议,它对用户的操作没有记忆功能。为了解决这种情况,引出了CookieSession机制,让浏览器具备了记忆功能
在此之后,客户端每次再向同一个网站发送请求时,请求头部都会带上cookie信息(包含了sessionid)
服务端在收到请求后,读取cookie信息,获取sessionid,在session空间中找到对应的信息进行比对
JWT(JSON Web Token)
JWT的功能也是让浏览器具有了记忆功能
JWT是保存在客户端的,广泛应用于单点登录
JWT特点:
HTTP协议是无状态协议,这句话本身到底对不对?
实际上,并不全对。HTTP/1.1中有一个Expect: 100-Continue的功能,它是这么工作的:
可以看出,这实际上很明显是一个有状态协议的套路,它需要先进行一次握手,然后再真正发送数据。不过,HTTP协议也规定,如果服务端不进行100 Continue的响应,建议客户端在等待较短的时间之后仍然上传数据,以达成与不支持Expect: 100-Continue功能的服务器的兼容,这样可以算是“能有状态则有状态,否则回到无状态的路上”,这样说HTTP 1.x是无状态的协议也是没错的。
至于HTTP/2,它应该算是一个有状态的协议了(有握手和GOAWAY消息,有类似于TCP的流控),所以以后说“HTTP是无状态的协议”就不太对了,最好说“HTTP 1.x是无状态的协议”
参考: