使用 OAuth 2 和 JWT 为微服务提供安全保障

47

Part 1 - 理论相关

作者 freewolf

关键词

微服务Spring CloudOAuth 2.0JWTSpring SecuritySSOUAA

写在前面

作为从业了十多年的IT行业和程序的老司机,今天如果你说你不懂微服务,都不好意思说自己的做软件的。SOA喊了多年,无人不知,但又有多少系统开发真正的SOA了呢?但是好像一夜之间所有人都投入了微服务的怀抱。

作为目前最主流的“微服务框架”,Spring Cloud发展速度很快,成为了最全面的微服务解决方案。不管什么软件体系,什么框架,安全永远是不可能绕开的话题,我也把它作为我最近一段时间研究微服务的开篇。

老话题!“如何才能在微服务体系中保证安全?”,为了达成目标,这里采用一个简单而可行方式来保护Spring Cloud中服务的安全,也就是建立统一的用户授权中心。

这里补充说一下什么是Authentication(认证)Authorization(鉴权),其实很简单,认证关心你是谁,鉴权关心你能干什么。举个大家一致都再说的例子,如果你去机场乘机,你持有的护照代表你的身份,这是认证,你的机票就是你的权限,你能干什么。

学习微服务并不是一个简单的探索过程,这不得学习很多新的知识,其实不管是按照DDD(Domain Driven Design)领域驱动设计中领域模型的方式,还是将微服务拆分成更小的粒度。都会遇到很多新的问题和以前一直都没解决很好的问题。随着不断的思考,随着熟悉Facebook/GitHub/AWS这些机构是如何保护内部资源,答案也逐渐浮出水面。

为了高效的实现这个目标,这里采用OAuth 2JWT(JSON Web Tokens)技术作为解决方案,

为什么使用OAuth 2

尽管微服务在现代软件开发中还算一个新鲜事物,但是OAuth 2已经是一个广泛使用的授权技术,它让Web开发者在自己提供服务中,用一种安全的方式直访问Google/Facebook/GitHub平台用户信息。但在我开始阐述细节之前,我将揭开聚焦到本文真正的主题:云安全

那么在云服务中对用户访问资源的控制,我们一般都怎么做呢?然我举一些大家似乎都用过的但又不是很完美的例子。

我们可以设置边界服务器或者带认证功能的反向代理服务器,假设所有访问请求都发给它。通过认证后,转发给内部相应的服务器。一般在Spring MVC Security开发中几乎都会这样做的。但这并不安全,最重要的是,一旦是有人从内部攻击,你的数据毫无安全性。

其他方式:我们为所有服务建立统一的权限数据库,并在每次请求前对用户进行鉴权,听起来某些方面的确有点愚蠢,但实际上这确实是一个可行的安全方案。

更好的方式: 用户通过授权服务来实现鉴权,把用户访问Session映射成一个Token。所有远程访问资源服务器相关的API必须提供Token。然后资源服务器访问授权服务来识别Token,得知Token属于哪个用户,并了解通过这个Token可以访问什么资源。

这听起来是个不错的方案,对不?但是如何保证Token的安全传输?如何区分是用户访问还是其他服务访问?这肯定是我们关心的问题。

所以上述种种问题让我们选择使用OAuth 2,其实访问Facebook/Google的敏感数据和访问我们自己后端受保护数据没什么区别,并且他们已经使用这样的解决方案很多年,我们只要遵循这些方法就好了。

OAuth 2是如何工作的

如果你了解OAuth 2相关的原理,那么在部署OAuth 2是非常容易的。
让我们描述下这样一个场景,“某App希望获得TomFacebook上相关的数据”

