本文转载自oAuth2.0 简介 - flyingww,有部分改动。


本文将对oAuth 2.0协议做一个简单介绍。

本文主要内容翻译自文章:OAuth 2 Simplified

本文分为如下几个部分:

  1. 角色:应用、api和用户
  2. 创建一个app
  3. 授权:获取访问token
  4. 认证
  5. 和oAuth 1.0的区别

角色:应用、api和用户

  • oAuth的客户。oAuth的“客户”是那些尝试访问用户账户信息的应用程序,而这个访问操作需要得到用户的同意才能完成。

  • 资源服务器(Resource Server,也叫做api server)。资源服务器提供了访问用户信息的api接口。

  • 授权服务器(Authorization Server):授权服务器提供了接口来让用户决定同意或者拒绝当前访问请求。很多情况下,Authorization Server和Resource Server是合在一个系统里的,虽然oAuth2.0允许它们独立开,甚至用不同的域名。

  • 用户:用户是他们自身账户信息的拥有者,对本次访问请求有决定权。

创建一个APP

上文中提到oAuth的客户是应用程序。为了使用oAuth服务,应用程序需要先在oAuth服务上注册。注册时需要提供应用名称、网址、logo等信息。如果大家之前有使用过微信开放平台或者支付宝开放平台,对这个流程应该会深有体会。

重定向uri

oAuth服务只会把用户重定向到注册应用时提供的uri,以防止被攻击。所有的重定向uri都需要基于https协议,以防授权过程中token被截取。

对于原生app而言,重定向uri可以被定义成一个uri schema,类似于: demoapp://redirect 这样。

client id & secret

应用程序注册好之后,oAuth服务会为其分配一个client id和一个client secret。client id是公共信息,用来构建登陆uri。client secret属于私密信息,需妥善保存。

授权:获取访问token

oAuth 2.0协议的第一步是获得用户授权。对于基于浏览器的应用或者移动端app,授权过程主要是通过向用户展示授权页面来完成的。

oAuth2.0提供了多种授权类型:

  • Authorization Code。主要适用于web应用和移动端应用。
  • 密码。适用于用户名密码登陆场景。
  • 客户端证书。适用于无需用户参与的场景。
  • 隐式方式。之前作为没有密钥情况下的推荐方式,现在已经被PKCE取代了。

接下来将详细讨论每种授权类型的应用场景。

web应用

web应用可能是我们在oAuth场景下碰到的最多的一种应用。web应用通常使用服务端程序语言编写,同时源代码不公开。这就意味着它能很好地保存注册应用时分配的client secret。

授权

为了获取用户授权,我们为用户构造一个登录连接:

https://authorization-server.com/auth?
response_type=code&
client_id=CLIENT_ID&
redirect_uri=REDIRECT_URI&
scope=photos&
state=1234zyx

url参数说明如下:

  • response_type=code,表示服务器希望接收到授权码AUTHORIZATION_CODE
  • client_id ,注册应用时分配的client id
  • redirect_uri ,授权完成之后将请求重定向至该url,比如https://example-app.com/cb
  • scope ,用来说明希望访问用户账户信息的哪些部分
  • state,由当前应用生成的,用来做验证的一个随机字符串

用户访问上面的登陆链接,会看到类似下面这样的弹出页面:

[转]oAuth2.0 简介

如果用户点击“Allow”,授权服务会将当前访问重定向至redirect_uri,并在重定向url中附带授权码AUTHORIZATION_CODE和state随机字符串。

https://example-app.com/cb?code=AUTHORIZATION_CODE&state=1234zyx

url参数说明如下:

  • code。授权码AUTHORIZATION_CODE。
  • state。和登陆链接中的state字符串相同。

redirect_uri连接对应的处理逻辑,需要先看看授权服务在redirect_uri中附带的state字符串是否和登录连接中发出的state匹配。我们通常可以在session或者cookie中存储这个state字符串,然后在回调处理逻辑中比较是否一致。

获取access token

拿到AUTHORIZATION_CODE之后,我们就可以尝试获取access token了。我们可以通过向授权服务器发起一个post请求,用AUTHORIZATION_CODE换取access token:

POST https://api.authorization-server.com/token?
  grant_type=authorization_code&
  code=AUTHORIZATION_CODE&
  redirect_uri=REDIRECT_URI&
  client_id=CLIENT_ID&
  client_secret=CLIENT_SECRET

url参数说明如下:

  • grant_type=authorization_code,本流程用的授权类型是 authorization_code
  • code=AUTHORIZATION_CODE,持有的AUTHORIZATION_CODE
  • redirect_uri=REDIRECT_URI,重定向uri
  • client_id=CLIENT_ID,注册应用时分配的client id
  • client_secret=CLIENT_SECRET,注册应用时分配的client secret

授权服务器会回复一个access token及其过期时间:

{
  "access_token":"RsT5OjbzRn430zqMLgV3Ia",
  "expires_in":3600
}

或者是一个错误:

{
  "error":"invalid_request"
}

AUTHORIZATION_CODE 的作用在于让 token 不经过用户的浏览器直接传递,保护了 token 的安全。因为 AUTHORIZATION_CODE 只能用一次,且有时间限制,超时会失效,所以即使被截也未必能用。 其次,要获得 token,除了需要 AUTHORIZATION_CODE,还需要 client id/client secret。所以即使 AUTHORIZATION_CODE 被盗,也是无法获得 token 的。

Single-Page应用

Single-Page应用是指那些完全运行在浏览器中的应用。浏览器会下载这类应用的源代码并运行。这类应用没办法保证client secret的安全性,所以这类应用不能使用client secret。对于这类应用,授权流程会使用动态生成的client secret,而不是注册app时分配的。这个流程被称为PKCE扩展。

授权

首先创建一个长度为43到128位的随机字符串,这个字符串被称为code_verifier。比如:

5d2309e5bb73b864f989753887fe52f79ce5270395e25862da6940d5

用上面的随机字符串做SHA256运算,然后再做base64-encode运算,得到一个字符串,这个字符串被称为code_challenge。比如:

MChCW5vD-3h03HMGFZYskOSTir7II_MMTb8a9rJNhnI

然后使用上面生成的字符串来创建登陆链接:

https://authorization-server.com/auth?
response_type=code&
client_id=CLIENT_ID&
redirect_uri=REDIRECT_URI&
scope=photos&
state=1234zyx&
code_challenge=CODE_CHALLENGE&
code_challenge_method=S256

url参数说明如下:

  • response_type=code,表示希望接收到授权代码AUTHORIZATION_CODE
  • client_id,注册应用时分配的client id
  • redirect_uri ,授权完成之后将请求重定向至该url,比如https://example-app.com/cb
  • scope ,说明希望访问用户账户信息的哪些部分
  • state,由当前应用生成的,用来做验证的一个随机字符串
  • code_challenge,前面提到的code_challenge
  • code_challenge_method=S256,hash运算方法,这里使用的是sha256

用户访问上面的登陆链接,会看到类似下面这样的弹出页面:
file

如果用户点击“Allow”,授权服务会将当前访问重定向回redirect_uri,并在重定向uri中附带授权码和state随机字符串。

https://example-app.com/cb?code=AUTHORIZATION_CODE&state=1234zyx

url参数说明如下:

  • code。授权代码AUTHORIZATION_CODE。
  • state。和登陆链接中的state字符串相同。

redirect_uri连接对应的处理逻辑,需要先看看授权服务在redirect_uri中附带的state字符串是否和登录连接中发出的state匹配。我们通常可以在session或者cookie中存储这个state字符串,然后在回调处理逻辑中比较是否一致。

获取access token

拿到AUTHORIZATION_CODE之后,我们就可以尝试获取access token了。和web应用通过client secret获取access token方式不同,本场景使用上面生成的code_verifier来获取access token。我们构造如下的post请求来获取access token。

POST https://api.authorization-server.com/token?
  grant_type=authorization_code&
  code=AUTH_CODE_HERE&
  redirect_uri=REDIRECT_URI&
  client_id=CLIENT_ID&
  code_verifier=CODE_VERIFIER

