头图

单点登录(SSO)

记得要微笑

登录是每个网站中都经常用到的一个功能,在页面上我们输入账号密码,敲一下回车键,就登录了,但这背后的登录原理你是否清楚呢?

传统登录方式

最原始的登录方式就是每个网站都拥有自己的登录系统、独立的账户,用户访问每个系统都需要重新登录,系统与系统之间不能共享账户,登录状态不能共享会导致存在着一些弊端:

  • 当员工需要使用公司多个内部系统,就需要在每个系统都注册一个账户,如果员工离职了,那么每个系统还要各自删除该员工的账户信息
  • 明明都是这一个用户在使用系统,却要多次登录

image.png

上面登录方式弊端的根源是用户的登录状态无法实现共享,其中“状态”指的是什么呢?又如何实现“状态共享”呢?

有状态与无状态

我们都知道 HTTP 是一种无状态的协议,客户端每次发送请求时,首先要和服务器端建立一个连接,在请求完成后又会断开这个连接。这种方式可以节省传输时占用的连接资源,但同时也存在一个问题:每次请求都是独立的,服务器端无法判断本次请求和上一次请求是否来自同一个用户,进而也就无法判断用户的登录状态。

为了解决 HTTP 无状态的问题,Lou Montulli 在 1994 年的时候,推出了 Cookie

Cookie 是服务器端发送给客户端的一段特殊信息,这些信息以文本的方式存放在客户端,客户端每次向服务器端发送请求时都会带上这些特殊信息。

有了 Cookie 之后,服务器端就能够获取到客户端传递过来的信息了,如果需要对信息进行认证,还需要通过 Session

客户端请求服务端,服务端会为这次请求开辟一块内存空间,这个便是 Session 对象。Session 对象就是用来记录用户的登录信息,每个Session 对象都对应有一个sidsid是唯一的,登录成功后服务端会将sid注入到cookie中,下次登录或者请求将会携带该cookie进行认证

有了 CookieSession 之后,我们就可以进行登录认证了。

解释一下认证、授权的含义:认证指检查用户是不是登录了;授权指该用户能做什么,比如该用户能看到哪些菜单

Cookie + Session

Cookie + Session 的登录方式是最经典的一种登录方式,现在仍然有大量的企业在使用。

单体登录

image.png

用户首次登录时:

image.png

用户访问 a.com/pageA,并输入密码登录。服务器验证密码无误后,会创建 SessionId,并将它保存起来。服务器端响应这个 HTTP 请求,并通过 Set-Cookie 头信息,将 SessionId 写入 Cookie 中。

服务器端的 SessionId 可能存放在很多地方,例如:内存、文件、数据库等。

第一次登录完成之后,后续的访问就可以直接使用 Cookie 进行身份验证了:

image.png

用户访问 a.com/pageB 页面时,会自动带上第一次登录时写入的 Cookie。服务器端比对 Cookie 中的 SessionId 和保存在服务器端的 SessionId 是否一致。如果一致,则身份验证成功。

单体集群

当访问量高了,上面一台服务器肯定支撑不住,就需要对服务器进行集群

image.png

加上反向代理的作用有两个:

  • 请求分发,负载均衡,减轻节点服务器访问压力
  • 每个Cookie都对应着一个domaina.com域名下的请求只会携带该域名中的Cookie,这时访问b.com页面时不会携带a.com域名中的Cookie,通过反向代理到同一域名(d.com)下,使得每个节点服务器都能接收到该域名(d.com)下的Cookie

同时,Session不能存储在每台节点服务器的tomcat内存中,会导致Session不能共享,当访问从a.com切换到b.com又需要重新登录。为了每个节点服务器能共享Sessiontomcat有内置集群方法,可以同步复制节点tomcat内存中的Session,只需修改tomcat配置文件即可:

  • 修改server.xml,取消注释<Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster"/>
  • 修改应用web.xml,增加节点<distributable />

