认证与会话安全(五)
一、会话及会话管理
会话是指客户端与服务端一系列交互过程,这一系列的交互过程可能包含很多次请求和响应,由于HTTP协议是无状态的,服务器为了识别请求来自哪个客户端用户,引入了会话机制。
通常使用Cookie与Session来实现Web应用的会话管理,Cookie通过在客户端记录信息确定用户身份,Session通过在服务器端记录信息确定用户身份。
Cookie机制基本思想就是让浏览器记录一组服务器特有的信息,每次访问服务器时将这些信息提供给服务器。Cookie是由服务端生成,返回给客户端,并由客户端存储在本地,后面的请求时,将Cookie信息携带传递给服务端,服务端进行检查Cookie状态。从Cookie的过期时间上划分,可以分为会话Cookie和持久Cookie,会话Cookie存储在内存中,当关闭浏览器时,Cookie就会消失。持久Cookie存储在硬盘中,不会随着浏览器的关闭而消失,一般手动清理或者到了过期时间,Cookie才会失效。Cookie属性如下:
- name:value Cookie通过key:value键值对形式,设置cookie值;
- domain:cookie设置的域信息;
- path:所属相对根路径;
- expires:cookie过期时间,超过过期时间,cookie将会删除;
- httponly:只能由服务器创建,客户端通过js脚本将无法读取到cookie信息,从而避免cookie内容的泄漏,减少了XSS攻击的;
- secure:安全cookie是在https访问下的cookie形态,以确保cookie在从客户端传递到服务端过程中始终为加密形式的。
Session的作用就是它在Web服务器上保持用户的状态信息供在任何时间从任何设备上的页面进行访问。客户端首次请求服务端时,服务端会为客户端创建一个Session对象用户存储用户的状态信息,并分配唯一表示SessionID返回给客户端,后续客户端的访问会携带该SessionID,服务端通过SessionID就能确定用户。Session常用如下(不同的服务器语言可能会有差别):
- SessionID:标识Session对象的唯一编号;
- CreationTime:Session会话创建时间;
- LastAccessedTime:客户端上次发送与此相关联的请求的时间;
- TimeOut:Session对象的过期时间,默认为30分钟;
- Attributes:Session属性的集合;
- setAttribute():添加属性;
- removeAttribute():移除属性。
Cookie与Session实现会话管理的步骤如下:
- 客户端首次访问服务器,服务器创建会话,产生session对象,用于记录用户信息;
- Session对象分配一个唯一标识SessionID,通过set-cookie设置到响应头中,以Cookie形式返回给客户端;
- 客户端将拿到的Cookie中的SessionID信息保存起来,当客户端再次访问服务端时,将Cookie信息设置到请求头中,发送给服务器;
- 服务器得到SessionID后,从服务器中查询出来会话信息,得到Session对象,从而跟踪客户端的状态。
Cookie和Session都有过期时间,超过过期时间或者关闭浏览器都会使会话信息丢失。还有一些浏览器是禁止Cookie的,这时候可以使用胖URL方式继续携带SessionID信息与服务端交互。
二、基本认证和摘要认证
很多Web应用和页面需要有特定身份的用户才可以访问,为了达到这个目的,需要对访问用户进行身份的确认,这就是认证。
HTTP协议标准提供了基本(Basic)认证、摘要(Digest)认证以及令牌(BearerToken)认证三种认证方式。HTTP WWW-Authenticate 响应标头定义了 HTTP 身份验证的方法(“质询”),它用于获取特定资源的访问权限。使用 HTTP 身份验证的服务器将以 401 Unauthorized 响应去响应受保护资源的请求。该响应必须包含至少一个 WWW-Authenticate 标头和至少一个质询,以指示使用哪些身份验证方案访问资源。常见的Web容器都有提供基于Http基本认证和摘要认证的API和配置。
一个 WWW-Authenticate 标头中允许多个质询,并且一个响应中允许多个 WWW-Authenticate 标头。服务器也可以在其他的响应消息中包含 WWW-Authenticate 标头,以指示提供的凭据可能会影响响应。 客户端在接收 WWW-Authenticate 标头之后,通常会提示用户接收凭据,然后重新请求资源。这个新的请求会使用 Authorization 标头向服务器提供凭据,并针对所选择的“质询”身份验证方法进行合适的编码。客户端应该选择它理解的最安全的质询。
基本认证是用户在请求服务器资源时提供用户名和密码进行简单认证的方式,在每次进行基本认证请求时,都会在Authorization请求头中利用Base64对 “用户:密码” 字符串进行编码。这种方式并不安全,不适合在Web项目中使用,但它是一些现代主流认证的基础。基本认证过程如下:
1.客户端发起未携带认证信息的请求到服务端资源;
2.服务器端返回401 Unauthorized响应信息,并在WWW-Authenticate头部中说明认证形式。HTTP基本认证时,WWW-Authenticate=Basic realm=“被保护的页面地址”,响应格式如下:
HTTP/1.1 401 Unauthorized WWW-Authenticate: Basic realm="Access to the staging site", charset="UTF-8"
3.客户端会收到401 Unauthorized响应,并弹出一个对话框,询问用户名和密码。当用户输入后,客户端会将用户名和密码使用冒号进行拼接并用Base64编码,然后将其放入到请求的Authorization头部后重新请求资源,Authorization格式如下:
Authorization: Basic YWxhZGRpbjpvcGVuc2VzYW1l
4.服务器端对客户端发来的信息进行解码得到用户名和密码,并对该信息进行校验判断是否正确,最终给客户端返回响应内容。
摘要认证时为了弥补基本认证的缺点而产生的,摘要认证同样使用询问密码的方式,但是不会直接发送明文密码,摘要认证的过程如下:
1.客户端发起未携带认证信息的请求到服务端资源;
2.服务器端返回401 Unauthorized响应信息,并在WWW-Authenticate头部中说明认证形式。HTTP基本认证时,WWW-Authenticate=Digest realm=“被保护的页面地址”,响应格式如下:
HTTP/1.1 401 Unauthorized WWW-Authenticate: Digest realm="http-auth@example.org", qop="auth, auth-int", algorithm=SHA-256, nonce="7ypf/xlj9XXwfDPEoM4URrv/xwf94BcCAzFZH4GiTo0v", opaque="FQhe/qaU925kfnzjCev0ciny7QMkPqMAFRtzCUYo5tdS" WWW-Authenticate: Digest realm="http-auth@example.org", qop="auth, auth-int", algorithm=MD5, nonce="7ypf/xlj9XXwfDPEoM4URrv/xwf94BcCAzFZH4GiTo0v", opaque="FQhe/qaU925kfnzjCev0ciny7QMkPqMAFRtzCUYo5tdS"
3.客户端会收到401 Unauthorized响应,并弹出一个对话框,询问用户名和密码。当用户输入后,客户端会将用户名和密码进行加密,然后将其放入到请求的Authorization头部后重新请求资源,Authorization格式如下:
Authorization: Digest username="Mufasa", realm="http-auth@example.org", uri="/dir/index.html", algorithm=MD5, nonce="7ypf/xlj9XXwfDPEoM4URrv/xwf94BcCAzFZH4GiTo0v", nc=00000001, cnonce="f2/wE4q74E6zIJEtWaHKaf5wv/H5QzzpXusqGemxURZJ", qop=auth, response="8ca523f5e9506fed4657c9700eebdbec", opaque="FQhe/qaU925kfnzjCev0ciny7QMkPqMAFRtzCUYo5tdS"
4.服务器端对客户端发来的信息进行解码得到用户名和密码,并对该信息进行校验判断是否正确,最终给客户端返回响应内容。
三、OAuth2.0协议
OAuth2.0是OAuth协议的延续版本,OAuth是一个关于授权的开放平台标准,允许用户授权第三方应用访问他们提供的需要认证才能访问的资源,而不需要将用户名和密码提供给第三方,例如使用QQ授权登录百度网盘等。OAuth2.0常被应用于以下情景:
- 开放平台之间的授权:社交联合登录、开放API平台;
- 微服务授权:微服务API间调用授权;
- 企业内部应用授权:单点登录(SSO)。
OAuth 2.0协议涉及到4个角色,即:
- 资源所有者(Resource Owner):能够许可受保护资源访问权限的实体,当资源所有者是一个人时,它被称为终端用户;
- 客户端(Client):发起授权请求和资源请求的第三方平台,Oauth2.0协议接入方;
- 授权服务器(Auhtorization Server):在成功验证资源所有者且获得授权后颁发访问令牌给客户端的服务器;
- 资源服务器(Resource Server):托管受保护资源的服务器,能够接收和响应使用访问令牌对受保护资源的请求。
根据官方标准,OAuth 2.0 使用四种授权模式:
- 授权码模式(authorization_code):功能最完整、流程最严谨的授权模式,授权步骤如下:
- 构造授权链接:第三方应用以Appkey、AppSecret、redirect_uri作为参数,以开放平台提供的授权码获取链接作为url构造授权链接;
- 获取授权码Code:访问构造的授权链接获取Code,如果授权成功则授权服务器会向浏览器发起重定向请求到redirect_uri指定的地址并携带参数Code;
- Code换Token:第三方应用以Code作为参数调用开放平台提供的Token获取链接得到access_token、refresh_token。通常Code只能使用一次。
- 资源访问:第三方应用使用access_token访问服务资源。部分场景下还可以使用refresh_token更新access_token。
授权过程时序图如下:
- 简化模式(Implicit): 它与授权模式相比减少了使用授权码Code换取access_token的步骤,适用于公开的浏览器单页应用,access_token直接从授权服务器返回(以url的hash模式附加到重定向的url中),易受到攻击。授权步骤如下:
- 构造授权链接:第三方应用以Appkey、AppSecret、redirect_uri作为参数,以开放平台提供的Token获取链接作为url构造授权链接;
- 获取access_token:请求授权链接,直接返回access_token。
- 资源访问:第三方应用使用access_token访问服务资源。
- 用户密码模式(password):使用用户名/密码作为授权方式从授权服务器上获取AccessToken,账号和密码需要告知第三方应用,除非特别信任第三方应用,否则不应该使用此模式。授权步骤如下:
- 访问授权页面:第三方应用引导用户访问开放平台提供的授权页面,该页面是包含账号密码的表单提交页面;
- 提交认证:用户提交账号密码表单到授权服务器,进行身份验证后返回access_token;
- 资源访问:第三方应用使用access_token访问服务资源。
- 客户端凭证模式(client_credentials):开放平台系统提前为客户端创建了客户端Id(client_id)和密钥(client_secret),客户在请求授权时携带这些参数访问授权服务器,该模式是极不安全的,需要对客户端完全信任,一般适用于合作方接口对接。授权步骤如下:
- 获取Token:第三方应用发送自己的身份信息(client_id、client_secret)给授权服务器直接获取access_token。
- 资源访问:第三方应用使用access_token访问服务资源。
Oauth2.0协议标准授权使用的URL及其参数说明:
请求参数 | response_type | 必选,响应类型,授权码模式时=code、简化模式时=token | |
client_id | 必选,客户端ID,用于标识一个客户端,等同于appId,在注册应用时生成 | ||
redirect_uri | 可选,表示重定向URI,可选项 | ||
scope | 可选,权限范围,用于对客户端的权限进行控制,如果客户端没有传递该参数,那么服务器则以该应用的所有权限代替 | ||
state | 可选,表示客户端的当前状态,可以指定任意值,认证服务器会原封不动地返回这个值 | ||
响应成功参数 | 授权模式时 | code | 必选,授权码代表用户确认授权的暂时性凭证,只能使用一次,推荐最大生命周期不超过10分钟 |
state | 可选,如果客户端传递了该参数,则必须原封不动返回 | ||
简化模式时 | access_token | 必选,访问令牌 | |
token_type | 必选,访问令牌类型,比如bearer,mac等等 | ||
expires_in | 可选,访问令牌的生命周期,以秒为单位,表示令牌下发后多久时间过期,如果没有指定该项,则使用默认值 | ||
scope | 可选,权限范围,如果最终下发的访问令牌对应的权限范围与实际应用指定的不一致,则必须在下发访问令牌时用该参数指定说明 | ||
state | 可选,如果客户端传递了该参数,则必须原封不动返回 | ||
错误响应参数 | error | 必须,错误代码 | |
error_description | 可选,具备可读性的错误描述信息 | ||
error_uri | 可选,错误描述信息页面地址 | ||
state | 可选,如果客户端传递了该参数,则必须原封不动返回 |
请求参数 | grant_type | 必选,授权码模式=authorization_code、账号密码模式=password,客户端凭证模式=client_credentials | |
授权码模式 | code | 必选,上一步骤获取的授权码 | |
redirect_uri | 必选,授权回调地址,如果上一步有设置,则必须相同 | ||
client_id | 必选,客户端ID,在注册应用时生成 | ||
client_secret | 客户端密钥,如果在注册应用时有提供,则需要携带该参数 | ||
账号密码模式 | username | 用户名,密码模式时使用 | |
password | 密码,密码模式时使用 | ||
客户端凭证模式 | client_id | 必选,客户端ID,在注册应用时生成 | |
client_secret | 客户端密钥,如果在注册应用时有提供,则需要携带该参数 | ||
响应成功参数 | access_token | 必选,访问令牌 | |
token_type | 必选,访问令牌类型,比如bearer,mac等等 | ||
expires_in | 可选,访问令牌的生命周期,以秒为单位,表示令牌下发后多久时间过期,如果没有指定该项,则使用默认值 | ||
refresh_token | 可选,刷新令牌,用于刷新access_token | ||
scope | 可选,权限范围,如果最终下发的访问令牌对应的权限范围与实际应用指定的不一致,则必须在下发访问令牌时用该参数指定说明 | ||
响应失败参数 | error | 必须,错误代码 | |
error_description | 可选,具备可读性的错误描述信息 | ||
error_uri | 可选,错误描述信息页面地址 |
客户端经过一系列方法最终将拿到access_token,此时就可以通过access_token请求资源服务器了,至于怎么传递access_token则Oauth协议并没有规定,而是在RFC6750文件中做了定义,主要有三种方式实现:
- 作为请求头Authorization的值传递:
GET /resource HTTP/1.1 Host: server.example.com Authorization: Bearer access_token
- 表单编码传递:
POST /resource HTTP/1.1 Host: server.example.com Content-Type: application/x-www-form-urlencoded access_token=xxxx
- 附加到Url参数传递:
GET /resource?access_token=xxx HTTP/1.1 Host: server.example.com
为了防止access_token被客户端无限期使用,通常access_token都提供有效期,在令牌将要到期前需要重新获取令牌,此时再走授权码的流程会影响用户体验,因此Oautn2.0协议提供了一个用于刷新令牌机制,通过使用refresh_token参数调用token获取接口即可更新令牌,grant_type=refresh_token表示要更新令牌、client_id和client_secret用于确认身份、refresh_token表示更新令牌的令牌。请求格式如下:
https://xxx/oauth/token?grant_type=refresh_token&client_id=xxx&client_secret=xxx&refresh_token=xxx
四、Bearer(令牌)认证
Bearer认证是 OAuth 2.0 中使用的认证类型。它的令牌BearerToken(access_token)是一个不透明的字符串,对于使用它的客户端没有任何意义。前面讲到了基本认证使用Basic标识,摘要认证使用Digest标识,则令牌认证使用Bearer 标识,因此可以看到在请求头参数Authorization的值由两部分组成,空格前的认证类型标识、空格后是token字符串。
五、JWT
JWT是JSON WEB TOKEN的缩写,是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准,是一种紧凑安全的适合分布式站点单点登录场景的Token,支持跨域验证,一般用于身份认证,JWT主要包含三部分:
- 头部(Header):由token的类型和算法名称组成,格式如下,使用Base64编码就得到JWT的第一部分。
{ 'alg': "HS256", //算法 'typ': "JWT" //token类型 }
- 载荷(Payload):包含一些关于用户的信息声明,进行Base64编码后得到JWT的第二部分。
- 签名(Signature):签名字符串需要使用编码过的header、编码过的payload、秘钥、签名算法作为参数生成。签名是用于验证消息在传递过程中有没有被更改,对于使用私钥签名的token,它还可以验证JWT的发送方是否为它所称的发送方。
这三部分以圆点.连接形成JWT。后端生成JWT后返回给客户端,客户端再次访问服务器时,一般将其到请求头的Authorization参数中(也可以放在cookie中)提交给服务器进行验证,格式如下:
Authorization: Bearer jwt
六、单点登录SSO概述
在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统,这种解决方案称为单点登录(Single Sign On)。
要实现SSO,需要满足以下两个条件:
- 所有应用系统共享一个身份认证系统:统一的认证系统是SSO的前提之一。认证系统的主要功能是将用户的登录信息和用户信息库相比较,对用户进行登录认证;认证成功后,认证系统应该生成统一的认证标志(ticket)返还给用户。另外,认证系统还应该对ticket进行校验,判断其有效性。
- 所有应用系统能够识别和提取ticket信息:要实现SSO的功能,让用户只登录一次,就必须让应用系统能够识别已经登录过的用户。应用系统应该能对ticket进行识别和提取,通过与认证系统的通讯,能自动判断当前用户是否登录过,从而完成单点登录的功能。
SSO既有缺点也有优点,优点是可以提高用户效率,进行统一用户管理,缺点是涉及到多系统不利于重构。总体来说实现SSO的方案有共享Session和使用ticket票据的方式,共享Session通常是使用Cookie作为SessionID载体,存在跨域问题,除非把Cookie的范围设置到顶级域名,所有的应用系统都使用子域名才能共享Cookie。因此SSO主要采用ticket票据(jwt、oauth、openID)的方式进行身份验证。
实现单点登录典型的解决方案有如下4种:
- 集中式认证服务(CAS):一种开源的单点登录框架,通过使用该框架可以将单点登录机制完整落地,是实战性比较强的方案。
- JWT认证:轻量级认证标准方案。
- OAuth认证:通过用户授权认证。
- OpenID认证:是基于OAuth2.0的认证方案,但不提供授权功能。
七、CAS框架原理和使用
CAS框架一种开源的单点登录框架,包含CASServer和CASClient两部分,前者负责用户身份验证和颁发TGT,后者是接入单点登录系统的客户端。在CAS框架中包含三个重要的核心概念:
- TGT:全称Ticket Grangting Ticket,是用户登录后生成的票根,包含用户的认证身份、有效期等信息,存储于CAS Server中。
- TGC:全称Ticket Granted Cookie,是存储在Cookie中的一段数据,类似于会话ID,用户与CAS Server进行交互时,帮助用户找到对应的TGT。
- ST:全称Service Ticket,是CAS Server使用TGT签发的一次性票据,CAS Client 使用ST与CAS Server进行交互,以获取用户的验证状态。
CAS单点登录的时序图如下:
CAS框架源码下载地址为CASServer,CASClient,配置参考官方网站。CASServer的搭建步骤如下:
- 下载cas-overlay-template源码,解压后执行Mvn打包命令构建为war包;
- 复制war包到Tomcat服务器的webapp目录;
- 启动tomcat服务器,访问应用。
CASClient通常为自己的项目,但CAS也提供了一个基础案例cas-sample-java-webapp做参考,通过阅读源代码可以理解CAS的原理,通过在此基础上进行重构可以实现我们自己的单点登录。