The Red Heart

今天在网上闲逛的时候看到了这个漂亮的心形曲线特效,动感十足。于是动手分析了一下它的实现方式,记于此文。

See the Pen The Red Heart by Ninja Lau (@mutoo) on CodePen.

记得 Make things with math 的作者 Steven Wittens 曾经说过:任何看起来复杂的函数图形,都是由各种简单函数组成的。这个心形曲线特效也不例外。

0x01 Heart Curve

Wolfram MathWorld 的 Heart Curve 页面记载了几种心形曲线的公式:

Heart Curve

其中第六个参数方程非常符合我们的案例。代码中的 heartPosition 函数正是出于此:

var heartPosition = function (rad) {
    return [16 * Math.pow(Math.sin(rad), 3), -(15 * Math.cos(rad) - 5 * Math.cos(2 * rad) - 2 * Math.cos(3 * rad) - Math.cos(4 * rad))];
};

该函数传入一个由原点发出的射线与x轴的夹角(弧度),返回一组坐标 [x, y] 。由于 canvas 中坐标系的 Y 轴是由上而下的,所以公式中的 Y 坐标需要取反。

0x02 Dotted Heart Curve

通过该函数绕原点一周,我们能得到一个单位长度的心形。从 0 rad 开始,并以设定好的角度 dr 步进,将心形曲线上的一些采样点保存下来:

var pointsOrigin = [];
var dr = 0.1, i;
for (i = 0; i < Math.PI * 2; i += dr)
    pointsOrigin.push(heartPosition(i));

为了让心形更有层次感,我们可以参加一个简单的缩放函数,并创建多个心形:

var scaleAndTranslate = function (pos, sx, sy) {
    return [pos[0] * sx, pos[1] * sy];
};

for (i = 0; i < Math.PI * 2; i += dr)
    pointsOrigin.push(scaleAndTranslate(heartPosition(i), 2, 2));

for (i = 0; i < Math.PI * 2; i += dr)
    pointsOrigin.push(scaleAndTranslate(heartPosition(i), 3, 3));

0x03 Particles

现在我们有了三个不同大小的心形曲线上的采样点了。为了让这些点变得生动一点,接下来需要制造一些粒子在其上面运动。这些粒子遵循下面的运动规则:

  • 从采样点中随机选取一些点为作目标点;
  • 使用简单的物理规则向目标点移动,例如:速度、摩擦力等;
  • 当粒子接近目标点后,会根据概率重新选择目标点。其中有比较大的概念选择临近的采样点,这样可以让心形曲线保持完整的流动性。

0x04 Particle Tracking

粒子运动形单影支,如果能加上轨迹效果就更好了。可以在粒子初始化时在相同位置创建若干个轨迹粒子。轨迹粒子的运动可以采用缓动公式,逐渐向前一个轨迹粒子靠拢即可。

关于粒子运动的算法,这里不细说。想深入研究的话,强烈推荐 Foundation ActionScript 3.0 Animation: Making Things Move 一书,里面的例子简单易懂。

0x05 Heart Beating

既然是个心,那么如何让它跳动呢。首先我们需要一个函数,能对采样点进行缩放:

var targetPoints = [];
var pulse = function (kx, ky) {
    for (i = 0; i < pointsOrigin.length; i++) {
        targetPoints[i] = [];
        targetPoints[i][0] = kx * pointsOrigin[i][0];
        targetPoints[i][1] = ky * pointsOrigin[i][1];
    }
};

然后周期性地改变缩放系数:

var time = 0;
var loop = function () {
    var k = 0.5 + 0.5 * Math.cos(time);
    pulse(k, k);
    time += ((Math.sin(time)) < 0 ? 9 : (k > 0.8) ? .2 : 1) * 0.04;

    /* render particles */

    window.requestAnimationFrame(loop, canvas);
};

有趣的是,这里 time 的步进并不是均速的,而是一组分段函数,为了是模拟心跳的那种节奏感。这里用了很多魔数(magic numbers),显然都是慢慢微调出来的。

0x06 Color

红心的色彩是通过 HSLA 色彩空间随机选取不同饱和度明度的 30% 透明红色,以此来表现出丰富的色泽,虽然只有红色,但是能够给人很炫丽的感觉。

网络游戏 与 GMT / UTC

在游戏开发的过程中,免不了跟「日期/时间」打交道,尤其是网络游戏。策划通过配置活动的起止时间来实现活动的开放与关闭。

