浅谈一下前后端鉴权方式 ^.^

29 阅读18分钟

虽然本人现在从事前端开发,但是之前一直是 PHP 全栈,所以对前后端鉴权机制也有一定的了解,就找些资料简单记录一下吧。(瞎掰扯~)

常见鉴权机制

HTTP 是无状态的协议(对于事务处理没有记忆能力,每次客户端和服务端会话完成时,服务端不会保存任何会话信息。):每个请求都是完全独立的,服务端无法确认当前访问者的身份信息,无法分辨上一次的请求发送者和这一次的发送者是不是同一个人。所以服务器与浏览器为了进行会话跟踪(知道是谁在访问我),就必须主动的去维护一个状态,这个状态用于告知服务端前后两个请求是否来自同一浏览器,由此产生了很多种鉴权方式。

  • HTTP Basic Authentication
  • Session-Cookie
  • Token
  • OAuth

另外我们要注意区分 AuthenticationAuthrization,一个是认证一个是授权Authentication 是为了验证你是不是本人,而 Authrization 是为了验证你有没有做某件事情的权限。我们分别举三个例子来说明三种情况让大家对认证和授权的关系有更好的理解。

  • 只认证不授权
    • 只是登录应用,并不进行其他操作,这时候不需要授权只进行认证。
  • 既认证又授权
    • 我们使用第三方应用登录的时候,既输入了第三方应用的账号密码来认证,又授权了本应用读取第三方登录应用已经注册了的个人信息数据等。
  • 不认证只授权
    • 我们点开小程序时,需要获取个人信息,这种时候相当于只授权数据给小程序,并未进行认证,毕竟在应用内部使用小程序,很少有需要再登录认证这种操作。

各鉴权机制流程与原理

  一旦涉及认证授权,必须要考虑的一个问题就是状态管理。所谓的状态管理就是说我们在进行登录之后的一段时间里,不希望每次访问它都需要重新登录。所以开发者必须要考虑怎么样保持用户的登录状态以及设置失效时间。而这个过程需要前后端通力合作来完成。

  • 下面就来简单谈一下几种常见的认证和授权方式的流程与原理,本人瞎掰扯,欢迎大佬指点。

HTTP Basic Authentication

  这种授权方式是浏览器遵守 HTTP 协议实现的基本授权方式,HTTP 协议进行通信的过程中定义了基本认证允许 HTTP 服务器对客户端进行用户身份证的方法。

基本流程

  • 发送请求:客户端向服务器请求数据,请求的内容可能是一个网页或者是一个 ajax 异步请求,此时假设客户端尚未被验证(服务器验证并判断是否返回 401),则客户端提供如下请求至服务器。
Get /index.html HTTP/1.0 
Host: www.test.com
  • 服务器返回 401:服务器向客户端发送验证请求代码 401,WWW-Authenticate: Basic realm="text.com" 这句话是关键,如果没有客户端不会弹出用户名和密码输入界面,服务器返回的数据大抵如下。
HTTP/1.0 401 Unauthorised 
Server: SokEvo/1.0 
WWW-Authenticate: Basic realm="test.com"
Content-Type: text/html 
Content-Length: xxx
  • 客户端弹出窗口:当符合 http1.0 或 1.1 规范的客户端收到 401 返回值时,将自动弹出一个登录窗口,要求用户输入用户名和密码。
    • 这个时候请求时属于 pending 状态,当用户输入用户名密码的时候客户端会再次发送请求头带 Authorization 的请求。
  • 用户输入用户名和密码:输入密码后,点击提交会将用户名及密码以 Base64 加密方式加密,并将密文放入前一条请求信息中,则客户端发送的第一条请求信息则变成如下内容。
Get /index.html HTTP/1.0 
Host: www.test.com
Authorization: Basic xxxxxxx(base64 密文)
// 加密过程是浏览器默认的行为,不需要我们人为加密,我们只需要输入用户名密码即可。
  • 服务端解密:服务器收到上述请求信息后,将 Authorization 字段后的用户信息取出并解密,将解密后的用户名及密码与用户数据库进行比较验证,如用户名及密码正确,服务器则根据请求,将所请求资源发送给客户端。

http_basic_auth.png

优点:简单便捷,兼容性好。

缺点:未使用 TLS/SSL 的情况下信息容易泄露,不安全;无法注销,只能关闭浏览器或标签页。