但该方式有着以下的缺点:

  • 每个服务都需要去修改配置文件
  • 占用带宽
  • 当服务器内存不足时,复制Session可能会造成Session丢失

那有没有更好的方法解决Seesion一致性问题呢?

文件系统?数据库?分布式缓存?

其实这些都可以,只是性能有差异,文件系统、数据库的性能不会很高,分布式缓存解决这一问题

分布式缓存大致有两种:1、MenCache;2、Redis;更多场景下还是会使用Redis存储。ConcurrentHashMap存储使用的 JVM 堆内存,而Redis使用的是物理内存。

image.png

Session-单点登录

上面都是一个系统的登录,如果我们想一个账户登录多个系统怎么办呢?

image.png

通过反向代理到同一域名(o.com)下,使得每个节点服务器都能接收到该域名(o.com)下的Cookie,但我们怎么知道用户像访问哪个集群网站呢?

解决办法是将站点反向代理到同一个域名下,并建立路由或者二级域名与集群站点的映射关系,一般会使用路由做映射,确定用户要访问哪一个集群站点,最后轮询分发到哪个节点服务

{
  https://o.com/seller/#/xxx:l.com,
  https://o.com/admin/#/xxx:m.com,
  https://o.com/mall/#/xxx:n.com,
}

Cookie + Session 存在的问题

虽然我们使用 Cookie + Session 的方式完成了登录验证,但仍然存在一些问题:

  • 由于服务器端需要对接大量的客户端,也就需要存放大量的 Session,这样会导致服务器压力过大。
  • 如果服务器端是一个集群,使用Seesion复制同步登录态,需要将 Session 同步到每一台机器上,无形中增加了服务器端维护成本。

Cookie + Token

为了解决 Session + Cookie 机制暴露出的诸多问题,我们可以使用 Token 的登录方式。

Token 是服务端生成的一串字符串,以作为客户端请求的一个令牌。当第一次登录后,服务器会生成一个 Token 并返回给客户端,客户端后续访问时,只需带上这个 Token 即可完成身份认证。

单体登录

image.png

用户首次登录时:

image.png

  • 用户输入账号密码,并点击登录。
  • 服务器端验证账号密码无误,创建 Token
  • 服务器端将 Token 返回给客户端,由客户端自由保存
  • 后续页面访问时:

image.png

  • 用户访问 a.com/pageB 时,带上第一次登录时获取的 Token
  • 服务器端验证 Token ,有效则身份验证成功。

Token 机制的特点

根据上面的案例,我们可以分析出 Token 的优缺点:

  • 服务器端不需要存放 Token,所以不会对服务器端造成压力,即使是服务器集群,也不需要增加维护成本。
  • Token 可以存放在前端任何地方,可以不用保存在 Cookie 中,提升了页面的安全性。
  • Token 下发之后,只要在生效时间之内,就一直有效,如果服务器端想收回此 Token 的权限,并不容易。

Token怎么生成

实施 Token 验证的方法挺多的,还有一些标准方法,比如 JWT,读作:jot ,表示:JSON Web TokensJWT 标准的 Token 有三个部分:

  • header(头部)
  • payload(数据)
  • signature(签名)

中间用点分隔开,并且都会使用 Base64 编码,所以真正的 Token 看起来像这样:

eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJuaW5naGFvLm5ldCIsImV4cCI6IjE0Mzg5NTU0NDUiLCJuYW1lIjoid2FuZ2hhbyIsImFkbWluIjp0cnVlfQ.SwyHTEx_RQppr97g4J5lKXtabJecpejuef8AqKYMAJc
Header

每个 JWT token 里面都有一个 header,也就是头部数据。里面包含了使用的算法,这个 JWT 是不是带签名的或者加密的。主要就是说明一下怎么处理这个 JWT token

头部里包含的东西可能会根据 JWT 的类型有所变化,比如一个加密的 JWT 里面要包含使用的加密的算法。唯一在头部里面要包含的是 alg 这个属性,如果是加密的 JWT,这个属性的值就是使用的签名或者解密用的算法。如果是未加密的 JWT,这个属性的值要设置成 none