在全球化的今天,作为东八区的开发者,我们做的游戏并不是只有我们自己在玩——来自世界各地的玩家都可以参与我们的游戏。所以在日期处理这件事上,就需要多加考虑,不然在许多地方会捅篓子,或者被玩家有机可趁。

举 (chun) 个 (shu) 例 (xu) 子 (gou)

东八区的运营小组设定新服务器在 4月10日 10:00:00 开服。然后程序员写下 var start = new Date("2015-04-10 10:00:00") 并期望得到正确的结果——没错,本地开发环境的终端返回的正确的信息:

Fri Apr 10 2015 10:00:00 GMT+0800 (CST)

于是程序员将写好的程序部署到了测试服上,不幸的事发生了。服务器到了东八区的 12:00 才开服。玩家白等了两个小时,非常生气,纷纷差评!

后来查明原因是因为程序员为了跟异国恋的女友同步时间,故意把时区调到墨尔本才导致了这起事故。


于是问题就来了——时区是如何影响时间的呢?我们需要先理念两个概念:UTC 和 GMT 时间

GMT 时间把地球分成 24 个时区,每个地区都使用自己的 GMT 时间作息。当运营小组设定开服日期的时候,显然是以自己所在的东八区的 GMT 时间作为标准。

但是如果程序在运行着不同时区的电脑上,虽然当地时间表示都一样,但是实际上却是不同的 UTC 时间。将本地时区修改到 GMT +10 就可以重现故事里的 BUG ,可以看到两个日期的 UTC 时间戳是不同的:

// change system time zone to GMT +8

> new Date("2015-04-10 10:00:00").valueOf()
1428631200000

// change system time zone to GMT +10

> new Date("2015-04-10 10:00:00").valueOf()
1428624000000

另一个例子

说完服务端,来说说客户端。

运营组说了,开服活动在 7 个自然日后结束——也就是 4月17日 00:00 结算活动。前端需要有个倒计时展示。

反例 0x01

前端程序员:服务器列表里已经给了开服时间的时间戳:1428631200000 ——好家伙!拿这个直接算出当天零点,然后加上七天就好了吧!

// Fri Apr 10 2015 10:00:00 GMT+0800 (CST) 
var start = 1428631200000; 

var aDayOfMS = 24 * 60 * 60 * 1000;
start = Math.floor(start / aDayOfMS) * aDayOfMS;

var end = start + aDayOfMS * 7;

直接拿 UTC 时间戳舍去天数后面的时间,直觉上好像没什么问题。但是被舍去的小时数实际上只有 02:00 而不是 10:00 。由于 UTC 时间戳是以 GMT +0 为基准,所以上面这段代码只有当服务端在 GMT +0 时区的时候是正确的。

// Fri Apr 10 2015 10:00:00 GMT+0800 (CST) 
> var start = 1428631200000; 
> start % aDayOfMS
7200000

> 7200000 / ( 60 * 60 * 1000)
2

反例 0x02

前端程序员:OMG,怎么回事!那我换个方法:

// Fri Apr 10 2015 10:00:00 GMT+0800 (CST)
var startDate = new Date(1428631200000);

startDate.setHours(0);
startDate.setMinutes(0);
startDate.setSeconds(0);
startDate.setMilliseconds(0);

var aDayOfMS = 24 * 60 * 60 * 1000;
var end = aDayOfMS * 7 + startDate.valueOf();

这下没问题了么?NO. 对于 10:00 GMT+8 这段代码只有在 GMT -2 到 GMT +12 这些时区的时候才是正确的。如果不在这个范围,例如到了 GMT -4 这段程序算出的 startDate 会跑到 4月9日 00:00 去。

> startDate;
Thu Apr 09 2015 00:00:00 GMT-0400 (EDT)

正解 0x03

那么要如何处理才能得到正确的东八区的自然日的 4月10日 00:00 的 UTC 时间呢?

如果开服时间是固定的 10:00 那很好办,直接在 UTC 上减去 10 小时即可。但如果开服时间是个变量,那最好的实现是服务端直接给出自然日的时间戳(通过 0x02 的代码)或者至少给出服务器的时区,前端可以通过以下代码计算出正确的开服自然日时间:

var zoneOffset = ServerTimeZone - new Date().getTimezoneOffset(); // in minutes
var dateOnServer = new Date(ServerOpenTimestamp - zoneOffset * 60 * 1000);
var ServerOpenDate = ServerOpenTimestamp - dateOnServer.getHours() * 60 * 60 * 1000;

