<audio id="audio" title="03 | 授权服务：授权码和访问令牌的颁发流程是怎样的？" controls="" preload="none"><source id="mp3" src="https://static001.geekbang.org/resource/audio/22/db/227a3b35ec78177f64085a1f7f1bc6db.mp3"></audio>

你好，我是王新栋。

在上一讲，我从为什么需要授权码这个问题开始，为你串了一遍授权码许可流程整体的通信过程。在接下来的三讲中，我会着重为你讲解关于授权服务的工作流程、授权过程中的令牌，以及如何接入OAuth 2.0。这样一来，你就可以吃透授权码许可这一最经典、最完备、最常用的授权流程了，以后再处理授权相关的逻辑就更得心应手了。现在呢，让我们开始这一讲。

在介绍授权码许可类型时，我提到了很多次 “授权服务”。一句话概括，授权服务就是**负责颁发访问令牌**的服务。更进一步地讲，OAuth 2.0的核心是授权服务，而授权服务的核心就是令牌。

为什么这么说呢？当第三方软件比如小兔，要想获取小明在京东店铺的订单，就必须先从京东商家开放平台的授权服务那里获取访问令牌，进而通过访问令牌来 **“代表”** 小明去请求小明的订单数据。这不恰恰就是整个OAuth 2.0授权体系的核心吗？

那么，授权服务到底是怎么生成访问令牌的，这其中包含了哪些操作呢？还有一个问题是，访问令牌过期了而用户又不在场的情况下，又如何重新生成访问令牌呢？

带着这两个问题，我们就以授权码许可类型为例，一起深入探索下授权服务这个核心组件吧。

## 授权服务的工作过程

开始之前，你还是要先回想下小明给小兔软件授权订单数据的整个流程。

我们说小兔软件先要让小明去京东商家开放平台那里给它授权数据，那这里是不是你觉得很奇怪？你总不能说，“嘿，京东，你把数据给小兔用吧”，那京东肯定会回复说，“小明，小兔是谁啊，没在咱家备过案，我不能给他，万一是骗子呢？”

对吧，你想想是不是这个逻辑。所以，授权这个大动作的前提，肯定是小兔要去平台那里“备案”，也就是注册。注册完后，京东商家开放平台就会给小兔软件app_id和app_secret等信息，以方便后面授权时的各种身份校验。

同时，注册的时候，第三方软件也会请求受保护资源的可访问范围。比如，小兔能否获取小明店铺3个月以前的订单，能否获取每条订单的所有字段信息等等。这个权限范围，就是scope。后面呢，我还会详细讲述范围控制。

文字说起来有点抽象，咱们还是直接上代码吧。关于注册后的数据存储，我们使用如下Java代码来模拟：

```
Map&lt;String,String&gt; appMap =  new HashMap&lt;String, String&gt;();//模拟第三方软件注册之后的数据库存储

appMap.put(&quot;app_id&quot;,&quot;APPID_RABBIT&quot;);
appMap.put(&quot;app_secret&quot;,&quot;APPSECRET_RABBIT&quot;);
appMap.put(&quot;redirect_uri&quot;,&quot;http://localhost:8080/AppServlet-ch03&quot;);
appMap.put(&quot;scope&quot;,&quot;nickname address pic&quot;);

```

备完案之后，咱们接着继续前进。小明过来让平台把他的订单数据给小兔，平台咔咔一查，对了下暗号，发现小兔是合法的，于是就要推进下一步了。

咱们上节课讲过，在授权码许可类型中，授权服务的工作，可以划分为两大部分，一个是**颁发授权码code**，一个是**颁发访问令牌access_token**。为了更能表达授权码和访问令牌的存在，我在图中用深色将其标注了出来：

<img src="https://static001.geekbang.org/resource/image/a5/11/a5d231c5b356ecf2031yy7d17207c011.png" alt="">

我们先看看颁发授权码code的流程。

### 过程一：颁发授权码code

在这个过程中，授权服务需要完成两部分工作，分别是**准备工作**和**生成授权码code**。

你可能会问了，这个“准备”都包括哪些工作？我们可以想到，小明在给第三方软件小兔打单软件进行授权的时候，会看到授权页面上有一个授权按钮，但是授权服务在小明看到这个授权按钮之前，实际上已经做了一系列动作。