形如:

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

意思是这个 JWT 用的算法是 HS256。上面的内容得用 base64url 的形式编码一下,所以就变成这样:

eyJhbGciOiJIUzI1NiJ9

这部分JSON经过Base64编码后形成Token的第一部分。

Payload

Payload 里面是 Token 的具体内容,这些内容里面有一些是标准字段,你也可以添加其它需要的内容。下面是标准字段:

  • iss:Issuer,发行者
  • sub:Subject,主题
  • aud:Audience,观众
  • exp:Expiration time,过期时间
  • nbf:Not before
  • iat:Issued at,发行时间
  • jti:JWT ID

比如下面这个 Payload ,用到了 iss 发行人,还有 exp 过期时间这两个标准字段。另外还有两个自定义的字段,一个是 name ,还有一个是 admin

{
 "iss": "ninghao.net",
 "exp": "1438955445",
 "name": "wanghao",
 "admin": true
}

使用 base64url 编码以后就变成了这个样子:

eyJpc3MiOiJuaW5naGFvLm5ldCIsImV4cCI6IjE0Mzg5NTU0NDUiLCJuYW1lIjoid2FuZ2hhbyIsImFkbWluIjp0cnVlfQ
Signature

JWT 的最后一部分是 Signature ,这部分内容有三个部分,先是用 Base64 编码的 header.payload ,再用加密算法加密一下,加密的时候要放进去一个 Secret ,这个相当于是一个密码,这个密码秘密地存储在服务端。

  • header
  • payload
  • secret
const encodedString = base64UrlEncode(header) + "." + base64UrlEncode(payload); 
HMACSHA256(encodedString, 'secret');

处理完成以后看起来像这样:

SwyHTEx_RQppr97g4J5lKXtabJecpejuef8AqKYMAJc

最后这个在服务端生成并且要发送给客户端的 Token 看起来像这样:

eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJuaW5naGFvLm5ldCIsImV4cCI6IjE0Mzg5NTU0NDUiLCJuYW1lIjoid2FuZ2hhbyIsImFkbWluIjp0cnVlfQ.SwyHTEx_RQppr97g4J5lKXtabJecpejuef8AqKYMAJc

客户端收到这个 Token 以后把它存储下来,下回向服务端发送请求的时候就带着这个 Token 。服务端收到这个 Token ,然后进行验证,通过以后就会返回给客户端想要的资源。

签发与验证 JWT

在应用里实施使用基于 JWT 这种 Token 的身份验证方法,你可以先去找一个签发与验证 JWT 的功能包。无论你的后端应用使用的是什么样的程序语言,系统,或者框架,你应该都可以找到提供类似功能的包。

签发 JWT

在项目里随便添加一个 .js 文件,比如 index.js,在文件里添加下面这些代码:

const jwt = require('jsonwebtoken')

// Token 数据
const payload = {
  name: 'wanghao',
  admin: true
}

// 密钥
const secret = 'ILOVENINGHAO'

// 签发 Token
const token = jwt.sign(payload, secret, { expiresIn: '1day' })

// 输出签发的 Token
console.log(token)

非常简单,就是用了 jsonwebtoken 里面提供的 jwt.sign 功能,去签发一个 token。这个 sign 方法需要三个参数:

  • playload:签发的 token 里面要包含的一些数据。
  • secret:签发 token 用的密钥,在验证 token 的时候同样需要用到这个密钥。
  • options:一些其它的选项。
验证 JWT

验证 JWT 的用效性,确定一下用户的 JWT 是我们自己签发的,首先要得到用户的这个 JWT Token,然后用 jwt.verify 这个方法去做一下验证,如果不是该自己Secret签发的Token,校验会不通过。这个方法是 Node.jsjsonwebtoken 这个包里提供的,在其它的应用框架或者系统里,你可能会找到类似的方法来验证 JWT