这样在不同的时区,都能得到正确的时间了。同时也防止了那些故意修改时区的玩家绕过前端的限制。

        server open      activity end
    E
    |
+10 +   04-10 12:00   -   04-12 02:00
    |
 +8 +   04-10 10:00   -   04-17 00:00
    |
    |
  0 +   04-10 02:00   -   04-16 16:00
    |
 -5 +   04-09 21:00   -   04-16 11:00
    |
    W

不同概率模型的抽奖体验

游戏中大量使用概率模型来决定事件,而不同的概率模型会带来不同的感受。以下列举了一些常见的概率模型在抽奖中的应用以及产生的体验。

普通概率模型

策划希望平均每 10 次抽奖能中一次,则每次抽奖中奖率为 10%

使用普通概率模型可以用以下程序模拟:

function roll(probability) {
    return Math.random() <= probability;
}

roll(0.1);

经过模拟统计,可以得到如下数据:

抽奖次数 10000
设定中奖率 10%
中奖次数约 10000 * 10% = 1000
两次中奖的平均间隔 10
标准差 9.62
实际期望 10%

普通概率模型是最常用的一种模型,但是在游戏运营过程中的确发现很多小白玩家不能正确理解——他们认为中奖率 10% 的设定等同于抽 10 次肯定会中一次。这显然是错误的,普通概率模型的中奖抽奖次数是基于正态分布的,而且每次抽奖的事件是独立的,并不会因为你前面抽了 9 次没中奖,第十次就一定能中奖。

虽然在大量的统计中,两次中奖的平均间隔是 10 次,但是还有一个有趣的数据是连续 10 次都没中奖的概率约为 (1-10%)^10 ~= 34.8% 可不小呢。

此外「标准差」是一个很有意思的数据,经过模拟统计,10% 中奖率得到的标准差为 9.62 ——也就是说绝大分部人经过 10 ± 9.62 次抽奖即能中奖,运气再背抽 20 次也差不多能得到奖励了。

这种概率模型能非常准确地实现策划的需求,但是会惹来一些小白玩家的差评——为什么你说中奖率是 10% 但是我抽了 20 次还没有中奖!然后给你打个一星。所以很多游戏运营商为了顾及玩家的体验,会对普通概率模型进行修订,增设一些保底抽奖次数,例如每第 10 次固定中奖(10,20,30...)

对于这种做法,我暂不于评价。但是让我们看看如果硬生生地加入固定中奖的设定,会给数值带来什么变化吧。

固定中奖模型

每次抽奖中奖率依旧为 10% ,但每第十次抽奖必中。

程序中需要增加计数器,来标记玩家的累计抽奖次数:

var count = 0;

function roll(probability, threshold) {
    count++;
    if (count % threshold == 0)
        return true;
    else
        return Math.random() <= probability;
}

roll(0.1, 10);

经过模拟统计,可以得到以下数据:

抽奖次数 10000
中奖次数约 10000 / 10 + (10000 - 10000 / 10) * 10% = 1900
两次中奖的平均间隔约 5.26
标准差 3.26
实际期望 19%

这时候玩家得到的抽奖体验是:10 次抽奖肯定能中奖,而且不止中一次,爽暴了是不是。实际期望高达 19% 这远远超出策划 10% 的预期。所以策划琢磨着不能便宜了玩家,只能把中奖率调低。但是这会导致中奖集中在每 10 次附近,抽奖的乐趣几近丧失。

这样看来,固定中奖模型是否真的无药可救?其实还是有可以优化的地方。

计数器模型

每次抽奖中奖率依旧为 10% ,若连续 9 次未中奖,下一次抽奖必中奖。

这个需求看起来和上面好像没什么不同,但是保底的条件不再是每第 10 次,而是发生在每连续 9 次未中奖后。也就是说计数器会在每次中奖后清 0 重计。

var count = 0;

function roll(probability, threshold) {
    var result = false;

    count++;
    if (count % threshold == 0)
        result = true;
    else
        result = Math.random() <= probability;

    if (result)
        count = 0;

    return result;
}

roll(0.1, 10);

经过模拟统计,可以得到以下数据:

抽奖次数 10000
中奖次数约 1533(使用程序统计)
两次中奖的平均间隔约 6.51
标准差约 3.39
实际期望约 15.3%

不同于前两个模型的中奖次数可以很直观地列出公式来,这里只能用「蒙特卡罗方法(Monte Carlo method)」写程序模拟统计出实验数据;