url参数说明如下:

  • grant_type=authorization_code,本流程用的授权类型是 authorization_code
  • code=AUTH_CODE_HERE,3.2.1节中获取到的AUTHORIZATION_CODE
  • redirect_uri=REDIRECT_URI,重定向uri
  • client_id=CLIENT_ID,注册应用时分配的client id
  • code_verifier=CODE_VERIFIER,上文中生成的code_verifier随机字符串

授权服务器会使用请求参数中的code_verifier,根据上面提到的code_challenge的生成流程来重新生成code_challenge。通过和登录请求中传递的code_challenge进行比较,就能确认当前请求的有效性。这个流程保证了,即使AUTHORIZATION_CODE可能会被恶意盗取,但盗取者没办法成功获得access token,因为他们没有client secret。

移动端APP

就像基于浏览器的应用一样,移动端app也没有办法保证client secret的安全性。因此,移动端app也是使用基于PKCE的方式,无需client secret。

授权

对于移动端app,我们构建的登陆按钮会将用户重定向到手机上授权服务的原生app,或者授权服务提供的web页面。这个流程就好比当使用微信登陆或者支付宝做授权登陆时,唤起微信或者支付宝原生app。

使用授权服务的原生app

如果用户的手机上安装了facebook,我们可以将他重定向到下面的uri:

fbauth2://authorize?response_type=code&client_id=CLIENT_ID
  &redirect_uri=REDIRECT_URI&scope=email&state=1234zyx

url参数说明如下:

  • response_type=code,表示服务器希望接收到AUTHORIZATION_CODE
  • client_id=CLIENT_ID,注册应用时分配的client id
  • redirect_uri=REDIRECT_URI ,重定向uri,比如: fb00000000://authorize
  • scope=email,说明希望访问用户账户信息的哪些部分
  • state=1234zyx,由当前应用生成的,用来做验证的一个随机字符串

对于要支持PKCE扩展的服务器来说,我们应该首先创建一个随机字符串作为code_verifier并存在本地,然后在上面的url中添加如下参数:

  • code_challenge,前面提到的code_challenge
  • code_challenge_method=S256,hash运算方法,这里使用的是sha256

使用web浏览器

如果授权服务本身不提供原生app。我们可以使用手机浏览器来访问授权uri。注意要使用手机浏览器而不是app上面的webview,以防止让用户进入钓鱼网站。

我们要么使用手机端浏览器,要么使用ios的SafariViewController,这个特性是ios 9才提供的。SafariViewController可以让用户看到地址栏的url地址,让他们知道他们正在访问的正确url地址。同时,这个特性也会和ios的safari浏览器共享cookie,并能防止app偷窥或者修改浏览器的内容,所以总体上是安全的。

然后我们就可以将用户重定向到下面的url来获取AUTHORIZATION_CODE了(如果授权服务支持PKCE,需要加上上面讨论过的code_challenge和code_challenge_method参数):

https://facebook.com/dialog/oauth?response_type=code&client_id=CLIENT_ID
  &redirect_uri=REDIRECT_URI&scope=email&state=1234zyx

用户会看到下面的弹出页面:
file

获取access token

用户点击“Okay”后,会通过下面的url重定向到你的应用:

fb00000000://authorize?code=AUTHORIZATION_CODE&state=1234zyx

你的移动端应用需要先验证下state字段的正确性,然后就可以用AUTHORIZATION_CODE去获取access token了。

access token的获取过程和"3.1 web应用"中讨论的流程类似,只是请求中不会包含client_secret。如果授权服务支持PKCE,需要加上上面讨论过的code_challenge和code_challenge_method参数。

POST https://api.authorization-server.com/token
  grant_type=authorization_code&
  code=AUTH_CODE_HERE&
  redirect_uri=REDIRECT_URI&
  client_id=CLIENT_ID&
  code_verifier=VERIFIER_STRING

url参数说明如下:

  • grant_type=authorization_code,本流程用的授权类型是 authorization_code
  • code=AUTH_CODE_HERE,上一个步骤中获取到的AUTHORIZATION_CODE
  • redirect_uri=REDIRECT_URI,重定向uri
  • client_id=CLIENT_ID,注册应用时分配的client id
  • code_verifier=VERIFIER_STRING,创建code_challenge使用的随机字符串