这些动作，就是所谓的准备工作，包括验证基本信息、验证权限范围（第一次）和生成授权请求页面这三步。我们具体分析下。

**第一步，验证基本信息。**

验证基本信息，包括对第三方软件小兔合法性和回调地址合法性的校验。

在 Web 浏览器环境下，颁发code的整个请求过程，都是浏览器通过前端通信来完成，这就意味着所有信息都有被冒充的风险。因此，授权服务必须对第三方软件的存在性做判断。

同样，回调地址也是可以被伪造的。比如，不法分子将其伪装成钓鱼页面，或者是带有恶意攻击性的软件下载页面。因此从安全上考虑，授权服务需要对回调地址做基本的校验。

```
if(!appMap.get(&quot;redirect_uri&quot;).equals(redirectUri)){
    //回调地址不存在
}

```

在授权服务的程序中，这两步验证通过后，就会生成或者响应一个页面（**属于授权服务器上的页面**），以提示小明进行授权。

**第二步，验证权限范围（第一次）。**

既然是授权，就会涉及范围。比如，我们使用微信登录第三方软件的时候，会看到微信提示我们，第三方软件可以获得你的昵称、头像、性别、地理位置等。如果你不想让第三方软件获取你的某个信息，那么可以不选择这一项。同样在小兔中也是一样，当小明为小兔进行授权的时候，也可以选择给小兔的权限范围，比如是否授予小兔获取3个月以前的订单的访问权限。

这就意味着，我们需要对小兔传过来的scope参数，与小兔注册时申请的权限范围做比对。如果请求过来的权限范围大于注册时的范围，就需要作出越权提示。**记住，此刻是第一次权限校验。**

```
String scope = request.getParameter(&quot;scope&quot;);
if(!checkScope(scope)){
    //超出注册的权限范围
}

```

**第三步，生成授权请求页面。**

这个授权请求页面就是授权服务上的页面，如下图所示：

<img src="https://static001.geekbang.org/resource/image/5e/66/5e024b40a98b65a54082106a96734c66.png" alt="">

页面上显示了小兔注册时申请的today、history 两种权限，小明可以选择缩小这个权限范围，比如仅授予获取today信息的权限。

至此，颁发授权码code的准备工作就完成了。你要注意哈，我一直强调说这也是准备工作，因为当用户点击授权按钮“approve”后，才会**生成授权码code值和访问令牌acces_token值**，“一切才真正开始”。

这里需要说明下：在上面的准备过程中，我们忽略了小明登录的过程，但只有用户登录了才可以对第三方软件进行授权，授权服务才能够获得用户信息并最终生成code 和 app_id（第三方软件的应用标识） + user（资源拥有者标识）之间的对应关系。你可以把登录部分的代码，作为附加练习。

小明点击“approve”按钮之后，生成授权码code的流程就正式开始了，主要包括验证权限范围（第二次）、处理授权请求生成授权码code和重定向至第三方软件这三大步。接下来，我们一起分析下这三步。

**第四步，验证权限范围（第二次）。**

在步骤二中，生成授权页面之前授权服务进行的第一次校验，是对比小兔请求过来的权限范围scope和注册时的权限做的比对。这里的第二次验证权限范围，是用小明进行授权之后的权限，再次与小兔软件注册的权限做校验。

那这里为什么又要校验一次呢？因为这相当于一次用户的输入权限。小明选择了一定的权限范围给到授权服务，对于权限的校验我们要重视对待，凡是输入性数据都会涉及到合法性检查。另外，这也是要求我们养成一种**在服务端对输入数据的请求，都尽可能做一次合法性校验的好习惯**。

```
String[] rscope =request.getParameterValues(&quot;rscope&quot;);

if(!checkScope(rscope)){
    //超出注册的权限范围
}

```

**第五步，处理授权请求，生成授权码code。**

当小明同意授权之后，授权服务会校验响应类型response_type的值。response_type有code和token两种类型的值。在这里，我们是用授权码流程来举例的，因此代码要验证response_type的值是否为code。

```
String responseType = request.getParameter(&quot;response_type&quot;);
if(&quot;code&quot;.equals(responseType)){
  
}

```