玩家的体验依然是 10 次抽奖肯定能中奖,而且不止中一次。但是我们在没有改变概率的情况下将期望降了下来。此外玩家不会有每第 10 次抽奖一定中奖的刻板印象。缺点是这个模型很难一次到位地调出想要的期望概率,需要不停的尝试修改参数并模拟。

但这样真的好玩吗?反正我不觉得。

其它

其实还有很多有意思的抽奖模型可以琢磨,例如「随机步长累加模型」也是一种保底中奖模型,只不过去掉了独立随机事件,并把计数增长改为随机量,最终在累计超过阈值时得奖。

这种模型如果有个较大的阈值和较小的步长下限,还可以起到让玩家在头几次抽奖必然不中(大)奖的效果。另外在这种模型下,计数器甚至可以对玩家可见,让看玩家看到进度和目标,感受到奖励是可达的、近在眼前的。

除了抽奖,这种模型也经常出现在一些游戏机制里,例如装备锻造(亡灵杀手:夏侯惇)、踩地雷(宠物小精灵)等。

通用等级经验表配置

最近翻资料的时候看到一篇很早以前写的内部分享文章,关于等级-经验表的配置设计。对游戏开发来说,这项配置简单到不能再简单了,但是如果处理不好会增加许多沟通成本,甚至闹 Bug —— 没人喜欢 Bug. 所以我打算拿出来分享一下。

在游戏开发中,等级与经验表是一个非常重要的配置。在我参与的第一个项目的开发实践中因为没有确定一个比较好的规范,导致了一些分歧,所以本文将对等级经验表配置的合理性进行探索。

一些需要考虑的事情

  • 配置表中哪些参数是必须的?
  • 等级由 0 级开始还是由 1 级开始?
  • 升级所需经验是由程序取出两行经验相减,还是生成配置表的时候计算好?
  • 服务端如何存储等级和经验?以及如何处理经验上限?
  • 界面上如何表现经验?

这些问题如果没有一个明确的答案,开发过程中就会出现策划每新增一个功能,做出来的表都跟之前的不一样。然后接下来就是程序的恶梦了,沟通一不留神,就埋个 Bug.

此外需求总是多变的,例如等级由 0 还是 1 开始,在不同的功能模块中,可能就是个变量。还有界面的表现,有时希望进度条每级清零,有时又希望显示当前经验占总经验的百分比。

那么有没有省心一点的标准答案呢?其实只要给经验表增加一些冗余,就会发现其实问题也没有那么复杂。

例表 Level - Exp Table

lv0    lv1    start    end    interval
0      1      0        99     100
1      2      100      299    200
2      3      300      599    300
...
98     99     7000     9999   3000
99     100    10000    14999  5000

细看这个表,可以发现有许多字段是多余的:只须 end 或 interval 列即可推算出其它所有列。如果策划只提供 end 或 interval 中的一列,那么程序最好能自行生成其它四列。所幸经验表只是个小表,一点冗余并不会有多少负担,但是带来的收益是非常大的。它们的存在能够非常快地简化程序。该表在设计的时候做了如下考虑:

  • DO - 等级 0 与等级 1 同时填写,需要用哪种形式就取哪个,可以直观看出不同形式的上限等级(供强迫症策划参考);
  • DO - 预先计算每一等级的「起始经验」、「结束经验」和 「经验区间」,方便程序直接查询,必避在用的时候进行跨行计算,否则要多很多逻辑去小心处理边界问题;
  • DON'T - 千万不要用每一等级的「起始经验」作为关键数据(去推算其它列),否则你会在如何确定等级上限跟策划争论不休,尤其是当你没有满足上一条的时候。

扩展

这个配置表实际上可以用于描述任何跟经验有关的配置,不只是人物等级也可以是 VIP 等级-充值 之类的关系,附带属性只需增加相应的字段来存放额外的信息就行了,而程序检索起来也非常方便。

List: sample data

var exp_table = [{
    level0: 0,
    level1: 1,
    start_exp: 0,
    end_exp: 99,
    interval_exp: 100,
    extra_data: "a"
}, {
    level0: 1,
    level1: 2,
    start_exp: 100,
    end_exp: 299,
    interval_exp: 200,
    extra_data: "b"
}, {
    level0: 2,
    level1: 3,
    start_exp: 300,
    end_exp: 599,
    interval_exp: 300,
    extra_data: "c"
}];

代码

核心程序只有这三个,将错误的可能性降至最小:

List: core functions

