基于 Token 的 JWT 认证协议与 OAuth2.0 授权框架不恰当比较
2018-02-21 JHipster►Token JWT, OAuth2 0 评论
Authentication(认证)& Authorization(鉴权)
-
Authentication(认证)
关心你是谁
-
Authorization(鉴权)
关心你能干什么
举个大家一致都再说的例子:如果你去机场乘机,你持有的护照代表你的身份,这是认证,你的机票就是你的权限,你能干什么。
JWT & OAuth2
之所以说不恰当比较,是因为两者是完全不同的概念。
JWT 是一种认证协议
JWT提供了一种用于发布接入令牌(Access Token),并对发布的签名接入令牌进行验证的方法。 令牌(Token)本身包含了一系列声明,应用程序可以根据这些声明限制用户对资源的访问。
OAuth2 是一种授权框架
OAuth2是一种授权框架,提供了一套详细的授权机制。用户或应用可以通过公开的或私有的设置,授权第三方应用访问特定资源。
既然 JWT 和 OAuth2 没有可比性,为什么还要把这两个放在一起说呢?实际中确实会有很多人拿 JWT 和 OAuth2 作比较。很多情况下,在讨论OAuth2的实现时,会把JSON Web Token作为一种认证机制使用。这也是为什么他们会经常一起出现。
先来搞清楚 JWT 和 OAuth2 究竟是做什么的。
JSON Web Token (JWT)
-
官网: https://jwt.io/
-
JWT是一种安全标准。基本思路就是用户提供用户名和密码给认证服务器,服务器验证用户提交信息信息的合法性;如果验证成功,会产生并返回一个Token(令牌),用户可以使用这个token访问服务器上受保护的资源。
-
JWT 实际上是一个字符串,由 头部、载荷、签名 三部分以
.
拼接而成。-
JWT = Header(头部) + Payload(载荷) + VerifySignature(签名)
JWT 标准示例:
-
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJKb2huIFd1IEpXVCIsImlhdCI6MTQ0MTU5MzUwMiwiZXhwIjoxNDQxNTk0NzIyLCJhdWQiOiJ3d3cuZXhhbXBsZS5jb20iLCJzdWIiOiJqcm9ja2V0QGV4YW1wbGUuY29tIiwiZnJvbV91c2VyIjoiQiIsInRhcmdldF91c2VyIjoiQSJ9.drqyR1iSTn3I-0rsspWZUKig_mMxhQuZVFQQ3GldoLs
Header(头部)
头部用于描述关于该JWT的最基本的信息,例如其类型以及签名所用的算法等。这也可以被表示成一个JSON对象。
1 2 3 4 | { "typ": "JWT", "alg": "HS256" } |
在这里,我们说明了这是一个 JWT ,并且我们所用的签名算法(后面会提到)是HS256算法。
对它也要进行 Base64 编码,之后的字符串就成了 JWT 的 Header(头部)。
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
Payload(载荷)
1 2 3 4 5 6 7 8 9 | { "iss": "John Wu JWT", "iat": 1441593502, "exp": 1441594722, "aud": "www.example.com", "sub": "jrocket@example.com", "from_user": "B", "target_user": "A" } |
这里面的前五个字段都是由JWT的标准所定义的。
iss: 该 JWT 的签发者
iat(issued at): 在什么时候签发的
exp(expires): 什么时候过期,这里是一个Unix时间戳
aud: 接收该 JWT 的一方
sub: 该 JWT 所面向的用户
将上面的 JSON 对象进行 Base64编码 可以得到下面的字符串。这个字符串我们将它称作 JWT 的 Payload(载荷)。
eyJpc3MiOiJKb2huIFd1IEpXVCIsImlhdCI6MTQ0MTU5MzUwMiwiZXhwIjoxNDQxNTk0NzIyLCJhdWQiOiJ3d3cuZXhhbXBsZS5jb20iLCJzdWIiOiJqcm9ja2V0QGV4YW1wbGUuY29tIiwiZnJvbV91c2VyIjoiQiIsInRhcmdldF91c2VyIjoiQSJ9
VerifySignature(签名)
假设 Private Key(私钥) 是 `secret` 使用 HS256 算法加密,则得到 `加密私钥`
drqyR1iSTn3I-0rsspWZUKig_mMxhQuZVFQQ3GldoLs
三者以 . 拼接组成一个标准的 JWT
签名的目的
签名的目的是为了保证上边两部分信息不被篡改。如果尝试使用 Base64 对 头部以及载荷的内容解码之后对 Token 进行修改。
由于不知道服务器加密的时候使用的私钥,计算出来的签名一定不一样,签名信息就会失效。
一般使用一个私钥(Private Key)通过特定算法对 Header 和 Payload 进行混淆产生签名信息,所以只有原始的 Token 才能与签名信息匹配。
服务器应用在接受到 JWT 后,会首先对头部和载荷的内容用同一算法再次签名。
具体算法已经在 JWT 的头部中已经用 alg 字段指定,再加上私钥进行计算。
签名信息不匹配,说明这个 Token 的内容被别人动过的,服务器会拒绝这个 Token ,返回一个 HTTP 401 Unauthorized 响应。
-
这里有一个重要的实现细节
只有获取了私钥的应用程序(比如服务器端应用)才能完全认证 Token 包含声明信息的合法性。所以,永远不要把私钥信息放在客户端(比如浏览器)。
OAuth2 (Open Authorization)
-
OAuth2 不是一个标准协议,而是一个安全的授权框架。它详细描述了系统中不同角色、用户、服务前端应用(比如 API ),以及客户端(比如网站或移动 App)之间怎么实现相互认证。
-
其目的是为了让用户同意(授权)第三方应用,以便其访问当前所处服务的某些资源。这整个过程,这个第三方应用是并不能得知用户除授权信息以外的内容的(例如帐号或密码)
-
场景
某 App 希望获得 Tom 在 Facebook 上相关的数据
-
基本概念
Roles 角色
应用程序或者用户都可以是下边的任何一种角色:
-
资源拥有者 (Resource Owner) - 这里是 Tom
-
资源服务器 (Resource Server) - 这里是 Facebook
-
客户端应用 (Authorization Server) - 这里当然还是 Facebook ,因为 Facebook 有相关数据
-
认证服务器 (Client) - 这里是某 App
当 Tom 试图登录 Facebook ,某 App 将他重定向到 Facebook 的授权服务器,
当 Tom 登录成功,并且许可自己的 Email 和个人信息被某 App 获取。
这两个资源被定义成一个 Scope(权限范围),一旦准许,某 App 的开发者就可以申请访问权限范围中定义的这两个资源。
+--------+ +---------------+
| |--(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 ---| |
+--------+ +---------------+
Tom 允许了权限请求,再次通过重定向返回某 App ,重定向返回时携带了一个 Access Token(访问令牌),
接下来某 App 就可以通过这个 Access Token 从 Facebook 直接获取相关的授权资源(也就是 Email 和个人信息),
而无需重新做 Tom 相关的鉴权。而且每当 Tom 登录了某 App ,都可以通过之前获得的 Access Token ,直接获取相关授权资源。
到目前为止,我们如何直接将以上内容用于实际的例子中?OAuth2 十分友好,并容易部署,所有交互都是关于客户端和权限范围的。
关于第三方应用授权
拼接有效的oauth地址,请求授权服务
例如:https://oauth2server.com/auth?response_type=code&client_id=xxxx&redirect_uri=REDIRECT_URI&scope=photos&state=abc123
-
response_type: 代表授权后需要得到授权码
-
client_id: 第三方应用在授权服务申请成为合法可授权应用后所分配的应用标识,授权服务器可以通过它来区分是哪个第三方应用
-
redirect_url: 授权完成后,跳回第三方应用的地址
-
scope: 需要得到授权的范围内容(想要被授权哪些权限)
-
state: 随机字符(用于防止一些攻击)
询问用户可否授权
授权服务器在验证通过上述内容后,向用户呈现出第三方应该请求授权的提示,用户可以选择同意或者拒绝
-
拒绝: 用户不同意授权
-
同意: 用户同意授权,授权服务生成
auth_code
(授权码),并携带该信息回跳到第三方应用指定的redirect_uri
上
令牌交换,换取 access_token
授权服务在得到用户同意授权后,回将授权码(auth_code)带给第三方应用,第三方应用可以通过它来换取最终的授权凭证(access_token)
安全校验: 在得到服务器回调后,首先应当校验是否是你发出去的 state ,可以防止重定向端点的欺诈
-
auth_code:授权凭证,用它可换取
access_token
请求授权服务器,换取
access_token
(这个请求不是公开的)例如:https://api.oauth2server.com/token/grant_type=authorization_code&auth_code=xxxxxxxx&redirect_uri=REDIRECT_URI&client_id=xxxx&client_secret=secret
-
grant_type:
authorization_code
需要换取access_token
的请求 -
auth_code: 授权凭证
-
redirect_uri: 请求授权时发送的授权地址
-
client_id: 第三方应用在授权服务申请成为合法可授权应用后所分配的应用标识,授权服务器可以通过它来区分是哪个第三方应用
-
client_secret: 第三方应用在授权服务申请成为合法可授权应用后所分配的应用标识所对应的密钥信息(这是不能公开的重要信息)
授权服务验证授权码信息完成后,会返回 access token
例如:{"access_token":"A2g75_f2gg9g12gaoxgw","expires_in":"36000"}
获取授权内容
在 access_token
未过期的时间中,通过携带 access_token
去请求指定的资源 api 来回去授权用户内容。
OAuth2 “漏洞”
其实 OAuth2.0 的协议规范是不存在漏洞的(或者说迄今为止还未出现),但是由于开发者未能完全闭环的实现 OAuth2 的流程造成了一些问题
忽略 State 参数
忽略state可能会引发一个csrf攻击
通过 OAuth2 流程可以看出,核心数据是 access_token
,而 access_token
是用 auth_code
换取,看一下例子。
用户 A 请求授权,在流程中授权服务器返回了包含 auth_code=123
的回调
用户 B 请求授权,在流程中授权服务器返回了包含 auth_code=abc
的回调
到这里整个流程都是正常的,但是此时,如果把回调里面的授权码交换
用户 A 拿着 B 的授权码再请求授权码服务器的时候授权服务器已经不知道当前拿着授权码的用户是谁,它关心的是授权码是否有效,所以它会下发用户 B 的 access_token
,然后在第三方应用中,它却与用户 A 绑定。
state
其实是可以和授权的用户建立一个等价关系,所有 state
参数是很关键的。
引入 JWT
OAuth2 并不关心去哪找 Access Token 和把它存在什么地方的,生成随机字符串并保存 Token 相关的数据到这些字符串中并保存。
通过一个 Endpoints(令牌终端)
,其他服务可能会关心这个 Token 是否有效,它可以通过哪些权限。
这就是用户信息URL方法,授权服务器为了获取用户信息转换为资源服务器。
当我们谈及微服务时,我们需要找一个 Token 存储的方式,来保证授权服务器可以被水平扩展,尽管这是一个很复杂的任务。
所有访问微服务资源的请求都在 Http Header 中携带 Token,被访问的服务接下来再去请求授权服务器验证 Token 的有效性,
目前这种方式,我们需要两次或者更多次的请求,但这是为了安全性也没什么其他办法。
但扩展 Token 存储会很大影响我们系统的可扩展性,这是我们引入 JWT(读jot)的原因。
+-----------+ +-------------+
| | 1-Request Authorization | |
| |------------------------------------>| |
| | grant_type&username&password | |--+
| | |Authorization| | 2-Gen
| Client | |Service | | JWT
| | 3-Response Authorization | |<-+
| |<------------------------------------| Private Key |
| | access_token / refresh_token | |
| | token_type / expire_in / jti | |
+-----------+ +-------------+
简短来说,响应一个用户请求时,将用户信息和授权范围序列化后放入一个 JSON 字符串,然后使用 Base64 进行编码,
最终在授权服务器用私钥对这个字符串进行签名,得到一个JSON Web Token,我们可以像使用 Access Token一样的直接使用它,
假设其他所有的资源服务器都将持有一个 RSA 公钥。当资源服务器接收到这个在 Http Header 中存有 Token 的请求,
资源服务器就可以拿到这个 Token,并验证它是否使用正确的私钥签名(是否经过授权服务器签名,也就是验签)。
验签通过,反序列化后就拿到 OAuth2 的验证信息。
验证服务器返回的信息可以是以下内容:
-
access_token - 访问令牌,用于资源访问
-
refresh_token - 当访问令牌失效,使用这个令牌重新获取访问令牌
-
token_type - 令牌类型,这里是Bearer也就是基本HTTP认证
-
expire_in - 过期时间
-
jti - JWT ID
由于 Access Token 是 Base64 编码,反编码后就是下面的格式,标准的 JWT 格式。也就是 Header、 Payload、Signature 三部分。
{
"alg":"RS256",
"typ":"JWT"
}
{
"exp": 1492873315,
"user_name": "reader",
"authorities": [
"AURH_READ"
],
"jti": "8f2d40eb-0d75-44df-a8cc-8c37320e3548",
"client_id": "web_app",
"scope": [
"FOO"
]
}
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret
)
使用JWT可以简单的传输Token,用 RSA 签名保证Token很难被伪造。
Access Token字符串中包含用户信息和权限范围,我们所需的全部信息都有了,
所以不需要维护 Token 存储,资源服务器也不必要求 Token 检查。
+-----------+ +-----------+
| | 1-Request Resource | |
| |----------------------------------->| |
| | Authorization: bearer Access Token | |--+
| | | Resource | | 2-Verify
| Client | | Service | | Token
| | 3-Response Resource | |<-+
| |<-----------------------------------| Public Key|
| | | |
+-----------+ +-----------+
所以,在微服务中使用OAuth 2,不会影响到整体架构的可扩展性。
这里还有一些问题没有涉及,例如 Access Token 过期后,使用 Refresh Token 到认证服务器重新获取 Access Token 等。
Client Types 客户端类型
这里的客户端主要指API的使用者。它可以是的类型:
-
私有的(自建)
-
公开的(Google、GitHub、Twitter、Facebook 等)
Client Profile 客户端描述
OAuth2 框架也指定了集中客户端描述,用来表示应用程序的类型:
-
Web应用
-
用户代理
-
原生应用
Authorization Grants 认证授权
认证授权代表资源拥有者授权给客户端应用程序的一组权限,可以是下边几种形式:
-
授权码模式(Authorization Code)
-
简化模式(Implicit)
-
密码模式(Resource Owner Password Credentials)
-
客户端模式(Client Credentials)
-
令牌终端(Endpoints)
OAuth2框架需要下边几种终端:
-
认证终端
-
Token 终端
-
重定向终端
从上边这些应该可以看出,OAuth2 定义了一组相当复杂的规范。
总结
JWT 使用场景
无状态的分布式 API
JWT 的主要优势在于使用无状态、可扩展的方式处理应用中的用户会话。服务端可以通过内嵌的声明信息,很容易地获取用户的会话信息,而不需要去访问用户或会话的数据库。在一个分布式的面向服务的框架中,这一点非常有用。
但是,如果系统中需要使用黑名单实现长期有效的 Token 刷新机制,这种无状态的优势就不明显了。
优势
-
快速开发
-
不需要 Cookie
-
JSON 在移动端的广泛应用
-
不依赖于社交登录
-
相对简单的概念理解
限制
-
Token有长度限制
-
Token不能撤销
-
需要 Token 有失效时间限制(exp)
OAuth2 使用场景
外包认证服务器
上边已经讨论过,如果不介意API的使用依赖于外部的第三方认证提供者,你可以简单地把认证工作留给认证服务商去做。
也就是常见的,去认证服务商(比如 Facebook)那里注册你的应用,然后设置需要访问的用户信息,比如电子邮箱、姓名等。
当用户访问站点的注册页面时,会看到连接到第三方提供商的入口。
用户点击以后被重定向到对应的认证服务商网站,获得用户的授权后就可以访问到需要的信息,然后重定向回来。
优势
-
快速开发
-
实施代码量小
-
维护工作减少
-
灵活的实现方式
-
可以和 JWT 同时使用
-
可针对不同应用扩展
限制
- 框架沉重
具体选哪种认证、鉴权需要根据业务具体场景甄选,适应实际需求才是最好的。
参考文献:
http://www.ruanyifeng.com/blog/2014/05/oauth_2_0.html
https://segmentfault.com/a/1190000009164779