// 验证 Token
jwt.verify(token, 'secret', (error, decoded) => {
  if (error) {
    console.log(error.message)
    return
  }
  console.log(decoded)
})

Token 单体集群

image.png

Token-单点登录

image.png

使用Token可能遇到哪些问题呢?

  1. Token能不能被盗?

    Token在https协议中是不容易被盗取的,https中的SSL协议会对请求头和请求体进行加密,也就是说传输给服务端的Cookie是经过加密的,请求被拦截是不容易解密的。当然,如果你的电脑安装了有些植入木马的恶意程序,窃取你的账户和令牌,这就没办法了,所以还是倡导健康上网,拒绝盗版软件。

  2. 密钥直接放在服务器中被开发人员窃取了怎么办?
    防火防盗却防不住自家人怎么办?密钥一般都不会直接放在项目中,可能会放在文件系统或者github上,而且不同环境有着不同的密钥。为了防止开发人员读取到密钥后进行打印,可以加上代码审查环节,提交上去的代码会自动扫描,如果发现打印密钥的关键代码,果断开除员工。另外,密钥也有可能不定时更换。
  3. Token如何续期?
    令牌的有效期到了,如果让用户重新走一遍上面的流程,再申请一个新的令牌,很可能体验不好,而且也没有必要。具体方法是,服务端颁发令牌的时候,一次性颁发两个令牌,一个用于获取数据,另一个用于获取新的令牌(refresh token 字段)。令牌到期前(前端倒计时判断过期或者请求发现登录异常),用户使用 refresh token 发一个请求,去更新令牌。

CAS

无论采用Session还是Token实现单点登录,上面每个子系统都需要实现自个认证服务,但是它们的Session或者Token都是共享的,是可以抽离成一个中央认证服务(CAS),所有的子系统都通过中央认证服务进行登录、认证。

CASCentral Authentication Service的缩写,中央认证服务,一种独立开放指令协议。CAS Yale 大学发起的一个开源项目,旨在为 Web 应用系统提供一种可靠的单点登录方法。CAS 包含两个部分: CAS ServerCAS ClientCAS Server 需要独立部署,主要负责对用户的认证工作;CAS Client 负责处理对客户端受保护资源的访问请求,需要登录时,重定向到 CAS Server

image.png

CAS Client 与受保护的客户端应用部署在一起,以 Filter 方式保护受保护的资源。对于访问受保护资源的每个 Web 请求,CAS Client 会分析该请求的 Http 请求中是否包含 Service Ticket,如果没有,则说明当前用户尚未登录,于是将请求重定向到指定好的 CAS Server 登录地址,并传递 Service (也就是要访问的目的资源地址),以便登录成功过后转回该地址。用户在第 3 步中输入认证信息,如果登录成功,CAS Server 随机产生一个相当长度、唯一、不可伪造的 Service Ticket,并缓存以待将来验证,之后系统自动重定向到 Service 所在地址,并为客户端浏览器设置一个 Ticket Granted Cookie(TGC)CAS Client 在拿到 Service 和新产生的 Ticket 过后,在第 5,6 步中与 CAS Server 进行身份核实,以确保 Service Ticket 的合法性。

在该协议中,所有与 CAS 的交互均采用 SSL 协议,确保,ST TGC 的安全性。协议工作过程中会有 2 次重定向的过程,但是 CAS ClientCAS Server 之间进行 Ticket 验证的过程对于用户是透明的。

另外,CAS 协议中还提供了 Proxy (代理)模式,以适应更加高级、复杂的应用场景,具体介绍可以参考 CAS 官方网站上的相关文档。

image.png

