前言:

最近有空翻了翻技术的RoadMap,发现自己在认证和授权这块很多概念比较模糊,于是大致就整理分析了下,全文如下。

为什么我们需要认证?

因为HTTP是无状态的,于是为了追踪用户想出来很多方法,包括 cookie/session 机制、token、flash 跨浏览器 cookie甚至浏览器指纹等。

image-20200702213420870

认证是什么?和授权的关系?

认证是 Authentication,指的是、当用户登陆过后系统便能追踪到他的身份做出符合相应业务逻辑的操作。即使用户没有登录,大多数系统也会追踪他的身份,只是当做来宾或者匿名用户来处理。认证技术解决的是 “我是谁?”的问题。

授权是 Authorization,指的是什么样的身份被允许访问某些资源,在获取到用户身份后继续检查用户的权限。单一的系统授权往往是伴随认证来完成的,但是在开放 API 的多系统结构下,授权可以由不同的系统来完成,例如 OAuth。授权技术是解决“我能做什么?”的问题。

基于cookie的认证方式,每个请求里携带cookie信息以便于服务器进行识别

工作流程:

image-20200702213611987

  1. 用户输入用户名和密码,发送给服务器
  2. 服务器验证用户名和密码,正确的话创建一个会话(Session)
  3. 服务器将SessionID保存到客户端浏览器中(一般使用set-cookie 方法),因为保存的地方是浏览器的Cookie,所以这个认证方式被称为Cookie Based
  4. 后续浏览器发送SessionID到服务器,服务器若能找到对应Session,那么服务器就会正确响应
  5. 当用户退出登陆,session 会在服务端和客户端同时被销毁

缺点:

  • cookie 能很好的处理单域和子域,但是遇到跨域的问题就会变得难以处理
  • 原生移动平台并不一定和 cookie 能良好的兼容,在使用中会存在一些限制和需要注意的地方

Basic Authentication

image-20200702214303488

工作流程:

image-20200702214509125

  1. 组合用户名和密码然后 Base64 编码
  2. 给编码后的字符串添加 Basic 前缀,然后设置名称为 Authorization 的 header 头部

API 的使用方式:

  1. 将用户名和密码使用冒号连接,例如 username:kido123456
  2. 为了防止用户名或者密码中存在超出 ASCII 码范围的字符,推荐使用UTF-8编码
  3. 将上面的字符串使用 Base 64 编码,例如 dXNlcm5hbWU6a2lkbzEyMzQ1Ng==
  4. 在 HTTP 请求头中加入 “Basic + 编码后的字符串”,即:Authorization: Basic dXNlcm5hbWU6a2lkbzEyMzQ1Ng==

缺点:

编码后的密码如果明文传输则容易在网络传输中泄露,在密码不会过期的情况下,密码一旦泄露,只能通过修改密码的方式

Token Based Authentication

基于Token的认证,在每个请求中携带被签名过的Token信息传送到服务端

工作流程:

image-20200702215937176

  1. 用户输入用户名密码,发送给服务器

  2. 服务器验证一下用户名和密码,正确的话就返回一个签名过的 token( token 可以认为就是个长长的字符串)给客户端

  3. 后续每次请求中,浏览器会把 token 作为 http header 中的一部分发送给服务器

  4. 服务器可以验证一下签名是否有效,如果有效那么认证就成功了,可以返回客户端需要的数据

缺点:

  • token 最大的缺点就是它的大小,最小的它都比 cookie 要大,如果 token 中包含很多声明,向服务器发送的每个请求都要有这个 token

JWT Based Authentication

基于JWT token的认证方式,由于一些信息,比如用户 ID、过期等信息,不需要再外部存储中关联。因此业界对 token 做了进一步优化,设计了一种自包含令牌,令牌签发后无需从服务器存储中检查是否合法,通过解析令牌就能获取令牌的过期、有效等信息,这就是JWT (JSON Web Token)

一般的,一个基本的JWT令牌为一段点分3段式结构:

1
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

生成JWT的流程:

image-20200702221501133

  1. header json 的 base64 编码为令牌第一部分,用于描述关于该JWT的最基本的信息,例如其类型以及签名所用的算法等。

  2. payload json 的 base64 编码为令牌第二部分,可以用来放一些不敏感的信息。

    比如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    
    {
        "iss": "Kido JWT Service",
        "iat": 1441593502,
        "exp": 1441594722,
        "aud": "www.bundle.com",
        "sub": "kido@gmail.com",
        "from_user": "B",
        "target_user": "A"
    }
    

    其中的前五个字段都是由 JWT 的标准所定义的

    • iss: 该 JWT 的签发者
    • sub: 该 JWT 所面向的用户
    • aud: 接收该JWT的一方
    • exp(expires): 什么时候过期,这里是一个Unix时间戳
    • iat(issued at): 在什么时候签发的
  3. 将上面第一、第二部分拼接完的字符串用HS256算法进行加密。在加密的时候,我们还需要提供一个密钥(secret),加密后的内容也是一个字符串,最后这个字符串就是签名,把这个签名拼接在刚才的字符串后面就能得到完整的jwt。

缺点:

  • 安全性,因为数据只是Base64编码,并没有加密,所以不存存放敏感数据
  • 性能,JWT 过长,HTTP的header可能比body还大,请求的开销会更大
  • 一次性,如果想修改其中的内容,就必须签发一个新的JWT。

OAuth

OAuth(Open Authorization)是一个关于授权(authorization)的开放网络标准,而不是认证标准,提供资源的服务器不需要知道确切的用户身份(session),只需要验证授权服务器授予的权限(token)即可,目前的版本是2.0版

OAuth 在客户端与服务提供商之间,设置了一个 授权层(authorization layer)。客户端不能直接登录服务提供商,只能登录授权层,以此将用户与客户端区分开来。客户端登录授权层所用的令牌(token),与用户的密码不同。用户可以在登录的时候,指定授权层令牌的权限范围和有效期

客户端登录授权层以后,服务提供商根据令牌的权限范围和有效期,向客户端开放用户储存的资料

OAuth 负责解决分布式系统之间的授权问题,即使有时候客户端和资源服务器或者认证服务器存在同一台机器上。OAuth 没有解决认证的问题,但提供了良好的设计利于和现有的认证系统对接

工作流程:

image-20200702230856330

  1. 用户访问第三方应用程序(客户端),客户端要求用户给予授权。

  2. 用户同意给予客户端授权。

  3. 客户端使用第 2 步获得的授权,向认证服务器申请令牌。

  4. 认证服务器对客户端进行认证以后,确认无误,同意发放令牌。

  5. 客户端使用令牌,向资源服务器申请获取资源。

  6. 资源服务器确认令牌无误,同意向客户端开放资源。

上述流程中,第二步是关键,就是用户如何给客户端授权,有了授权之后,客户端就可以获取令牌,进而获取资源

通常有四种授权方式

  • 授权码方式(authorization code)

    • 指的是第三方应用先申请一个授权码,然后再用该码获取令牌
  • 简化方式(implicit)

    • 有些 Web 应用是纯前端应用,没有后端。这时就不能用上面的方式了,必须将令牌储存在前端。RFC 6749 就规定了第二种方式,允许直接向前端颁发令牌。这种方式没有授权码这个中间步骤,所以称为"隐藏式”(implicit)
  • 密码方式(resource owner password credentials)

  • 如果你高度信任某个应用,RFC 6749 也允许用户把用户名和密码,直接告诉该应用。该应用就使用你的密码,申请令牌,这种方式称为"密码式”(password)

  • 客户端方式(client credentials)

  • 适用于没有前端的命令行应用,即在命令行下请求令牌

OpenID Authentication

OpenID是一个去中心化的网上身份认证系统。对于支持OpenID的网站,用户不需要记住像用户名和密码这样的传统验证标记。取而代之的是,他们只需要预先在一个作为OpenID身份提供者(identity provider, IdP)的服务进行注册

如果要访问各种实体(例如Web 站点),通常需要与每个实体关联的唯一帐戸。OpenID 可使由 OpenID 提供者处理的一组凭证授予对支持 OpenID 的任何数目的实体的访问权

要求用户登录到某个支持 OpenID 并充当依赖方的实体(例如Web 站点)时,用户通过直接与 OpenID 提供者进行交互而不是向 依赖方(RP )提供其凭证来进行认证。OpenID 提供者验证用户身份,并将认证确认信息发送回 RP。从 OpenID 提供者收到此确认信息时,RP 会将用户作为已认证用户接受

