什么是 oAuth
oAuth
协议为用户资源的授权提供了一个安全的、开放而又简易的标准。与以往的授权方式不同之处是 oAuth
的授权不会使第三方触及到用户的帐号信息(如用户名与密码),即第三方无需使用用户的用户名与密码就可以申请获得该用户资源的授权,因此oAuth
是安全的。
什么是 Spring Security
Spring Security
是一个安全框架,前身是Acegi Security
,能够为Spring
企业应用系统提供声明式的安全访问控制。Spring Security
基于Servlet
过滤器、IoC 和 AOP,为 Web 请求和方法调用提供身份确认和授权处理,避免了代码耦合,减少了大量重复代码工作。
为什么需要 oAuth2
应用场景
我们假设你有一个“云笔记”产品,并提供了“云笔记服务”和“云相册服务”,此时用户需要在不同的设备(PC、Android、iPhone、TV、Watch)上去访问这些“资源”(笔记,图片)
那么用户如何才能访问属于自己的那部分资源呢?此时传统的做法就是提供自己的账号和密码给我们的“云笔记”,登录成功后就可以获取资源了。但这样的做法会有以下几个问题:
“云笔记服务”和“云相册服务”会分别部署,难道我们要分别登录吗?
如果有第三方应用程序想要接入我们的“云笔记”,难道需要用户提供账号和密码给第三方应用程序,让他记录后再访问我们的资源吗?
用户如何限制第三方应用程序在我们“云笔记”的授权范围和使用期限?难道把所有资料都永久暴露给它吗?
如果用户修改了密码收回了权限,那么所有第三方应用程序会全部失效。
只要有一个接入的第三方应用程序遭到破解,那么用户的密码就会泄露,后果不堪设想。
为了解决如上问题,oAuth
应用而生。
名词解释
- 第三方应用程序(Third-party application): 又称之为客户端(client),比如上节中提到的设备(PC、Android、iPhone、TV、Watch),我们会在这些设备中安装我们自己研发的 APP。又比如我们的产品想要使用 QQ、微信等第三方登录。对我们的产品来说,QQ、微信登录是第三方登录系统。我们又需要第三方登录系统的资源(头像、昵称等)。对于 QQ、微信等系统我们又是第三方应用程序。
- HTTP 服务提供商(HTTP service): 我们的云笔记产品以及 QQ、微信等都可以称之为“服务提供商”。
- 资源所有者(Resource Owner): 又称之为用户(user)。 用户代理(User Agent): 比如浏览器,代替用户去访问这些资源。
- 认证服务器(Authorization server): 即服务提供商专门用来处理认证的服务器,简单点说就是登录功能(验证用户的账号密码是否正确以及分配相应的权限)
- 资源服务器(Resource server): 即服务提供商存放用户生成的资源的服务器。它与认证服务器,可以是同一台服务器,也可以是不同的服务器。简单点说就是资源的访问入口,比如上节中提到的“云笔记服务”和“云相册服务”都可以称之为资源服务器。
交互过程
oAuth 在 “客户端” 与 “服务提供商” 之间,设置了一个授权层(authorization layer)。”客户端” 不能直接登录 “服务提供商”,只能登录授权层,以此将用户与客户端区分开来。”客户端” 登录授权层所用的令牌(token),与用户的密码不同。用户可以在登录的时候,指定授权层令牌的权限范围和有效期。”客户端” 登录授权层以后,”服务提供商” 根据令牌的权限范围和有效期,向 “客户端” 开放用户储存的资料。
开放平台
交互模型
交互模型涉及三方:
- 资源拥有者:用户
- 客户端:APP
- 服务提供方:包含两个角色
1.
- 认证服务器 2.
- 资源服务器
认证服务器
认证服务器负责对用户进行认证,并授权给客户端权限。认证很容易实现(验证账号密码即可),问题在于如何授权。比如我们使用第三方登录 “有道云笔记”,你可以看到如使用 QQ 登录的授权页面上有 “有道云笔记将获得以下权限” 的字样以及权限信息认证服务器需要知道请求授权的客户端的身份以及该客户端请求的权限。我们可以为每一个客户端预先分配一个 id,并给每个 id 对应一个名称以及权限信息。这些信息可以写在认证服务器上的配置文件里。然后,客户端每次打开授权页面的时候,把属于自己的 id 传过来,如:
1
http://www.tangyuewei.com/login?client_id=yourClientId
随着时间的推移和业务的增长,会发现,修改配置的工作消耗了太多的人力。有没有办法把这个过程自动化起来,把人工从这些繁琐的操作中解放出来?当开始考虑这一步,开放平台的成型也就是水到渠成的事情了。
oAuth2 开放平台
开放平台是由 oAuth2.0 协议衍生出来的一个产品。它的作用是让客户端自己去这上面进行注册、申请,通过之后系统自动分配client_id
,并完成配置的自动更新(通常是写进数据库)。
客户端要完成申请,通常需要填写客户端程序的类型(Web、App 等)、企业介绍、执照、想要获取的权限等等信息。这些信息在得到服务提供方的人工审核通过后,开发平台就会自动分配一个client_id
给客户端了。
到这里,已经实现了登录认证、授权页的信息展示。那么接下来,当用户成功进行授权之后,认证服务器需要把产生的access_token
发送给客户端,方案如下:
- 让客户端在开放平台申请的时候,填写一个 URL,例如:http://www.tangyuewei.com
- 每次当有用户授权成功之后,认证服务器将页面重定向到这个URL(回调),并带
上access_token
,例如:http://www.tangyuewei.com?access_token=123456789
- 客户端接收到了这个
access_token
,而且认证服务器的授权动作已经完成,刚好可以把程序的控制权转交回客户端,由客户端决定接下来向用户展示什么内容。
令牌的访问与刷新
Access Token
Access Token 是客户端访问资源服务器的令牌。拥有这个令牌代表着得到用户的授权。然而,这个授权应该是临时的,有一定有效期。这是因为,Access Token 在使用的过程中可能会泄露。给 Access Token 限定一个 较短的有效期 可以降低因 Access Token 泄露而带来的风险。
然而引入了有效期之后,客户端使用起来就不那么方便了。每当 Access Token 过期,客户端就必须重新向用户索要授权。这样用户可能每隔几天,甚至每天都需要进行授权操作。这是一件非常影响用户体验的事情。希望有一种方法,可以避免这种情况。
于是oAuth2.0
引入了Refresh Token
机制
Refresh Token
Refresh Token
的作用是用来刷新Access Token
。认证服务器提供一个刷新接口,例如:
1
http://www.tangyuewei.com/refresh?refresh_token=&client_id=
传入refresh_token
和 client_id
,认证服务器验证通过后,返回一个新的 Access Token。为了安全,oAuth2.0 引入了两个措施:
oAuth2.0
要求,Refresh Token
一定是保存在客户端的服务器上 ,而绝不能存放在狭义的客户端(例如 App、PC 端软件)上。调用refresh
接口的时候,一定是从服务器到服务器的访问。oAuth2.0
引入了client_secret
机制。即每一个client_id
都对应一个client_secret
。这个client_secret
会在客户端申请client_id
时,随client_id
一起分配给客户端。客户端必须把client_secret
妥善保管在服务器上,决不能泄露。刷新Access Token
时,需要验证这个client_secret
。 实际上的刷新接口类似于:1
http://www.tangyuewei.com/refresh?refresh_token=&client_id=&client_secret=
以上就是
Refresh Token
机制。Refresh Token
的有效期非常长,会在用户授权时,随Access Token
一起重定向到回调URL
,传递给客户端。
客户端授权模式
概述
客户端必须得到用户的授权(authorization grant),才能获得令牌(access token)。oAuth 2.0 定义了四种授权方式。
- implicit:简化模式,不推荐使用
- authorization code:授权码模式
- resource owner password credentials:密码模式
- client credentials:客户端模式
简化模式
简化模式适用于纯静态页面应用。所谓纯静态页面应用,也就是应用没有在服务器上执行代码的权限(通常是把代码托管在别人的服务器上),只有前端 JS 代码的控制权。
这种场景下,应用是没有持久化存储的能力的。因此,按照oAuth2.0
的规定,这种应用是拿不到Refresh Token
的。
该模式下,access_token
容易泄露且不可刷新
授权码模式
授权码模式适用于有自己的服务器的应用,它是一个一次性的临时凭证,用来换取access_token
和refresh_token
。认证服务器提供了一个类似这样的接口:
1
https://www.tangyuewei.com/exchange?code=&client_id=&client_secret=
需要传入code
、client_id
以及client_secret
。验证通过后,返回access_token
和refresh_token
。一旦换取成功,code
立即作废,不能再使用第二次。
这个code
的作用是保护token
的安全性。简单模式下,token
是不安全的。这是因为在第 4 步当中直接把token
返回给应用。而这一步容易被拦截、窃听。引入了code
之后,即使攻击者能够窃取到code
,但是由于他无法获得应用保存在服务器的client_secret
,因此也无法通过code
换取token
。而第 5 步,为什么不容易被拦截、窃听呢?这是因为,首先,这是一个从服务器到服务器的访问,黑客比较难捕捉到;其次,这个请求通常要求是https
的实现。即使能窃听到数据包也无法解析出内容。
有了这个code
,token
的安全性大大提高。因此,oAuth2.0
鼓励使用这种方式进行授权,而简单模式则是在不得已情况下才会使用。
密码模式
密码模式中,用户向客户端提供自己的用户名和密码。客户端使用这些信息,向 “服务商提供商” 索要授权。在这种模式中,用户必须把自己的密码给客户端,但是客户端不得储存密码。这通常用在用户对客户端高度信任的情况下,比如客户端是操作系统的一部分。
一个典型的例子是同一个企业内部的不同产品要使用本企业的oAuth2.0
体系。在有些情况下,产品希望能够定制化授权页面。由于是同个企业,不需要向用户展示“xxx将获取以下权限”等字样并询问用户的授权意向,而只需进行用户的身份认证即可。这个时候,由具体的产品团队开发定制化的授权界面,接收用户输入账号密码,并直接传递给鉴权服务器进行授权即可。
客户端模式
如果信任关系再进一步,或者调用者是一个后端的模块,没有用户界面的时候,可以使用客户端模式。鉴权服务器直接对客户端进行身份验证,验证通过后,返回token
。