function getConfigFromExp(config_table, total_exp) {
    var row;
    for (row = 0, len = config_table.length; row < len; row++) {
        if (config_table[row].end_exp >= total_exp) {
            return config_table[row];
            break;
        }
    }
    return config_table[row - 1]; // top level
}

function getConfigFromLevel0(config_table, level0) {
    return config_table[level0];
}

function getConfigFromLevel1(config_table, level1) {
    return config_table[level1 - 1];
}

使用方法:

List: usage

> getConfigFromExp(exp_table, 0).level0
0

> getConfigFromExp(exp_table, 0).level1
1

> getConfigFromExp(exp_table, 0).extra_data
"a"

> getConfigFromExp(exp_table, 999).level0
2

> getConfigFromExp(exp_table, 999).level1
3

> getConfigFromExp(exp_table, 999).end_exp
599

> getConfigFromLevel0(exp_table, 0)
Object {level0: 0, level1: 1, start_exp: 0, end_exp: 99, interval_exp: 100…}

> getConfigFromLevel1(exp_table, 1)
Object {level0: 0, level1: 1, start_exp: 0, end_exp: 99, interval_exp: 100…}

关于数据存储

除此之外,配置变更是游戏运营过程中的家常便饭。所以应该如何存储玩家的等级与经验,也是一个需要考虑的因素。

  1. 存储玩家的总经验,在游戏中换算成等级;
  2. 存储玩家的等级,以及当前等级获得的经验;

对于方案一,当经验配置变更时,玩家看到的等级也会随着变化。这可能不是一个很好的情况; 所以方案二可能是比较好的选择,具体情况视需求而定。

方案一相关公式

已知当前总经验 totalExp ;

取当前等级相关配置:

var config = getConfigFromExp(exp_table, totalExp);

计算升级进度:

var percent = (totalExp - config.start_exp) / config.interval * 100;

判断能否升级(假定等级从 1 开始算):

var hasNextLevel = !!getConfigFromLevel1(exp_table, config.level1 + 1);
var canLevelUp = hasNextLevel && exp >= config.end_exp; 

方案二相关公式

已知当前等级 level ; 已知当前等级获得经验 exp ;

取当前等级相关配置(假定等级从 1 开始算):

var config = getConfigFromLevel1(exp_table, level);

计算当前总经验:

var totalExp = config.start_exp + exp;

计算升级进度:

var percent = exp / config.interval * 100;

判断能否升级:

var hasNextLevel = !!getConfigFromLevel1(exp_table, level + 1);
var canLevelUp = hasNextLevel && exp >= config.interval; 

妈妈再也不用担心我和策划撕 B 了~

OAuth 2.0 学习笔记

参考资料

阮一峰 理解OAuth 2.0
OAuth 2.0 官方文档 RFC6749
中文非常好的同学可以看此中文翻译 (LOL)

授权方式