工作流程:

image-20200702233648904

  1. 用户尝试访问受保护资源(例如Web 页面)

  2. 依赖方(RP)(在此示例中为 WebSphere Application Server)显示受保护资源的表单登录页面

  3. 用户返回给RP OpenID 标识

  4. RP 使用用户标识,并将用户重定向到相应 OpenID 提供者

  5. OpenID 提供者提示用户提供凭证

  6. 用户提供与 OpenID 提供者关联的帐戸的凭证

  7. OpenID 提供者认证用户,并(可选)提示用户批准或拒绝向 RP 提供用户信息。 它之后会将用户重定向回 RP,并返回认证结果。如果 OpenID 提供者认证成功,那么 RP 会尝试对用户进行授权。如果授权成功,那么 RP 会建立与用户的认证会话。

SAML

SAML,全称为Security Assertion Markup Language,是一种用于安全性断言的标记语言,目前的最新版本是2.0

SAML在单点登录中大有用处:在SAML协议中,一旦用户身份被主网站(身份鉴别服务器,Identity Provider,IDP)认证过后,该用户再去访问其他在主站注册过的应用(服务提供者,Service Providers,SP)时,都可以直接登录,而不用再输入身份和口令

工作流程:

image-20200702224617714

  1. 通过浏览器访问一个网页,若是联合身份域,则Google不会向用户索取用户名密码

  2. Google 服务生成SAML request

  3. 将用户请求重定向到 IDP 来认证身份

    类似这种URL:

    1
    
    https://idp.uni.nl/sso?SAMLRequest=fVLLTuswEN0j8Q…c%3D
    

    嵌入到HTTP请求中的的SAML request 就是 SAML 认证请求消息,它是基于XML的,一般长这样:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    
    <AuthnRequest  ID="kfcn...lfki" 
      Version="2.0" 
      IssueInstant="2020-02-05T08:28:50Z" 
      ProtocolBinding="urn:oasis:names:tc:SAML: 2.0:bindings:HTTP-POST" 
      ProviderName="google.com" 
      AssertionConsumerServiceURL="https://www.google.com/a/uni.nl/acs">
      <Issuer>google.com</Issuer>
      <NameIDPolicy  AllowCreate="true"
        Format="urn:oasis:names:tc:SAML:1.1:nameid-format:unspecified"/>;
    </AuthnRequest>;
    
  4. 当 IDP 收到重定向过来的认证请求时,首先就需要进行身份验证,若验证通过,则生成特定的响应跳转回Google的特定页面,(AssertionConsumerService,简称ACS),同样SAML的身份响应也是基于XML的,结构体如下:

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    
    <Response Version="2.0" 
      IssueInstant="2020-02-05T08:29:00Z" 
      Destination="https://www.google.com/a/my.uni.nl/acs" InResponseTo="kfcn...lfki">   
      <Issuer>https://idp.uni.nl/</Issuer>   
      <Status>
        <StatusCode   
          Value="urn:oasis:names:tc:SAML:2.0:status:Success"/> 
      </Status> 
      <Assertion Version="2.0" 
        IssueInstant="2020-02-05T08:29:00Z">     
        <Issuer>https://idp.uni.nl/</Issuer>   
        <Subject> 
          <NameID>alice</NameID>   
          <SubjectConfirmation ...> 
            <SubjectConfirmationData 
              NotOnOrAfter="2020-02-05T08:34:00Z"   
              Recipient="https://www.google.com/a/my.uni.nl/acs" InResponseTo="kfcn...lfki"/>  
            </SubjectConfirmation> 
        </Subject> 
        <Conditions NotBefore="2020-02-05T08:28:30Z" NotOnOrAfter="2020-02-05T08:34:00Z"> 
        </Conditions> 
        <AuthnStatement 
          AuthnInstant="2020-02-05T08:29:00Z" 
          SessionNotOnOrAfter="2020-02-05T16:29:00Z> 
        </AuthnStatement> 
      </Assertion>
     </Response>
    
  5. 当Google接受到SAML认证响应之后,会首先验证消息的签名是否正确以及是否因超时而失效。然后再从认证消息中提取出Google能识别用户身份(NameID),如果以上的步骤都是顺利的,用户将会成功登陆Google