Session-Cookie

  这种授权方式是利用服务端的 Session 和浏览器(客户端)的 Cookie 来实现的前后端通信认证模式。HTTP 协议是一个无状态的协议,服务器不会知道到底是哪一台浏览器访问了它,因此需要一个标识用来让服务器区分不同的浏览器。cookie 就是这个管理服务器与客户端之间状态的标识。   cookie 的原理是,浏览器第一次向服务器发送请求时,服务器在 response 头部设置 Set-Cookie 字段,浏览器收到响应就会设置 cookie 并存储,在下一次该浏览器向服务器发送请求时,就会在 request 头部自动带上 Cookie 字段,服务器端收到该 cookie 用以区分不同的浏览器。   当然,这个 cookie 与某个用户的对应关系应该在第一次访问时就存在服务器端,这时就需要 session 了。另外 cookie 记得设置过期时间,如果不设置过期时间关闭浏览器就会消失,设置过期时间的话会保存在本地磁盘上。服务端也记得配置 seesion,尤其是分布式服务器在鉴权机制上需要考虑 cookie 共享与 seesion 共享等问题。   session 是会话的意思,浏览器第一次访问服务端,服务端就会创建一次会话,在会话中保存标识该浏览器的信息。它与 cookie 的区别就是 session 是缓存在服务端的,cookie 则是缓存在客户端,他们都由服务端生成,是为了弥补 HTTP 协议无状态的缺陷。每当请求到达服务端时会先校验请求中的用户标识是否存在于 session 中,如果有则表示已经认证成功,否则表示认证失败。

基本流程

  • 服务器在接受客户端首次访问时在服务器端创建 seesion,然后保存 seesion(我们可以将 seesion 保存在内存中,也可以保存在 redis 中,推荐使用后者。),然后给这个 session 生成一个唯一的标识字符串 sessionId(sid)
  • 通过秘钥(自定义)对 sid 进行签名处理,避免客户端修改 sid。(非必需步骤)生成 sid 后把 sid 和用户信息映射起来保存在服务器,最后在响应头中种下 (set-cookie) 这个唯一标识字符串。
  • 浏览器中收到请求响应的时候会解析响应头,然后将 sid 保存在本地 cookie 中,浏览器在下次 http 请求的请求头中会带上该域名下的 cookie 信息。
  • 服务器在之后接受客户端请求时会去解析请求头 cookie 中的 sid,然后根据这个 sid 去找服务器端保存的该客户端的 session 判断该请求是否合法。
  • 在后续请求中,服务器会一直根据 sid 认证,如果验证通过,则继续处理。一旦用户登出,服务端和客户端同时销毁该会话。

session_cookie.jfif

优点:简单便捷,浏览器会自动带上;不需要每次都从数据库取数据比对(如果 sid 不存服务器的话);可以方便管理用户注销与登录(删除/添加 session)。

缺点:脱离浏览器没法用,比如移动端、PC端等;session 存储在服务端,增大了服务器的开销;由于 sid 存在服务端,若被人取到 sid 容易受到跨站请求伪造(CSRF)的攻击,我们可以设置 HttpOnly(脚本无法读取保存在本地的 sid,可以防止 XSS 注入后获取 cookie 中的 sid,从而伪造攻击。)、Secure 设置为 true(使用 HTTPS)来提高安全性;在分布式服务器上,需要共享 session 等配置,会限制负载均衡和集群水平拓展的能力。

  • session 也依赖于 cookie 机制,除了比cookie安全点外,cookie 认证的其他缺点 session 几乎也都有。但也有一点区别,且 session cookie 的鉴权方式比单独使用 cookie 鉴权更多一点。

    • session 比 cookie 安全,session 是存储在服务器端的,cookie 是存储在客户端的。
    • cookie 只支持存字符串数据,想要设置其他类型的数据,需要将其转换成字符串,session 可以存任意数据类型。
    • cookie 可设置为长时间保持,比如我们经常使用的默认登录功能,session 一般失效时间较短,客户端关闭(默认情况下)或者 session 超时都会失效。
    • 单个 cookie 保存的数据不能超过 4K,session 可存储数据远高于 cookie,但是当访问量过多,会占用过多的服务器资源。
  • cookie 认证的基本流程

cookie_auth.jfif

Token

Token 授权

  token 又叫令牌,本质上就是一串无意义的字符串,一般放在请求头里,请求头 key 一般是 Authorization,当然也可以和服务端约定好自定义成其他的,只要服务端能够从请求头中拿到 token 就好了。   token 认证的出现最大的特点就是让登录认证不再依赖于 cookie 机制了,将 token 放在了请求头里,那些由于 cookie 机制导致的弊端自然就没有了。

  • 客户端使用用户名跟密码请求登录
  • 服务端收到请求,去验证用户名与密码。
  • 验证成功后,服务端会根据自定义规则签发一个 Token,再把这个 Token 发送给客户端。
  • 客户端收到 Token 以后可以把它存储起来,比如放在 Cookie 里或者 LocalStorage 里。
  • 客户端每次向服务端请求资源的时候需要带着服务端签发的 Token,放在请求头 Authorization 中。
  • 服务端收到请求,然后去验证客户端请求里面带着的 Token,如果验证成功,就向客户端返回请求的数据(从数据库查询签发的 Token,并查询用户数据。) ,如果不成功返回 401 错误码,鉴权失败。