客户端(Client)征求用户(Resource Owner/User)的授权获得访问凭证(Access Token)的过程。

     +--------+                               +---------------+
     |        |--(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 ---|               |
     +--------+                               +---------------+

                     Figure 1: Abstract Protocol Flow

授权(Authorization Grant)方式有如下四种类型:

Authorization Code

     +----------+
     | Resource |
     |   Owner  |
     |          |
     +----------+
          ^
          |
         (B)
     +----|-----+          Client Identifier      +---------------+
     |         -+----(A)-- & Redirection URI ---->|               |
     |  User-   |                                 | Authorization |
     |  Agent  -+----(B)-- User authenticates --->|     Server    |
     |          |                                 |               |
     |         -+----(C)-- Authorization Code ---<|               |
     +-|----|---+                                 +---------------+
       |    |                                         ^      v
      (A)  (C)                                        |      |
       |    |                                         |      |
       ^    v                                         |      |
     +---------+                                      |      |
     |         |>---(D)-- Authorization Code ---------'      |
     |  Client |          & Redirection URI                  |
     |         |                                             |
     |         |<---(E)----- Access Token -------------------'
     +---------+       (w/ Optional Refresh Token)

   Note: The lines illustrating steps (A), (B), and (C) are broken into
   two parts as they pass through the user-agent.

                     Figure 3: Authorization Code Flow

User 与 client 之前存在无法完全信任的用户代理(User-Agent)时,使用这种方式避免直接将 access token 转交给 user-agent,而是增加一个 authorization code 步骤,让 Client 直接向认证服务器(Authorization Server)索要 access token 。

Implicit

     +----------+
     | Resource |
     |  Owner   |
     |          |
     +----------+
          ^
          |
         (B)
     +----|-----+          Client Identifier     +---------------+
     |         -+----(A)-- & Redirection URI --->|               |
     |  User-   |                                | Authorization |
     |  Agent  -|----(B)-- User authenticates -->|     Server    |
     |          |                                |               |
     |          |<---(C)--- Redirection URI ----<|               |
     |          |          with Access Token     +---------------+
     |          |            in Fragment
     |          |                                +---------------+
     |          |----(D)--- Redirection URI ---->|   Web-Hosted  |
     |          |          without Fragment      |     Client    |
     |          |                                |    Resource   |
     |     (F)  |<---(E)------- Script ---------<|               |
     |          |                                +---------------+
     +-|--------+
       |    |
      (A)  (G) Access Token
       |    |
       ^    v
     +---------+
     |         |
     |  Client |
     |         |
     +---------+

   Note: The lines illustrating steps (A) and (B) are broken into two
   parts as they pass through the user-agent.

                       Figure 4: Implicit Grant Flow

上一种方式的减化版本,因为省去了 authorization code 这步,所以速度更快且更方便,但安全性较低。通常在 user-agent 即 client 的环境下使用,例如网页小程序或者浏览器插件。在这样的环境中,通常 token 的期限不会给的太长。

Resource Owner Password redentials

     +----------+
     | Resource |
     |  Owner   |
     |          |
     +----------+
          v
          |    Resource Owner
         (A) Password Credentials
          |
          v
     +---------+                                  +---------------+
     |         |>--(B)---- Resource Owner ------->|               |
     |         |         Password Credentials     | Authorization |
     | Client  |                                  |     Server    |
     |         |<--(C)---- Access Token ---------<|               |
     |         |    (w/ Optional Refresh Token)   |               |
     +---------+                                  +---------------+

            Figure 5: Resource Owner Password Credentials Flow

Client 使用用户的密码作为凭证,直接向 authorization server 获取 access token 以及 refresh token,通常只要在最开始时使用一次,所以理论上 client 不得保存用户的密码。一般在用户非常信任的环境中使用,例如用户自己的手机操作系统。

Client Credentials


     +---------+                                  +---------------+
     |         |                                  |               |
     |         |>--(A)- Client Authentication --->| Authorization |
     | Client  |                                  |     Server    |
     |         |<--(B)---- Access Token ---------<|               |
     |         |                                  |               |
     +---------+                                  +---------------+

                     Figure 6: Client Credentials Flow

客户端向 authorization server 获取自己已有权限的子集,可以将获得到的子集 access token 交给它的子客户端使用。

基本概念

Access Token

由认证服务器和资源服务器共同实施的一种凭证。由于该 token 是在用户的授权下产生的,所以资源服务器可以为出示此 token 的客户提供相应的资源。

Refresh Token

当 access token 过期或无效后,client 使用 refresh token 重新向认证服务器申请新的 access token 。

  +--------+                                           +---------------+
  |        |--(A)------- Authorization Grant --------->|               |
  |        |                                           |               |
  |        |<-(B)----------- Access Token -------------|               |
  |        |               & Refresh Token             |               |
  |        |                                           |               |
  |        |                            +----------+   |               |
  |        |--(C)---- Access Token ---->|          |   |               |
  |        |                            |          |   |               |
  |        |<-(D)- Protected Resource --| Resource |   | Authorization |
  | Client |                            |  Server  |   |     Server    |
  |        |--(E)---- Access Token ---->|          |   |               |
  |        |                            |          |   |               |
  |        |<-(F)- Invalid Token Error -|          |   |               |
  |        |                            +----------+   |               |
  |        |                                           |               |
  |        |--(G)----------- Refresh Token ----------->|               |
  |        |                                           |               |
  |        |<-(H)----------- Access Token -------------|               |
  +--------+           & Optional Refresh Token        +---------------+

               Figure 2: Refreshing an Expired Access Token

Access token 无效的情况可能发生在相关验证或解密算法变更的情况下。refresh token 结构较为单一,可以不受影响。当用户修改密码时,access token 和 refresh token 同时失效。

Token 必须是无法伪造的,可以是简单的 GUID(用于在数据库中检索相关权限),也可以是包含丰富信息(如作用域,过期时间等)的加密字符串。具体的实现可以根据需求自行变更。

OAuth 2.0 实现

官方网站提供了各种语言的开源实现 http://oauth.net/2/