Skip to content

Commit

Permalink
docs: add cookie and session (eggjs#510)
Browse files Browse the repository at this point in the history
  • Loading branch information
dead-horse authored and fengmk2 committed Mar 5, 2017
1 parent cd152b3 commit 28b36b6
Show file tree
Hide file tree
Showing 5 changed files with 222 additions and 68 deletions.
1 change: 1 addition & 0 deletions docs/source/_data/guide_toc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Core:
Deployment: /core/deployment.html
Logger: /core/logger.html
HttpClient: /core/httpclient.html
Cookie and Session: /core/cookie-and-session.html
Cluster and IPC: /core/cluster-and-ipc.html
View: /core/view.html
Error Handling: /core/error-handling.html
Expand Down
72 changes: 4 additions & 68 deletions docs/source/zh-cn/basics/controller.md
Original file line number Diff line number Diff line change
Expand Up @@ -476,7 +476,7 @@ module.exports = {

### cookie

HTTP 的请求头中有一个特殊的字段叫 [cookie](https://en.wikipedia.org/wiki/HTTP_cookie)服务端可以通过 cookie 将少量数据存到客户端中(浏览器会遵循协议将数据保存)。HTTP 请求都是无状态的,但是我们的 web 应用通常都需要知道发起请求的人是谁,一个常用的解决方案就是通过 cookie 来确认用户身份
HTTP 请求都是无状态的,但是我们的 web 应用通常都需要知道发起请求的人是谁。为了解决这个问题,HTTP 协议设计了一个特殊的请求头:[cookie](https://en.wikipedia.org/wiki/HTTP_cookie)服务端可以通过响应头(set-cookie)将少量数据响应给客户端,浏览器会遵循协议将数据保存,并在下次请求同一个服务的时候带上(浏览器也会遵循协议,只在访问符合 cookie 指定规则的网站时带上对应的 cookie 来保证安全性)

通过 `context.cookies`,我们可以在 controller 中便捷、安全的设置和读取 cookie。

Expand All @@ -496,73 +496,7 @@ exports.remove = function* (ctx) {

cookie 虽然在 HTTP 中只是一个头,但是通过 `foo=bar;foo1=bar1;` 的格式可以设置多个键值对。

#### `context.cookies.set(key, value, options)`

设置 cookie 其实是通过在 HTTP 响应中设置 set-cookie 头完成的,每一个 set-cookie 都会让浏览器在 cookie 中存一个键值对。在设置 cookie 值的同时,协议还支持许多参数来配置这个 cookie 的传输、存储和权限。

- maxAge (Number): 设置这个键值对在浏览器的最长保存时间。是一个从服务器当前时刻开始的毫秒数。
- expires (Date): 设置这个键值对的失效时间,如果设置了 maxAge,将会被覆盖。如果 maxAge 和 expires 都没设置,cookie 将会在浏览器的会话失效(一般是关闭浏览器时)的时候失效。
- path (String): 设置键值对生效的路径,默认设置在根路径上(`/`)。
- domain (String): 设置键值对生效的域名,默认没有配置。
- httpOnly (Boolean): 设置键值对是否不能被 js 访问,默认为 true,不允许被 js 访问。
- secure (Boolean): 设置键值对[只在 HTTPS 连接上传输](http://stackoverflow.com/questions/13729749/how-does-cookie-secure-flag-work),框架会帮我们判断当前是否在 HTTPS 连接上自动设置 secure 的值。

除了这些属性之外,框架另外扩展了 3 个参数的支持:

- overwrite(Boolean):设置 key 相同的键值对如何处理,如果设置为 true,则后设置的值会覆盖前面设置的,否则将会发送两个 set-cookie 响应头。
- sign(Boolean):设置是否对 cookie 进行签名,如果设置为 true,则设置键值对的时候会同时对这个键值对的值进行签名,后面取的时候做校验,可以防止前端对这个值进行篡改。默认为 true。
- encrypt(Boolean):设置是否对 cookie 进行加密,如果设置为 true,则在发送 cookie 前会对这个键值对的值进行加密,客户端无法读取到 cookie 的值。默认为 false。

在设置 cookie 时我们需要思考清楚这个 cookie 的作用,它需要被浏览器保存多久?是否可以被 js 获取到?是否可以被前端修改?

**默认的配置下,cookie 是加签不加密的,浏览器可以看到明文,js 不能访问,不能被客户端(手工)篡改。**

- 如果想要 cookie 在浏览器端可以被 js 访问并修改:

```js
ctx.cookies.set(key, value, {
httpOnly: false,
sign: false,
});
```

- 如果想要 cookie 在浏览器端不能被修改,不能看到明文:

```js
ctx.cookies.set(key, value, {
httpOnly: true, // 默认就是 true
encrypt: true, // 加密传输
});
```

注意:

1. 由于[浏览器和其他客户端实现的不确定性](http://stackoverflow.com/questions/7567154/can-i-use-unicode-characters-in-http-headers),为了保证 cookie 可以写入成功,建议 value 通过 base64 编码或者其他形式 encode 之后再写入。
2. 由于[浏览器对 cookie 有长度限制限制](http://stackoverflow.com/questions/640938/what-is-the-maximum-size-of-a-web-browsers-cookies-key),所以尽量不要设置太长的 cookie。一般来说不要超过 4000 bytes。

#### `context.cookies.get(key, options)`

由于 HTTP 请求中的 cookie 是在一个 header 中传输过来的,通过框架提供的这个方法可以快速的从整段 cookie 中获取对应的键值对的值。上面在设置 cookie 的时候,我们可以设置 `options.signed``options.encrypt` 来对 cookie 进行签名或加密,因此对应的在获取 cookie 的时候也要传相匹配的选项。

- 如果设置的时候指定为 signed,获取时未指定,则不会在获取时对取到的值做验签,导致可能被客户端篡改。
- 如果设置的时候指定为 encrypt,获取时未指定,则无法获取到真实的值,而是加密过后的密文。

### cookie 秘钥

由于我们在 cookie 中需要用到加解密和验签,所以需要配置一个秘钥供加密使用。在 `config/config.default.js`

```js
module.exports = {
keys: 'key1,key2',
};
```

keys 配置成一个字符串,可以按照逗号分隔配置多个 key。cookie 在使用这个配置进行加解密时:

- 加密时只会使用第一个秘钥。
- 解密或验签时会遍历 keys 进行解密。

如果我们想要更新 cookie 的秘钥,但是又不希望之前设置到用户浏览器上的 cookie 失效,可以将新的秘钥配置到 keys 最前面,等过一段时间之后再删去不需要的秘钥即可。
cookie 在 web 应用中经常承担了传递客户端身份信息的作用,因此有许多安全相关的配置,不可忽视, [cookie](../core/cookie-and-session.md#cookie) 文档中详细介绍了 cookie 的用法和安全相关的配置项,可以深入阅读了解。

### session

Expand Down Expand Up @@ -592,6 +526,8 @@ exports.deleteSession = function* (ctx) {
};
```

和 cookie 一样,session 也有许多安全等选项和功能,在使用之前也最好阅读 [session](../core/cookie-and-session.md#session) 文档深入了解。

#### 配置

对于 session 来说,主要有下面几个属性可以在 `config.default.js` 中进行配置:
Expand Down
215 changes: 215 additions & 0 deletions docs/source/zh-cn/core/cookie-and-session.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
title: cookie 与 session
---

## cookie

HTTP 请求都是无状态的,但是我们的 web 应用通常都需要知道发起请求的人是谁。为了解决这个问题,HTTP 协议设计了一个特殊的请求头:[cookie](https://en.wikipedia.org/wiki/HTTP_cookie)。服务端可以通过响应头(set-cookie)将少量数据响应给客户端,浏览器会遵循协议将数据保存,并在下次请求同一个服务的时候带上(浏览器也会遵循协议,只在访问符合 cookie 指定规则的网站时带上对应的 cookie 来保证安全性)。

通过 `context.cookies`,我们可以在 controller 中便捷、安全的设置和读取 cookie。

```js
exports.add = function* (ctx) {
const count = ctx.cookie.get('count');
count = count ? Number(count) : 0;
ctx.cookie.set('count', ++count);
ctx.body = count;
};

exports.remove = function* (ctx) {
const count = ctx.cookie.set('count', null);
ctx.status = 204;
};
```

#### `context.cookies.set(key, value, options)`

设置 cookie 其实是通过在 HTTP 响应中设置 set-cookie 头完成的,每一个 set-cookie 都会让浏览器在 cookie 中存一个键值对。在设置 cookie 值的同时,协议还支持许多参数来配置这个 cookie 的传输、存储和权限。

- maxAge (Number): 设置这个键值对在浏览器的最长保存时间。是一个从服务器当前时刻开始的毫秒数。
- expires (Date): 设置这个键值对的失效时间,如果设置了 maxAge,expires 将会被覆盖。如果 maxAge 和 expires 都没设置,cookie 将会在浏览器的会话失效(一般是关闭浏览器时)的时候失效。
- path (String): 设置键值对生效的 URL 路径,默认设置在根路径上(`/`),也就是当前域名下的所有 URL 都可以访问这个 cookie。
- domain (String): 设置键值对生效的域名,默认没有配置,可以配置成只在指定域名才能访问。
- httpOnly (Boolean): 设置键值对是否可以被 js 访问,默认为 true,不允许被 js 访问。
- secure (Boolean): 设置键值对[只在 HTTPS 连接上传输](http://stackoverflow.com/questions/13729749/how-does-cookie-secure-flag-work),框架会帮我们判断当前是否在 HTTPS 连接上自动设置 secure 的值。

除了这些属性之外,框架另外扩展了 3 个参数的支持:

- overwrite(Boolean):设置 key 相同的键值对如何处理,如果设置为 true,则后设置的值会覆盖前面设置的,否则将会发送两个 set-cookie 响应头。
- sign(Boolean):设置是否对 cookie 进行签名,如果设置为 true,则设置键值对的时候会同时对这个键值对的值进行签名,后面取的时候做校验,可以防止前端对这个值进行篡改。默认为 true。
- encrypt(Boolean):设置是否对 cookie 进行加密,如果设置为 true,则在发送 cookie 前会对这个键值对的值进行加密,客户端无法读取到 cookie 的值。默认为 false。

在设置 cookie 时我们需要思考清楚这个 cookie 的作用,它需要被浏览器保存多久?是否可以被 js 获取到?是否可以被前端修改?

**默认的配置下,cookie 是加签不加密的,浏览器可以看到明文,js 不能访问,不能被客户端(手工)篡改。**

- 如果想要 cookie 在浏览器端可以被 js 访问并修改:

```js
ctx.cookies.set(key, value, {
httpOnly: false,
sign: false,
});
```

- 如果想要 cookie 在浏览器端不能被修改,不能看到明文:

```js
ctx.cookies.set(key, value, {
httpOnly: true, // 默认就是 true
encrypt: true, // 加密传输
});
```

注意:

1. 由于[浏览器和其他客户端实现的不确定性](http://stackoverflow.com/questions/7567154/can-i-use-unicode-characters-in-http-headers),为了保证 cookie 可以写入成功,建议 value 通过 base64 编码或者其他形式 encode 之后再写入。
2. 由于[浏览器对 cookie 有长度限制限制](http://stackoverflow.com/questions/640938/what-is-the-maximum-size-of-a-web-browsers-cookies-key),所以尽量不要设置太长的 cookie。一般来说不要超过 4093 bytes。当设置的 cookie value 大于这个值时,框架会打印一条警告日志。

#### `context.cookies.get(key, options)`

由于 HTTP 请求中的 cookie 是在一个 header 中传输过来的,通过框架提供的这个方法可以快速的从整段 cookie 中获取对应的键值对的值。上面在设置 cookie 的时候,我们可以设置 `options.signed``options.encrypt` 来对 cookie 进行签名或加密,因此对应的在获取 cookie 的时候也要传相匹配的选项。

- 如果设置的时候指定为 signed,获取时未指定,则不会在获取时对取到的值做验签,导致可能被客户端篡改。
- 如果设置的时候指定为 encrypt,获取时未指定,则无法获取到真实的值,而是加密过后的密文。

### cookie 秘钥

由于我们在 cookie 中需要用到加解密和验签,所以需要配置一个秘钥供加密使用。在 `config/config.default.js`

```js
module.exports = {
keys: 'key1,key2',
};
```

keys 配置成一个字符串,可以按照逗号分隔配置多个 key。cookie 在使用这个配置进行加解密时:

- 加密和加签时只会使用第一个秘钥。
- 解密和验签时会遍历 keys 进行解密。

如果我们想要更新 cookie 的秘钥,但是又不希望之前设置到用户浏览器上的 cookie 失效,可以将新的秘钥配置到 keys 最前面,等过一段时间之后再删去不需要的秘钥即可。

## session

cookie 在 web 应用中经常承担标识请求方身份的功能,所以 web 应用在 cookie 的基础上封装了 session 的概念,专门用做用户身份识别。

框架内置了 [session](https://github.com/eggjs/egg-session) 插件,给我们提供了 `context.session` 来访问或者修改当前用户 session 。

```js
exports.fetchPosts = function* (ctx) {
// 获取 session 上的内容
const userId = ctx.session.userId;
const posts = yield ctx.service.post.fetch(userId);
// 修改 session 的值
ctx.session.visited = ctx.session.visited ? ctx.session.visited++ : 1;
ctx.body = {
success: true,
posts,
};
};
```

session 的使用方法非常直观,直接读取它或者修改它就可以了,如果要删除它,直接将它赋值为 null:

```js
exports.deleteSession = function* (ctx) {
ctx.session = null;
};
```

session 的实现是基于 cookie 的,默认配置下,用户 session 的内容加密后直接存储在 cookie 中的一个字段中,用户每次请求我们网站的时候都会带上这个 cookie,我们在服务端解密后使用。session 的默认配置如下:

```js
exports.session = {
key: 'EGG_SESS',
maxAge: 24 * 3600 * 1000, // 1 天
httpOnly: true,
encrypt: true,
};
```

可以看到这些参数除了 `key` 都是 cookie 的参数,`key` 代表了存储 session 的 cookie 键值对的 key 是什么。在默认的配置下,存放 session 的 cookie 将会加密存储、不可被前端 js 访问,这样可以保证用户的 session 是安全的。

### 扩展存储

session 默认存放在 cookie 中,但是如果我们的 session 对象过于庞大,就会带来一些额外的问题:

- 前面提到,浏览器通常都有限制最大的 cookie 长度,当设置的 session 过大时,浏览器可能拒绝保存。
- cookie 在每次请求时都会带上,当 session 过大时,每次请求都要额外带上庞大的 cookie 信息。

框架提供了将 session 存储到除了 cookie 之外的其他存储的扩展方案,我们只需要设置 `app.sessionStore` 即可将 session 存储到指定的存储中。

```js
// app.js
module.exports = app => {
app.sessionStore = {
* get (key) {
// return value;
},
* set (key, value, maxAge) {
// set key to store
},
* destroy (key) {
// destroy key
},
};
};
```

sessionStore 的实现我们也可以封装到插件中,例如 [egg-session-redis] 就提供了将 session 存储到 redis 中的能力,在应用层,我们只需要引入 [egg-redis][egg-session-redis] 插件即可。

```js
// plugin.js
exports.redis = {
enable: true,
package: 'egg-redis',
};
exports.sessionRedis = {
enable: true,
package: 'egg-redis-session',
};
```

**注意:一旦选择了将 session 存入到外部存储中,就意味着系统将强依赖于这个外部存储,当它挂了的时候,我们就完全无法使用 session 相关的功能了。因此我们更推荐大家只将必要的信息存储在 session 中,保持 session 的精简并使用默认的 cookie 存储,用户级别的缓存不要存储在 session 中。**

### session 实践

#### 修改用户 session 失效时间

虽然在 session 的配置中有一项是 maxAge,但是它只能全局设置 session 的有效期,我们经常可以在一些网站的登陆页上看到有 **记住我** 的选项框,勾选之后可以让登陆用户的 session 有效期更长。这种针对特定用户的 session 有效时间设置我们可以通过 `context.session.maxAge=` 来实现。

```js
const ms = require('ms');
// login 的 controller
exports.login = function* (ctx) {
const { username, password, rememberMe } = ctx.request.body;
const user = yield ctx.loginAndGetUser(username, password);

// 设置 session
this.session.user = user;
// 如果用户勾选了 `记住我`,设置一个月的过期时间
if (rememberMe) this.session.maxAge = ms('1m');
};
```

#### 延长用户 session 有效期

默认情况下,当用户请求没有导致 session 被修改时,框架都不会延长 session 的有效期,但是在有些场景下,我们希望用户每次访问都刷新 session 的有效时间,这样用户只有在长期未访问我们的网站的时候才会被登出。这个功能我们可以通过 `context.session.save()` 来实现。

例如,我们在项目中增加一个中间件,让它在 session 有值的时候强制保存一次,以达到延长 session 有效期的目的。

```js
// app/middleware/save_session.js
module.exports = () => {
return function* (next) {
yield next;
// 如果 session 是空的,则不保存
if (!this.session.populated) return;
this.session.save();
};
};

// config/config.default.js
// 在配置文件中引入中间件
exports.middleware = [ 'saveSession' ];
```
1 change: 1 addition & 0 deletions docs/themes/egg/languages/en.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ guide_toc:
Error Handling: Error Handling
i18n: i18n
Cluster and IPC: Cluster and IPC
Cooke and Session: Cookie and Session
Tutorials: Tutorials
Advanced: Advanced
Cluster Client: Cluster Enhancement
Expand Down
1 change: 1 addition & 0 deletions docs/themes/egg/languages/zh-cn.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ guide_toc:
Error Handling: 异常处理
i18n: 国际化
Cluster and IPC: 多进程模型和进程间通讯
Cooke and Session: Cookie 和 Session
Tutorials: 教程
Advanced: 进阶
Cluster Client: 多进程研发模式增强
Expand Down

0 comments on commit 28b36b6

Please sign in to comment.