token.jfif

优点:token 认证不局限于 cookie 且不受同源策略的影响,可以指定放在请求头某个字段中,可以给应用程序使用;不使用 cookie,攻击者无法猜到使用的 token 在哪,而且用户的 token 存在本地,只有在提交请求时才会放在请求头某个字段中供服务器读取(类似于获取 Referer 这种,脚本无法读取。),这些就可以一定程度上规避 CSRF 攻击。

缺点:加密解密消耗使得 token 认证比 Session-Cookie 更消耗性能;token 比 sessionId 大,更占带宽;token 需要去数据库中查询用户信息,增大数据库压力。

JWT (JSON Web Token)

  由于每次请求都要用 token 去数据库中查询用户信息,数据库的压力太大了。如果 token 携带了用户信息,不就不需要每次请求都访问数据库查了嘛,可以直接从 token 中直接解析出用户信息以及用户登录状态进行校验,这就是 JWT。给浏览器返回的 token 是一串带着用户信息的加密字符串。   JWT 全称是 Json Web Token。其实就是特殊的 token,理解起来就是携带着用户信息的 token。所以 JWT 认证和 token 认证本质上是一样的。只不过 token 认证的用户信息是从数据库里查的。而 JWT 认证的用户信息是直接从 token 解析出来的。

JWT 组成

jwt_3_parts.jpeg

Header

  • Header 部分是一个 JSON 对象,描述 JWT 的元数据如图所示。
    • 上面代码中,alg 属性表示签名的算法(algorithm),默认是 HMAC SHA256(写成 HS256)。typ 属性表示这个令牌(token)的类型(type),JWT 令牌统一写为 JWT。
    • 最后,将上面的 JSON 对象使用 Base64URL 算法(详见后文)转成字符串。

Payload

  • Payload 部分也是一个 JSON 对象,用来存放实际需要传递的数据。JWT 规定了 7 个官方字段,供选用。
iss (issuer):签发人
exp (expiration time):过期时间
sub (subject):主题
aud (audience):受众
nbf (Not Before):生效时间
iat (Issued At):签发时间
jti (JWT ID):编号
除了官方字段,你还可以在这个部分定义私有字段,比如用户名、用户昵称、权限、部门等等。
注意,JWT 默认是不加密的,任何人都可以读到,所以不要把秘密信息放在这个部分。
这个 JSON 对象也要使用 Base64URL 算法转成字符串。

Signature

  • Signature 部分是对前两部分的签名,防止数据篡改。
    • 首先,需要指定一个密钥(secret)。这个密钥(自定义)只有服务器才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。
    • HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
    • 算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用 . 分隔,就可以返回给用户。