上图是CAS官网上的标准流程,具体流程如下:

  1. 用户访问app系统,app系统是需要登录的,但用户现在没有登录。
  2. 跳转到CAS server,即SSO登录系统,以后图中的CAS Server我们统一叫做SSO系统。 SSO系统也没有登录,弹出用户登录页。
  3. 用户填写用户名、密码,SSO系统进行认证后,将登录状态写入SSOsession,浏览器(Browser)中写入SSO域下的Cookie
  4. SSO系统登录完成后会生成一个ST(Service Ticket),然后跳转到app系统,同时将ST作为参数传递给app系统。
  5. app系统拿到ST后,从后台向SSO发送请求,验证ST是否有效。
  6. 验证通过后,app系统将登录状态写入session并设置app域下的Cookie

至此,跨域单点登录就完成了。以后我们再访问app系统时,app就是登录的。接下来,我们再看看访问app2系统时的流程。

  1. 用户访问app2系统,app2系统没有登录,跳转到SSO
  2. 由于SSO已经登录了,不需要重新登录认证。
  3. SSO生成ST,浏览器跳转到app2系统,并将ST作为参数传递给app2
  4. app2拿到ST,后台访问SSO,验证ST是否有效。
  5. 验证成功后,app2将登录状态写入session,并在app2域下写入Cookie

oAuth2.0

在传统的客户端-服务器身份验证模式中,客户端请求服务器上访问受限的资源(受保护的资源)时,需要使用资源所有者的凭据在服务器上进行身份验证。资源所有者为了给第三方应用提供受限资源的访问权限,需要与第三方共享它的凭据。这就导致一些问题和局限:

  • 第三方应用需要存储资源所有者的凭据以供将来使用。该凭据通常是明文密码。
  • 服务器需要支持密码身份认证,尽管密码认证有固有的安全缺陷。
  • 第三方应用获得了对资源所有者的受保护资源的过于宽泛的访问权限,从而导致资源所有者不能限制对资源的有限子集的访问时限或权限。
  • 资源所有者不能撤销某个第三方的访问权限而不影响其它第三方,并且必须更改他们的密码才能做到。
  • 与任何第三方应用的妥协导致对终端用户的密码及该密码所保护的所有数据的妥协。

OAuth通过引入授权层以及从资源所有者角色分离出客户端角色来解决这些问题。详细说就是,OAuth在"客户端"与"服务提供商"之间,设置了一个授权层(authorization layer)。"客户端"不能直接登录"服务提供商",只能登录授权层,以此将用户与客户端区分开来。"客户端"登录授权层所用的令牌(token),与用户的密码不同。用户可以在登录的时候,指定授权层令牌的权限范围和有效期。"客户端"登录授权层以后,"服务提供商"根据令牌的权限范围和有效期,向"客户端"开放用户储存的资料。

CAS和OAuth2区别

  • CAS的单点登录时保障客户端的用户资源的安全,OAuth2则是保障服务端的用户资源的安全;
  • CAS客户端要获取的最终信息是,这个用户到底有没有权限访问我(CAS客户端)的资源;oauth2获取的最终信息是,我(oauth2服务提供方)的用户的资源到底能不能让你(oauth2的客户端)访问;
  • CAS的单点登录,资源都在客户端这边,不在CAS的服务器那一方。用户在给CAS服务端提供了用户名密码后,作为CAS客户端并不知道这件事。随便给客户端个ST,那么客户端是不能确定这个ST是用户伪造还是真的有效,所以要拿着这个ST去服务端再问一下,这个用户给我的是有效的ST还是无效的ST,是有效的我才能让这个用户访问。
  • OAuth2认证,资源都在OAuth2服务提供者那一方,客户端是想索取用户的资源。所以在最安全的模式下,用户授权之后,服务端并不能直接返回token,通过重定向送给客户端,因为这个token有可能被黑客截获,如果黑客截获了这个token,那用户的资源也就暴露在这个黑客之下了。于是聪明的服务端发送了一个认证code给客户端(通过重定向),客户端在后台,通过https的方式,用这个code,以及另一串客户端和服务端预先商量好的密码,才能获取到token和刷新token,这个过程是非常安全的。如果黑客截获了code,他没有那串预先商量好的密码,他也是无法获取token的。这样oauth2就能保证请求资源这件事,是用户同意的,客户端也是被认可的,可以放心的把资源发给这个客户端了。
  • CAS登录和OAuth2在流程上的最大区别就是,通过ST或者code去认证的时候,需不需要预先商量好的密码。