在授权服务中，需要将生成的授权码code值与app_id、user进行关系映射。也就是说，一个授权码code，表示某一个用户给某一个第三方软件进行授权，比如小明给小兔软件进行的授权。同时，我们需要将code值和这种映射关系保存起来，以便在生成访问令牌access_token时使用。

```
String code = generateCode(appId,&quot;USERTEST&quot;);//模拟登录用户为USERTEST

private String generateCode(String appId,String user) {
  ...
  String code = strb.toString();
  codeMap.put(code,appId+&quot;|&quot;+user+&quot;|&quot;+System.currentTimeMillis());
  return code;
}

```

在生成了授权码code之后，我们也按照上面所述绑定了响应的映射关系。这时，你还记得我之前讲到的授权码是临时的、一次性凭证吗？因此，我们还需要为code设置一个有效期。

OAuth 2.0规范建议授权码code值有效期为10分钟，并且**一个授权码code只能被使用一次**。不过根据经验呢，在生产环境中code的有效期一般不会超过5分钟。关于授权码code相关的安全方面的内容，我还会在第8讲中详细讲述。

同时，授权服务还需要**将生成的授权码code跟已经授权的权限范围rscope进行绑定并存储**，以便后续颁发访问令牌时，我们能够通过code值取出授权范围并与访问令牌绑定。因为第三方软件最终是通过访问令牌来请求受保护资源的。

```
Map&lt;String,String[]&gt; codeScopeMap =  new HashMap&lt;String, String[]&gt;();

codeScopeMap.put(code,rscope);//授权范围与授权码做绑定

```

**第六步，重定向至第三方软件。**

生成授权码code值之后，授权服务需要将该code值告知第三方软件小兔。开始时我们提到，颁发授权码code是通过前端通信完成的，因此这里采用重定向的方式。这一步的重定向，也是我在上一讲中提到的第二次重定向。

```
Map&lt;String, String&gt; params = new HashMap&lt;String, String&gt;();
params.put(&quot;code&quot;,code);

String toAppUrl = URLParamsUtil.appendParams(redirectUri,params);//构造第三方软件的回调地址，并重定向到该地址

response.sendRedirect(toAppUrl);//授权码流程的“第二次”重定向

```

到此，颁发授权码code的流程全部完成。当小兔获取到授权码code值以后，就可以开始请求访问令牌access_token的值了，也就是我们即将开始的过程二。

### 过程二：颁发访问令牌access_token

我们在过程一中介绍了授权码code的生成流程，但小兔最终是要获取到访问令牌access_token，才可以去请求受保护资源。而授权码呢，正如我在上一讲提到的，只是一个换取访问令牌access_token的临时凭证。

当小兔拿着授权码code来请求的时候，授权服务需要为之生成最终的请求访问令牌。这个过程主要包括验证第三方软件小兔是否存在、验证code值是否合法和生成access_token值这三大步。接下来，我们一起分析下每一步。

**第一步，验证第三方软件是否存在。**

此时，接收到的grant_type的类型为authorization_code。

```
String grantType = request.getParameter(&quot;grant_type&quot;);
if(&quot;authorization_code&quot;.equals(grantType)){
  
}

```

由于颁发访问令牌是通过后端通信完成的，所以这里除了要校验app_id外，还要校验app_secret。

```
if(!appMap.get(&quot;app_id&quot;).equals(appId)){
    //app_id不存在
}

if(!appMap.get(&quot;app_secret&quot;).equals(appSecret)){
    //app_secret不合法
}

```

**第二步，验证授权码code值是否合法。**