Base64URL 算法

  前面提到,Header 和 Payload 串型化的算法是 Base64URL。这个算法跟 Base64 算法基本类似,但有一些小的不同。   JWT 作为一个令牌(token),有些场合可能会放到 URL(比如 http://api.example.com/?token=xxx)。Base64 有三个字符 +/=,在 URL 里面有特殊含义,所以要被替换掉:= 被省略、+ 替换成 -/ 替换成 _。这就是 Base64URL 算法。

基本流程

了解以上内容后我们简单说一下流程

  • 基本流程与 token 一致,只是签发的 token 内容不同。
  • 前面我们提到了 JWT 是保存了用户信息的,所以我们需要做的事情主要是以下几点。
    • 客户端使用用户名跟密码请求登录
    • 服务端收到请求,去验证用户名与密码。
    • 验证成功后,服务端会根据自定义密钥与用户信息签发一个 Token,再把这个 Token 发送给客户端。
    • 客户端收到 Token 以后可以把它存储起来,比如放在 Cookie 里或者 LocalStorage 里。
    • 客户端每次向服务端请求资源的时候需要带着服务端签发的 Token,放在请求头 Authorization 中,当然你也可以放到 cookie 中,但是这样不能跨域。
      • Authorization: Bearer <token>
    • 服务器收到后根据 Header 中的加密算法与自定义的密钥,对 Payload 内容进行加密,然后生成结果与 Signature 一致的话,则认证通过,否则表示认证失败。

jwt.jfif

补充
  • JWT 默认是不加密,但也是可以加密的。生成原始 Token 以后,可以用密钥再加密一次(前端若需要使用 token 中的相关信息,请告诉其编码规则。)。
  • JWT 不加密的情况下,不能将秘密数据写入 JWT。
  • JWT 不仅可以用于认证,也可以用于交换信息。有效使用 JWT,可以降低服务器查询数据库的次数。
  • JWT 的最大缺点是,由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或者更改 token 的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑,但是也有好处,可以方便分布式服务器管理,因为密钥和解析加密逻辑都在代码里面。
  • JWT 本身包含了认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。为了减少盗用,JWT 的有效期应该设置得比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证。
  • 为了减少盗用,JWT 不应该使用 HTTP 协议明码传输,要使用 HTTPS 协议传输。
  • 由于 JWT 的有效期应该设置得比较短,所以就产生了登录状态信息续签问题。比如设置 token 的有效期为一个小时,那么一个小时后,如果用户仍然在这个应用上,这个时候当然不能指望用户再登录一次。目前可用的解决办法是在每次用户发出请求都返回一个新的 token,前端再用这个新的 token 来替代旧的,这样每一次请求都会刷新 token 的有效期。但是这样,需要频繁的生成 token。另外一种方案是用户每次请求时判断还有多久这个 token 会过期,在 token 快要过期时,返回一个新的 token。如果前端不想每次自己操作请求头,则可以放到 cookie 中,只需 set-cookie 并设置 http-only 与 HTTPS 即可。
  • 用户主动注销时 JWT 并不支持用户主动退出登录,客户端在别处使用 token 仍然可以正常访问。为了支持注销,可以在注销时将该 token 加入到服务器的 redis 黑名单中或者设置数据库存储也可。

OAuth

  OAuth 协议为用户资源的授权提供了一个安全的、开放而又简易的标准。与以往的授权方式不同之处是 OAuth 的授权不会使第三方触及到用户的帐号信息(如用户名与密码),即第三方无需使用用户的用户名与密码就可以申请获得该用户资源的授权,因此 OAuth 是安全的。   同时,任何第三方都可以使用 OAuth 认证服务,任何服务提供商都可以实现自身的 OAuth 认证服务,因而 OAuth 是开放的。我们常见的提供 OAuth 认证服务的厂商有支付宝、QQ、微信、微博、Github等。   OAuth 协议又有 1.0 和 2.0 两个版本。相比较 1.0(存在严重安全漏洞已停用),2.0 版整个授权验证流程更简单更安全,也是目前最主要的用户身份验证和授权方式。

  • 与 JWT 区别
    • OAuth2.0 是一种授权框架(鉴权的流程理念),用在使用第三方账号登录的情况,比如使用 QQ 登录某个 app。
    • JWT 是一种认证协议(鉴权的方法方式),用在前后端分离,需要简单的对后台 API 进行保护时使用。
    • 无论使用哪种方式切记用 HTTPS 来保证数据的安全性

基本流程

oauth.png

  • 请求认证(认证第三方应用是否合法):客户端(第三方应用)向 OAuth 服务提供商请求未授权的 RequestToken。即向 RequestToken URL 发起请求。
    • OAuth 服务提供商同意使用者的请求,并向其颁发未经用户授权的 oauth_token 与对应的 oauth_token_secret,并返回给使用者。
  • 用户同意(确认用户是否同意):使用者向 OAuth 服务提供商请求用户授权的 RequestToken。即向 UserAuthorization URL 发起请求并在请求中携带上一步服务提供商颁发的未授权的 oauth_token 与 oauth_token_secret。
    • OAuth 服务提供商通过网页要求用户登录并引导用户完成授权。
  • 换取 AccessToken(提供 AccessToken&RefreshToken(可选) 给第三方应用):RequestToken 授权后,使用者将向 AccessToken URL 发起请求,将上步授权的 RequestToken 换取成 AccessTokenRefreshToken
    • OAuth 服务提供商同意使用者的请求,并向其颁发 AccessToken 与对应的密钥,并返回给使用者。
  • 使用 AccessToken 换取资源(第三方应用通过 AccessToken 获取用户授权的相关资源):使用者以后就可以使用上步返回的 AccessToken 访问用户授权的资源。
    • 使用 AccessToken 换取资源失败:使用 RefreshToken 换取新的 AccessToken 来重新请求资源

oauth_token.png

oauth_refresh_token.png

OAuth2.0 提供了四种授权模式,开发者可以根据自己的业务情况自由选择。

  1. 授权码授权模式(Authorization Code Grant)
  2. 隐式授权模式(简化模式)(Implicit Grant)
  3. 密码授权模式(Resource Owner Password Credentials Grant)
  4. 客户端凭证授权模式(Client Credentials Grant)

单点登录扩展

JWT 单点登录流程

jwt_sso.jfif

Session 单点登录流程

session_sso.jfif

参考来源

一文详解前后端鉴权