OAuth定义了四种角色:

  • 资源所有者
    能够许可对受保护资源的访问权限的实体。当资源所有者是个人时,它被称为最终用户。
  • 资源服务器
    托管受保护资源的服务器,能够接收和响应使用访问令牌对受保护资源的请求。
  • 客户端
    使用资源所有者的授权代表资源所有者发起对受保护资源的请求的应用程序。术语“客户端”并非特指任何特定的的实现特点(例如:应用程序是否是在服务器、台式机或其他设备上执行)。
  • 授权服务器
    在成功验证资源所有者且获得授权后颁发访问令牌给客户端的服务器。

授权服务器和资源服务器之间的交互超出了本规范的范围。授权服务器可以和资源服务器是同一台服务器,也可以是分离的个体。一个授权服务器可以颁发被多个资源服务器接受的访问令牌。

抽象的协议流程

+--------+                               +---------------+
 |        |--(A)- Authorization Request ->|   Resource    |
 |        |                               |     Owner     |
 |        |<-(B)-- Authorization Grant ---|               |
 |        |                               +---------------+
 |        |
 |        |                               +---------------+
 |        |--(C)-- Authorization Grant -->| Authorization |
 | Client |                               |     Server    |
 |        |<-(D)----- Access Token -------|               |
 |        |                               +---------------+
 |        |
 |        |                               +---------------+
 |        |--(E)----- Access Token ------>|    Resource   |
 |        |                               |     Server    |
 |        |<-(F)--- Protected Resource ---|               |
 +--------+                               +---------------+

图中所示的抽象的OAuth 2.0流程描述了四种角色之间的交互,包括以下步骤:

(A)客户端从资源所有者处请求授权。授权请求可以直接向资源所有者发起(如图所示),或者更可取的是通过授权服务器作为中介间接发起。
(B)客户端收到授权许可,这是一个代表资源所有者的授权的凭据,使用本规范中定义的四种许可类型之一或者使用扩展许可类型表示。授权许可类型取决于客户端请求授权所使用的方法以及授权服务器支持的类型。
(C)客户端与授权服务器进行身份认证并出示授权许可以请求访问令牌。
(D)授权服务器验证客户端身份并验证授权许可,若有效则颁发访问令牌。
(E)客户端从资源服务器请求受保护资源并出示访问令牌进行身份验证。
(F)资源服务器验证访问令牌,若有效则处理该请求。
客户端从资源所有者获得授权许可(步骤(A)和(B)所示)的更好方法是使用授权服务器作为中介

授权许可是一个代表资源所有者授权(访问受保护资源)的凭据,客户端用它来获取访问令牌。RFC 6749规范定义了四种许可类型——授权码、隐式许可、资源所有者密码凭据和客户端凭据——以及用于定义其他类型的可扩展性机制。

以github授权为例讲述授权码类型

首先需要到github上进行应用注册,注册完后GitHub会返回客户端 ID(client_id)和客户端密钥(client_secret

image.png

客户端前端台以Session方式认证:

image.png

客户端前端台以Token方式认证:

image.png

参考:
基于 Token 的身份验证:JSON Web Token(附:Node.js 项目)
CAS
Seesion复制
阮一峰OAuth 2.0 的四种方式
RFC 6749中文版
RFC 6749-OAuth 2.0 授权框架简体中文翻译
github OAuth documentation

阅读 942
avatar
记得要微笑
前端工程师

求上而得中,求中而得下,求下而不得

1k 声望
1.9k 粉丝
0 条评论
你知道吗?

avatar
记得要微笑
前端工程师

求上而得中,求中而得下,求下而不得

1k 声望
1.9k 粉丝
宣传栏