1、Oauth2协议
1.1、什么是Oauth2
OAuth(开放授权)是一个开放标准,允许用户授权第三方移动应用访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方移动应用或分享他们数据的所有内容,OAuth2.0是OAuth协议的延续版本,但不向后兼容OAuth 1.0即完全废止了OAuth1.0。
第三方应用授权登录:在APP或者网页接入一些第三方应用时,时长会需要用户登录另一个合作平台,比如QQ,微博,微信的授权登录。为了这一功能的统一实现,于是出现了Oauth2标准,用于规范第三方登录接口与流程。
1.1.1、场景
场景模拟:
加入我住在一个小区内,经常购买快递,那么快递员需要通过门禁进来送快递。如果我将我的门禁账号密码给他,不是很安全。他只使用到了门禁,但他拥有了我所有的权限。
于是,我设计了一套授权机制。
第一步,门禁系统的密码输入器下面,增加一个按钮,叫做"获取授权"。快递员需要首先按这个按钮,去申请授权。
第二步,他按下按钮以后,屋主(也就是我)的手机就会跳出对话框:有人正在要求授权。系统还会显示该快递员的姓名、工号和所属的快递公司。
我确认请求属实,就点击按钮,告诉门禁系统,我同意给予他进入小区的授权。
第三步,门禁系统得到我的确认以后,向快递员显示一个进入小区的令牌(access token)。令牌就是类似密码的一串数字,只在短期内(比如七天)有效。
第四步,快递员向门禁系统输入令牌,进入小区。
有人可能会问,为什么不是远程为快递员开门,而要为他单独生成一个令牌?这是因为快递员可能每天都会来送货,第二天他还可以复用这个令牌。另外,有的小区有多重门禁,快递员可以使用同一个令牌通过它们。
互联网场景:
我们把上面的例子搬到互联网,就是 OAuth 的设计了。
首先,居民小区就是储存用户数据的网络服务。比如,微信储存了我的好友信息,获取这些信息,就必须经过微信的"门禁系统"。
其次,快递员(或者说快递公司)就是第三方应用,想要穿过门禁系统,进入小区。
最后,我就是用户本人,同意授权第三方应用进入小区,获取我的数据。
简单说,OAuth 就是一种授权机制。数据的所有者告诉系统,同意授权第三方应用进入系统,获取这些数据。系统从而产生一个短期的进入令牌(token),用来代替密码,供第三方应用使用。
值得注意的是,我们需要时刻注意我们自己的身份是什么。比如我现在开发了一个博客系统需要使用QQ登录,那么我们这个博客系统是第三方应用。
1.1.2、实现流程
提前准备:
客户端\第三方应用会向开放平台(QQ、微信)申请一个id和密钥,通过这个id和密钥声明自己是第三方应用。
认证流程:
- 用户使用开放平台接口登录客户端,客户端向用户发送一个携带自己第三方应用id的标识
- 用户同意开放平台登录,返回一个授权书
- 客户端通过这个授权书向授权服务器申请一个access token
- 至此,我们就拿到了类似于用户密码登录获取的token
- 通过这个token向资源服务器获取属于该用户的访问资源
资源服务器和授权服务器都是我们自己需要部署的服务,并且他们通常部署在一起。
事实上我们已经可以总结了:第三方应用携带者自身标识id重定向到开放平台登录,登录成功之后会重定向回我们的登录页,并携带上授权书。我们的后端就可以通过授权书再去请求开放平台后端拿到属于该用户的信息。
事实上,关键就在于这个授权书倒是是什么东西,可以是授权码,可以是token,可以是用户的账号和密码。
我们举一个小例子,以微博开放平台为例
1.2、四种授权实现
我们发现,在B这一操作是非常重要的,当用户确定授权的时候,会返回一个Authorization Grant(授权书)。我们并不知道他里面到底是一个什么东西,要确定他里面到底是账号密码,还是一个token,需要我们确认具体的登录形式是什么。
Oauth2协议定义了四种授权形式
- 授权码模式(authorization code)
- 简化模式(implicit,隐藏式)
- 密码模式(resource owner password credentials)
- 客户端模式(client credentials)
1.2.1、获取授权书
在了解四种授权形式之前,我们需要了解怎样请求授权书,我们需要携带什么参数
- 应用名称
- 应用网站
- 重定向URL或者回调URL(redirect_uri)
- 客户端标识(client_id)
- 客户端密钥(client_secret)
1.2.2、授权码模式
授权码模式(authorization code)是功能最完整、流程最严密的授权模式。
client:客户端,我们的服务
User-agent:用户浏览器
Resource Owner:资源服务
Autherization server:认证服务
1.2.2.1、获取授权码code
客户端携带一下参数重定向到开放平台,进行用户登录,登录成功后又重定向到客户端并携带授权码code,至此获取到了code码
携带参数如下:
https://开放平台.com/oauth/authorize?
response_type=code # (必须)响应类型,固定为code
client_id # (必须)客户端标识,标识是什么第三方登录
redirect_uri # (可选)重定向url,代表登录之后重定向到哪里
state # (可选推荐)此次登录标识,由后端生成并存储,该随机数在开放平台登录成功后会返回回来。用于标识登录,防止csrf跨域攻击
scope # (可选)请求资源范围,多个空格隔开。如果和客户端申请的一致,可忽略
refresh_token # (可选)表示更新令牌,用于获取下一次令牌
返回参数如下:
code=123456 # 用户登录成功码,服务器通过code + client_id(第三方服务id) + client_secret(第三方服务密钥)三个关键参数,可以去开放平台获取到AccessToekn,AccessToken能够去开放平台获取到属于该用户的一些信息,姓名头像等等
state # 与前面相同,与后台核验,用于防止csrf跨域攻击
此code即Authorization Code,我们的code很重要。在接下来资源请求中,code + client_id(第三方服务id) + client_secret(第三方服务密钥)三个参数同时向开放平台请求,才能获取一个密钥AccessToken,能够向开放平台请求获取到该用户的数据。
疑问:为什么用户同意登录,不直接颁发token,而是获取一个code,再通过code拼接请求来token,再通过token获取用户信息呢?
原因:如果不使用code而是直接颁发token。浏览器发送请求到开放平台,平台直接发送token,前端接收到token,并给到后端去。如此,则会将token暴露在浏览器中,有安全风险。而使用code码,同样会暴露到浏览器中,但是我们最终获取token还需要携带client_secret(第三方服务密钥,也就是我们后端密钥),这是直接通过后端服务器请求的并且client_secret(第三方服务密钥)是不暴露的。因此只要client_secret不暴露,就没有安全风险。
简而言之,code需要结合我后端服务器的秘钥才完整,才可以请求该用户信息。就算code暴露,黑客也不能够获取到该用户信息。
1.2.2.2、获取用户信息
前面我们提到,开放平台会生成一个code,并重定向到我们的网站中,至此我们拿到了code码。
接下来我们就需要获取到用户信息了,我们使用,code + client_id(第三方服务id) + client_secret(第三方服务密钥)三个关键参数,去向开放平台申请AccessToken,获取到的AccessToken就能够直接获取用户信息。
注意:在这段操作中,我们是完全不与用户端进行交互的,后端拿到code之后完完全全是后端在向开放平台请求AccessToken,因此授权码code模式是非常安全的模式。
我们的网站拿到code之后,请求AccessToken令牌参数
https://开放平台.com/oauth/token?
client_id=CLIENT_ID& # 标识我们平台的id
client_secret=CLIENT_SECRET& # 后端请求密钥,不可暴露
code=AUTHORIZATION_CODE& # 用户code码
grant_type=authorization_code& # 响应内容标识
redirect_uri=CALLBACK_URL # 成功之后重定向的链接
开放平台接收请求,会重定向到参数地址,并发送一段json数据,如下
{
"access_token":"ACCESS_TOKEN", # 用户请求token
"token_type":"bearer",
"expires_in":2592000,
"refresh_token":"REFRESH_TOKEN", # 刷新token
"scope":"read",
"uid":100101,
"info":{...}
}
至此获取到AccessToken之后,我们后端就可以使用该token再去开放平台获取属于该用户的用户信息了。
1.2.2.3、总结
如下图所示,授权码请求完整流程
1.2.3、简单模式
有些 Web 应用是纯前端应用,没有后端。这时就不能用上面的方式了,必须将令牌储存在前端。RFC 6749 就规定了第二种方式,允许直接向前端颁发令牌。这种方式没有授权码这个中间步骤,所以称为(授权码)"隐藏式"(implicit)也叫简单模式。
前面我们提到了为什么用code码而不是直接获取token,而这种模式中,是直接获取token的
这种方式把令牌直接传给前端,是很不安全的。因此,只能用于一些安全要求不高的场景,并且令牌的有效期必须非常短,通常就是会话期间(session)有效,浏览器关掉,令牌就失效了。
1.2.3.1、实现
重定向链接到开放平台上,用户登录成功之后重定向到我们平台并携带来token,后端接收到这个token就可以直接拿到并申请到属于该用户的信息。
第一步,我们网站提供一个链接,要求用户跳转到开放平台网站,授权用户数据给我们网站使用。
携带参数如下:
https://开放平台.com/oauth/authorize?
response_type=token& # 需要响应的内容
client_id=CLIENT_ID& # 标识我们平台的id
redirect_uri=CALLBACK_URL& # 登录成功之后重定向的链接
scope=read # (可选)请求资源范围,多个空格隔开。如果和客户端申请的一致,可忽略
上面 URL 中,response_type
参数为token
,表示要求直接返回令牌。
第二步,用户跳转到开放平台网站,登录后同意给予我们网站授权。这时,开放平台网站就会跳回redirect_uri
参数指定的跳转网址,并且把令牌作为 URL 参数,传给我们网站。至此,我们接收到了能够访问用户信息的token
https://a.com/callback
token=ACCESS_TOKEN # 用于访问用户信息的token
1.2.4、密码模式
如名称所示,密码模式即直接讲该用户的用户名和密码传给我们平台,我们平台使用用户名和密码去请求token令牌,通过该令牌去获取该用户信息。
这种方式需要用户给出自己的用户名/密码,显然风险很大,因此只适用于其他授权方式都无法采用的情况,而且必须是用户高度信任的应用。
第一步,我们平台网站要求用户提供开放平台网站的用户名和密码。拿到以后,我们平台就直接向第三方网站请求令牌。
携带参数如下:
https://oauth.开放平台.com/token?
grant_type=password& # 标识什么模式
username=USERNAME& # 用户用户名
password=PASSWORD& # 用户密码
client_id=CLIENT_ID # 标识我们平台的id
上面 URL 中,grant_type
参数是授权方式,这里的password
表示"密码式",username
和password
是开放平台的用户名和密码。
第二步,开放平台网站验证身份通过后,直接给出令牌。注意,这时不需要跳转,而是把令牌放在 JSON 数据里面,作为 HTTP 回应,我们平台因此拿到令牌。
1.2.5、凭证式
最后一种方式是凭证式(client credentials),适用于没有前端的命令行应用,即在命令行下请求令牌。
简单的说就是直接通过我们平台的id和密钥,去申请用户信息。
https://oauth.开放平台.com/token?
grant_type=client_credentials& # 标识是什么模式
client_id=CLIENT_ID& # 表示我们平台id
client_secret=CLIENT_SECRET # 我们平台的密钥
上面 URL 中,grant_type
参数等于client_credentials
表示采用凭证式,client_id
和client_secret
用来让开放平台确认我们平台的身份。
第二步,开放平台网站验证通过以后,直接返回令牌。
这种方式给出的令牌,是针对第三方应用的,而不是针对用户的,即有可能多个用户共享同一个令牌。
1.2.6、令牌的使用
之上的四种的授权模式中,最后都会获取到一个AccessToken,我们提到使用此AccessToken就可以去开放平台获取到用户信息,那么具体如何使用了?
开放平台有相关的链接,具体可以看官方文档,请求链接在请求体中携带Authorization
字段即可。
curl -H "Authorization: Bearer ACCESS_TOKEN" \
"https://api.开放平台.com"
1.2.7、更新令牌
令牌的有效期到了,如果让用户重新走一遍上面的流程,再申请一个新的令牌,很可能体验不好,而且也没有必要。OAuth 2.0 允许用户自动更新令牌。
具体方法是,开放平台网站颁发令牌的时候,一次性颁发两个令牌,一个用于获取数据,另一个用于获取新的令牌(refresh token 字段)。令牌到期前,用户使用 refresh token 发一个请求,去更新令牌。
这也是我们之前提到的refresh token
https://开放平台.com/oauth/token?
grant_type=refresh_token&
client_id=CLIENT_ID&
client_secret=CLIENT_SECRET&
refresh_token=REFRESH_TOKEN # 我们
评论区