授权服务器会验证上面的请求并返回acces token。

如果授权服务器支持PKCE,则会使用 code_verifier来重新生成code_challenge并和登录请求中的code_challenge进行对比,以达到无需使用client secret也能完成授权的过程。

其他授权类型

密码

oAuth2 也提供密码授权类型,通过用户名和密码来获取access token。因为这个过程涉及到让app程序获取用户密码,这种模式只能被用在这个app是由授权服务提供的情况下。比如,twitter app可以用这种方式来登陆移动端或者pc应用。

为了使用密码授权方式,通过如下的post请求即可:

POST https://api.authorization-server.com/token
  grant_type=password&
  username=USERNAME&
  password=PASSWORD&
  client_id=CLIENT_ID

url参数说明如下:

  • grant_type=password,本流程使用password授权类型
  • username=USERNAME,用户名
  • password=PASSWORD,密码
  • client_id=CLIENT_ID,首次注册应用时分配的client id

授权服务会返回和其他授权类型一样的access token。

应用访问

在某些情况下,应用可能需要oAuth授权服务,这些访问不是针对某个用户的,而是应用自身的。比如,当应用要更新url或者icon,或者获取所有用户的统计信息,这类请求其实和单个用户无关。oAuth提供了client_credentials这种授权类型来处理这种情况。

为了使用client_credentials授权类型,通过如下post请求即可:

POST https://api.authorization-server.com/token
    grant_type=client_credentials&
    client_id=CLIENT_ID&
    client_secret=CLIENT_SECRET

授权服务返回的结果中就会包含access token。

创建已授权请求

所有授权类型的结果就是获得了一个access token。

既然已经拿到了access token,我们就能轻松使用curl工具来构建一个授权过的api 请求了:

curl -H "Authorization: Bearer RsT5OjbzRn430zqMLgV3Ia" \ https://api.authorization-server.com/1/me

记住,一定要使用https协议,只有https才能防止请求被劫持或者被修改。

和oAuth1.0的区别

oAuth1.0主要是基于已经存在的协议,比如Flickr的 "FlickrAuth"和Google的 "AuthSub"而设计的。oAuth2.0是一个完全重新设计的协议,和oAuth1.0不兼容。

认证和签名

oAuth1.0协议失败的主因,可以归结于其对加密的要求。对于习惯了使用用户名密码的程序员来说,要使用oAuth1.0意味着需要安装和配置一些加密方面的库来使用这个协议。在传输过程中,用户名和密码等私密信息被加密后传输。

对于oAuth2.0来说,我们使用access token来代替用户名和密码,避免使用加密算法。

用户体验

oAuth2.0主要分为两步:

  1. 获取用户授权,授权的结果就是当前应用得到了access token。
  2. 使用access token来访问资源。

oAuth1.0将不同类型的应用的授权流程合并成一个流程,增加了流程的复杂性,而且用户体验非常不好。

oAuth2.0则为不同类型的应用提供不同的授权类型,增加了可支持的应用类型的同时(对于新的应用类型,可以新增授权类型),大大提升了用户体验。

oAuth2.0具有更灵活的token撤回机制

oAuth 1.0的access token通常具有很长的有效期,而且这个有效期可能是无限长。负责任的api接口提供方,通常可以让用户看到他们曾经授权过哪些应用来获取他们的账户信息,同时可以轻易撤回这些授权。

oAuth2.0通过分配一个短生命周期的acces token和一个长生命周期的refresh token来解决这个问题,避免某个access token长时间有效。

角色分离

oAuth2.0允许API Server和Authorization Server分离。

  • Authorization Server可以只是负责获得用户的授权并向应用分配access token。授权服务器需要知道应用的client_id和client_secret。
  • API Server只需要能够识别access token即可。
    API Server和Authorization Server可以独立部署,独立扩容。比如Google’s OAuth 2.0 使用 “accounts.google.com” 作为Authorization Server,使用 “www.gooogleapis.com” 作为API Server。

参考:

https://www.oauth.com/oauth2-servers/differences-between-oauth-1-2/

https://aaronparecki.com/oauth-2-simplified/

https://www.zhihu.com/question/275041157/answer/2584364537