授权服务在颁发授权码code的阶段已经将code值存储了起来，此时对比从request中接收到的code值和从存储中取出来的code值。在我们给出的课程[相关代码](https://github.com/xindongbook/oauth2-code)中，code值对应的key是app_id和user的组合值。

```
String code = request.getParameter(&quot;code&quot;);
if(!isExistCode(code)){//验证code值
	//code不存在
  return;
}
codeMap.remove(code);//授权码一旦被使用，须立即作废

```

这里我们一定要记住，**确认过授权码code值有效以后，应该立刻从存储中删除当前的code值**，以防止第三方软件恶意使用一个失窃的授权码code值来请求授权服务。

**第三步，生成访问令牌access_token值。**

关于按照什么规则来生成访问令牌access_token的值，OAuth 2.0规范中并没有明确规定，但必须符合三个原则：**唯一性、不连续性、不可猜性**。在我们给出的Demo中，我们是使用UUID来作为示例的。

和授权码code值一样，我们需要将访问令牌access_token值存储起来，并将其与第三方软件的应用标识app_id和资源拥有者标识user进行关系映射。也就是说，**一个访问令牌access_token表示某一个用户给某一个第三方软件进行授权**。

同时，**授权服务还需要将授权范围跟访问令牌access_token做绑定**。最后，还需要为该访问令牌设置一个过期时间expires_in，比如1天。

```
Map&lt;String,String[]&gt; tokenScopeMap =  new HashMap&lt;String, String[]&gt;();

String accessToken = generateAccessToken(appId,&quot;USERTEST&quot;);//生成访问令牌access_token的值
tokenScopeMap.put(accessToken,codeScopeMap.get(code));//授权范围与访问令牌绑定

//生成访问令牌的方法
private String generateAccessToken(String appId,String user){
  
  String accessToken = UUID.randomUUID().toString();
	String expires_in = &quot;1&quot;;//1天时间过期
  tokenMap.put(accessToken,appId+&quot;|&quot;+user+&quot;|&quot;+System.currentTimeMillis()+&quot;|&quot;+expires_in);

  return accessToken;
}

```

正因为OAuth 2.0规范没有约束访问令牌内容的生成规则，所以我们有更高的自由度。我们既可以像Demo中那样生成一个UUID形式的数据存储起来，让授权服务和受保护资源共享该数据；也可以将一些必要的信息通过结构化的处理放入令牌本身。**我们将包含了一些信息的令牌，称为结构化令牌，简称JWT**。在下一讲中，我还会与你详细讲述JWT。

至此，授权码许可类型下授权服务的两大主要过程，也就是颁发授权码和颁发访问令牌的流程，我就与你讲完了。

接下来，你在阅读别人的授权流程代码，或者是使用诸如通过微信登录的第三方软件的时候，就会明白背后的原理了。同时，你在自己搭建一个授权服务流程时，也会更加得心应手。这一切的原因，都在于颁发授权码和颁发访问令牌，就是授权服务的核心。

到这里，你应该还会注意到一个问题，在生成访问令牌的时候，我们还给它附加了一个过期时间expires_in，这意味着访问令牌会在一定的时间后失效。访问令牌失效，就意味着资源拥有者给第三方软件的授权失效了，第三方软件无法继续访问资源拥有者的受保护资源了。

这时，如果你还想继续使用第三方软件，就只能重新点击授权按钮，比如小明给小兔软件授权以后，正在愉快地处理他店铺的订单数据，结果没过多久，突然间小兔软件再次让小明进行授权。此刻，我们可以替小明感受一下他的心情。

显然，这样的用户体验非常糟糕。为此，OAuth 2.0中引入了刷新令牌的概念，也就是刷新访问令牌access_token的值。这就意味着，有了刷新令牌，用户在一定期限内无需重新点击授权按钮，就可以继续使用第三方软件。

接下来，我们就一起看看刷新令牌的工作原理吧。

## 刷新令牌

刷新令牌也是给第三方软件使用的，同样需要遵循**先颁发再使用**的原则。因此，我们还是从颁发和使用两个环节来学习刷新令牌。不过，这个颁发和使用流程和访问令牌有些是相同的，所以我只会和你重点讲述其中的区别。

### 颁发刷新令牌

其实，颁发刷新令牌和颁发访问令牌是一起实现的，都是在过程二的步骤三生成访问令牌access_token中生成的。也就是说，第三方软件得到一个访问令牌的同时，也会得到一个刷新令牌：

```
Map&lt;String,String&gt; refreshTokenMap =  new HashMap&lt;String, String&gt;();

String refreshToken = generateRefreshToken(appId,&quot;USERTEST&quot;);//生成刷新令牌refresh_token的值

private String generateRefreshToken(String appId,String user){

  String refreshToken = UUID.randomUUID().toString();

  refreshTokenMap.put(refreshToken,appId+&quot;|&quot;+user+&quot;|&quot;+System.currentTimeMillis());
  return refreshToken;
  
} 

```

看到这里你可能要问了，为什么要一起生成访问令牌和刷新令牌呢？

其实，这就回到了刷新令牌的作用上了。刷新令牌存在的初衷是，在访问令牌失效的情况下，为了不让用户频繁手动授权，用来通过系统重新请求**生成一个新的访问令牌**。那么，如果访问令牌失效了，而“身边”又没有一个刷新令牌可用，岂不是又要麻烦用户进行手动授权了。所以，它必须得和访问令牌一起生成。

到这里，我们就解决了刷新令牌的颁发问题。

### 使用刷新令牌

说到刷新令牌的使用，我们需要先明白一点。在OAuth 2.0规范中，刷新令牌是一种特殊的授权许可类型，是嵌入在授权码许可类型下的一种特殊许可类型。在授权服务的代码里，当我们接收到这种授权许可请求的时候，会先比较grant_type和 refresh_token的值，然后做下一步处理。

这其中的流程主要包括如下两大步骤。

**第一步，接收刷新令牌请求，验证基本信息。**

此时请求中的grant_type值为refresh_token。

```
String grantType = request.getParameter(&quot;grant_type&quot;);
if(&quot;refresh_token&quot;.equals(grantType)){
  
}

```

和颁发访问令牌前的验证流程一样，这里我们也需要验证第三方软件是否存在。需要注意的是，这里需要同时验证刷新令牌是否存在，目的就是要保证传过来的刷新令牌的合法性。

```
String refresh_token = request.getParameter(&quot;refresh_token&quot;);

if(!refreshTokenMap.containsKey(refresh_token)){
    //该refresh_token值不存在
}

```

另外，我们还需要验证刷新令牌是否属于该第三方软件。授权服务是将颁发的刷新令牌与第三方软件、当时的授权用户绑定在一起的，因此这里需要判断该刷新令牌的归属合法性。

```
String appStr = refreshTokenMap.get(&quot;refresh_token&quot;);
if(!appStr.startsWith(appId+&quot;|&quot;+&quot;USERTEST&quot;)){
    //该refresh_token值不是颁发给该第三方软件的
}

```

**需要注意，一个刷新令牌被使用以后，授权服务需要将其废弃，并重新颁发一个刷新令牌。**

**第二步，重新生成访问令牌。**

生成访问令牌的处理流程，与颁发访问令牌环节的生成流程是一致的。授权服务会将新的访问令牌和新的刷新令牌，一起返回给第三方软件。这里就不再赘述了。

## 总结

今天的课马上又要结束了，我和你讲了授权码许可类型下授权服务的工作原理。授权服务可以说是整个OAuth 2.0体系中的 “灵魂” 组件，任何一种许可类型都离不开它的支持，它也是最复杂的组件。

这是因为它将复杂性尽可能地“揽在了自己身上”，才使得诸如小兔这样的第三方软件接入OAuth 2.0的时候更加便捷。那关于如何快速地接入OAuth 2.0，我在第5讲中和你详细展开。

授权服务的步骤流程比较多，因此我把这节课配套的代码放到了[GitHub](https://github.com/xindongbook/oauth2-code/tree/master/src/com/oauth/ch03)上，可以帮助你更好地理解授权服务的流程。

总结来讲，关于这一讲，我希望你能记住以下3点。

1. 授权服务的核心就是，**先颁发授权码code值，再颁发访问令牌access_token值**。
1. 在颁发访问令牌的**同时还会颁发刷新令牌refresh_token值，这种机制可以在无须用户参与的情况下用于生成新的访问令牌**。正如我们讲到的小明使用小兔软件的例子，当访问令牌过期的时候，刷新令牌的存在可以大大提高小明使用小兔软件的体验。
1. 授权还要有授权范围，**不能让第三方软件获得比注册时权限范围还大的授权，也不能获得超出了用户授权的权限范围，始终确保最小权限安全原则。**比如，小明只为小兔软件授予了获取当天订单的权限，那么小兔软件就不能访问小明店铺里面的历史订单数据。

## 思考题

刷新令牌有过期时间吗，会一直有效吗？和我说说你的想法吧。

欢迎你在留言区分享你的观点，也欢迎你把今天的内容分享给其他朋友，我们一起交流。