OAuth 2 在整个流程中有四种角色:

  • 资源拥有者(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 TokenFacebook直接获取相关的授权资源(也就是Email和个人信息),而无需重新做Tom相关的鉴权。而且每当Tom登录了某App,都可以通过之前获得的Access Token,直接获取相关授权资源。

到目前为止,我们如何直接将以上内容用于实际的例子中?OAuth 2十分友好,并容易部署,所有交互都是关于客户端和权限范围的。

  • OAuth 2中的客户端权限范围和我们平时的用户和权限是否相同?

  • 我需要将授权映射到权限范围中或将用户映射到客户端中?

  • 为什么我需要客户端?

你也许在之前在类似的企业级开发案例中尝试映射过相关的角色。这会很棘手!

任何类型的应用都提供用户登录,登录结果是一个Access Token,所有的之后的API调用都将这个Access Token加入HTTP请求头中,被调用服务去授权服务器验证Access Token并获取该Token可访问的权限信息。这样一来,所有服务的访问都会请求另外的服务来完成鉴权。

权限范围和角色,客户端和用户

OAuth 2中,你可以定义哪个应用(网站、移动客户端、桌面应用、其他)可以访问那些资源。这里只有一个尺寸,来自哪里的哪个用户可以访问那些数据,当然也是哪个应用或者服务可以访问那些资源。换一种说法,权限范围就是控制那些端点对客户端可见,或者用户根据他的权限来获取相关的数据。

在一个在线商店中,前端可以看做一个客户端,可以访问商品、订单和客户信息,但后端可以关于物流和合同等,另一方面,用户可以访问一个服务但并不是全部的数据,这可以是因为用户正在使用Web应用,当他不能的时候,其他用户却可以。服务之间的访问时我们要讨论的另一个维度。如果你熟悉数学,我可以说在OAuth 2中,客户端-权限范围关系是线性独立于用户-权限关系。

为什么是JWT

OAuth 2并不关心去哪找Access Token和把它存在什么地方的,生成随机字符串并保存Token相关的数据到这些字符串中保存好。通过一个令牌端点,其他服务可能会关心这个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,并验证它是否使用正确的私钥签名(是否经过授权服务器签名,也就是验签)。验签通过,反序列化后就拿到OAuth 2的验证信息。

验证服务器返回的信息可以是以下内容:

  • access_token - 访问令牌,用于资源访问

  • refresh_token - 当访问令牌失效,使用这个令牌重新获取访问令牌

  • token_type - 令牌类型,这里是Bearer也就是基本HTTP认证

  • expire_in - 过期时间

  • jti - JWT ID

由于Access TokenBase64编码,反编码后就是下面的格式,标准的JWT格式。也就是HeaderPayloadSignature三部分。

{ 
  "alg":"RS256",
  "typ":"JWT"
}
{
  "exp": 1492873315,
  "user_name": "reader",
  "authorities": [
    "AURH_READ"
  ],
  "jti": "8f2d40eb-0d75-44df-a8cc-8c37320e3548",
  "client_id": "web_app",
  "scope": [
    "FOO"
  ]
}
&:lƧs)ۡ-[+
F"2"Kآ8ۓٞ:u9ٴ̯ޡ 9Q32Zƌ޿$ec{3mxJh0DF庖[!뀭N)㥔knVVĖV|夻ׄE㍫}Ŝf9>'<蕱굤Bۋеϵov虀DӨ8C4K}Emޢ    YVcaqIW&*uʝub!׏*Ť\՟-{ʖX܌WTq

使用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等,后面会有具体的例子来展开讨论这些问题。

如果您感兴趣,后面还会有实现部分,敬请期待~

由于 http://asciiflow.com/ 流程图使用中文就无法对齐了,本文中流程图都是英文了~

你可能感兴趣的

29 条评论
jlkmhh · 3月30日

如果应用不打算被用来作第三方登陆,不使用oauth2,只用jwt也可以实现相同的功能,流程更简单

+1 回复

Ceker · 2017年04月23日

公私钥这个性能怎么样?

回复

0

基本不存在性能问题吧 这样处理 授权服务只生成 JWT,只进行一次签名,token本身不加密只做了base64. 其他就是资源服务器验签。分布式的 互相不依赖 没啥问题

freewolf 作者 · 2017年04月23日
0

目前看 这是比较好的一种方式 内部HTTP通讯也会验证 微服务内部尽量减少HTTP通讯, 尽量都通过RPC和MQ来进行数据交换~HTTP串行通讯,问题很多

freewolf 作者 · 2017年04月23日
0

RSA签名要慢一点,大概几百毫秒吧,验签快得很,瞬时响应。写个测试就知道了。

张豪 · 2017年10月10日
wangxufire · 2017年04月25日

每个接口都得去授权服务验证Token?JWT存的东西多会很长,scope不只是简单的read、write,成每个接口,如果都存token里。

回复

0

说错了 是每次请求都得去授权服务验证Token吗?会不会很慢

wangxufire · 2017年04月25日
0

@wangxufire 只有第一次登录时完成授权,然后拿着token直接访问接口,资源服务器自己就能验证token(有公钥),无需再次连接授权服务器,权限可以自己配置,如果你觉得JWT东西太多,这个处理比较麻烦,只能找个服务器统一存储,每次返回JWT-ID,然后每次根据JWT ID再去还原内容。我觉得Token长点没事儿

freewolf 作者 · 2017年04月25日
0

@freewolf 但是github的tokrn就很短,看着很优雅,不知道是什么流程,如果做了存储那么每次请求就又得花查询的开销了

wangxufire · 2017年04月25日
内涵的涵 · 2017年04月26日

请告知原文地址

回复

0

这就是原文呀,参考了一些文章 都是碎片的~

freewolf 作者 · 2017年04月27日
pc813 · 2017年05月01日

freewolf 你好,我把此文章放到了 linkedinfo (https://www.linkedinfo.co) ,我的一个业余项目。如您觉得不妥,请随时联系我将之撤下。

linkedinfo.co 是一个集合了各类优秀技术文章的站点,不展现全文,所有条目都附上作者与原文链接,读者最终都会进入到作者原文的站点。做 linkedinfo 的初衷是想方便自己且方便其他想学习的朋友,能更方便地找到自己感兴趣的技术文章。

各位感兴趣的朋友可以去 https://linkedinfo.co 看看,非常希望能得到你们的意见与建议 (可在about 页面留言 https://linkedinfo.co/about )。

回复

0

这个网站是你的业务项目?

Nathan · 2017年07月24日
0

业余玩玩,希望有时间搞好它。

pc813 · 2017年07月24日
采果 · 2017年09月18日

hi, 刚学习,一直对oAuth很困惑,访问resouce server的时候需要带上token,那访问普通api的时候要怎么操作,是所有api都在resource server上吗?能加个微信请教吗?谢谢,kevinzhong1987

回复

软件之道在易 · 2017年11月12日

我现在研究 Spring-Cloud + SSO + OAuth + JWT,不知道各位有什么好文章推荐?

回复

0

我也是,可以请教吗?

王静 · 7月18日
Richard_Li · 3月12日

求问part 2 在哪里

回复

0

part2还没写呢 哈哈哈 最近我准备放出一个开源的微服务 rbac 微服务 jwt集成

freewolf 作者 · 3月15日
0

@freewolf 博主准备用RBAC几?

发给官兵 · 5月3日
0

@发给官兵 其实无所谓看具体需求

freewolf 作者 · 12月4日
fmadcat · 10月16日

期待Part2...

回复

0

都拖一年了,竟然还有人期待,我这拖延症,也够可以,其实代码早有了,最近整理放上来吧。

freewolf 作者 · 10月16日
0

多谢了~最近正在做这块,想寻求一个最佳实践方案~

fmadcat · 10月16日
happy菇凉 · 12月4日

用了oauth2.0之后,传统用户名密码登录怎么做呢

回复

0

@happy菇凉 你登陆你自己的 oauth是第三方授权

freewolf 作者 · 12月4日
载入中...