eggjs中Cookie与Session使用

# Cookie【要点】

HTTP 请求都是无状态的,但是我们的 Web 应用通常都需要知道发起请求的人是谁。为了解决这个问题,HTTP 协议设计了一个特殊的请求头:Cookie (opens new window)

服务端可以通过响应头(set-cookie)将少量数据响应给客户端,浏览器会遵循协议将数据保存,并在下次请求同一个服务的时候带上(浏览器也会遵循协议,只在访问符合 Cookie 指定规则的网站时带上对应的 Cookie 来保证安全性)。

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

    • {Boolean} httpOnly: 设置键值对是否可以被 js 访问,默认为 true,不允许被 js 访问
    • {Boolean} signed:设置是否对 Cookie 进行签名,如果设置为 true,则设置键值对的时候会同时对这个键值对的值进行签名,后面取的时候做校验,可以防止前端对这个值进行篡改。默认为 true。
    • {Boolean} encrypt:设置是否对 Cookie 进行加密,如果设置为 true,则在发送 Cookie 前会对这个键值对的值进行加密,客户端无法读取到 Cookie 的明文值。默认为 false。

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

ctx.cookies.set(key, value, {
  httpOnly: false,
  signed: false,
});
1
2
3
4

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

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

注意:

由于浏览器和其他客户端实现的不确定性 (opens new window),为了保证 Cookie 可以写入成功,建议 value 通过 base64 编码或者其他形式 encode 之后再写入。由于浏览器对 Cookie 有长度限制限制 (opens new window),所以尽量不要设置太长的 Cookie。一般来说不要超过 4093 bytes。当设置的 Cookie value 大于这个值时,框架会打印一条警告日志。 Cookie 秘钥由于我们在 Cookie 中需要用到加解密和验签,所以需要配置一个秘钥供加密使用。在 config/config.default.jsmodule.exports = { keys: 'key1,key2',};keys 配置成一个字符串,可以按照逗号分隔配置多个 key。Cookie 在使用这个配置进行加解密时:加密和加签时只会使用第一个秘钥。解密和验签时会遍历 keys 进行解密。

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


# Session

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

框架内置了 Session (opens new window) 插件,给我们提供了 ctx.session 来访问或者修改当前用户 Session 。

class HomeController extends Controller {
  async fetchPosts() {
    const ctx = this.ctx;
    // 获取 Session 上的内容
    const userId = ctx.session.userId;
    const posts = await ctx.service.post.fetch(userId);
    // 修改 Session 的值
    ctx.session.visited = ctx.session.visited ? (ctx.session.visited + 1) : 1;
    ctx.body = {
      success: true,
      posts,
    };
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

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

ctx.session = null;
1

需要 特别注意 的是:设置 session 属性时需要避免以下几种情况(会造成字段丢失,详见 koa-session (opens new window) 源码)

不要以 _ 开头不能为 isNew

// ❌ 错误的用法
ctx.session._visited = 1;   //    --> 该字段会在下一次请求时丢失
ctx.session.isNew = 'HeHe'; //    --> 为内部关键字, 不应该去更改

// ✔️ 正确的用法
ctx.session.visited = 1;    //   -->  此处没有问题
1
2
3
4
5
6

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

Session 的默认配置如下:

exports.session = {
  key: 'EGG_SESS',
  maxAge: 24 * 3600 * 1000, // 1 天
  httpOnly: true,
  encrypt: true,
};
1
2
3
4
5
6

在默认的配置下,存放 Session 的 Cookie 将会加密存储、不可被前端 js 访问,这样可以保证用户的 Session 是安全的。

# 扩展存储

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

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

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

sessionStore 的实现我们也可以封装到插件中,

例如 egg-session-redis (opens new window) 就提供了将 Session 存储到 redis 中的能力,

在应用层,我们只需要引入 egg-redis (opens new window)egg-session-redis (opens new window) 插件即可。

// plugin.js
exports.redis = {
  enable: true,
  package: 'egg-redis',
};
exports.sessionRedis = {
  enable: true,
  package: 'egg-session-redis',
};
1
2
3
4
5
6
7
8
9

注意:一旦选择了将 Session 存入到外部存储中,就意味着系统将强依赖于这个外部存储,当它挂了的时候,我们就完全无法使用 Session 相关的功能了。

因此我们更推荐大家只将必要的信息存储在 Session 中,保持 Session 的精简并使用默认的 Cookie 存储,用户级别的缓存不要存储在 Session 中。

# Session 实践

# 修改用户 Session 失效时间

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

const ms = require('ms');
class UserController extends Controller {
  async login() {
    const ctx = this.ctx;
    const { username, password, rememberMe } = ctx.request.body;
    const user = await ctx.loginAndGetUser(username, password);

    // 设置 Session
    ctx.session.user = user;
    // 如果用户勾选了 `记住我`,设置 30 天的过期时间
    if (rememberMe) ctx.session.maxAge = ms('30d');
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

# 延长用户 Session 有效期

默认情况下,当用户请求没有导致 Session 被修改时,框架都不会延长 Session 的有效期,但是在有些场景下,我们希望用户如果长时间都在访问我们的站点,则延长他们的 Session 有效期,不让用户退出登录态。框架提供了一个 renew配置项用于实现此功能,它会在发现当用户 Session 的有效期仅剩下最大有效期一半的时候,重置 Session 的有效期。

// config/config.default.js
module.exports = {
  session: {
    renew: true,
  },
};
1
2
3
4
5
6

配合使用

//登录
const token = app.jwt.sign({username: data.username}, app.config.jwt.secret);
const user = yield this.service.user.getUserInfo(data);
ctx.session.user = user;
ctx.set('Authorization', token);
ctx.status = 204;
1
2
3
4
5
6

role.js

module.exports = function (app) {
    const roleFn = function (role) {
        return function () {
            return this.session.user && this.session.user.roles.includes(role);
        }
    };
    app.role.use('admin', roleFn('admin'));
    app.role.use('node', roleFn('nodeUser'));
    app.role.use('resource', roleFn('resourceUser'));
    
     // 错误函数
      app.role.failureHandler = function(ctx) {
        const message = '当前用户权限不足';
        console.log(ctx.acceptJSON);
        //  this.body = { target: '/403.html', status: 'deny' };
        if (ctx.acceptJSON) {
          ctx.status = 403;
          ctx.body = {
            message,
          };
        }else{
          this.realStatus = 200;
          this.redirect('/403.html');
        }
      };
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

router.js

module.exports = app => {
    const nodeRole = app.role.can('node');
    const resourceRole = app.role.can('resource');

    app.get('/node', nodeRole, 'node.index');
    app.get('/resource', resourceRole, 'resource.index');
}
1
2
3
4
5
6
7

# 示例

https://github.com/eggjs/egg-session/blob/master/test/fixtures/cookie-session/app/controller/home.js

https://github.com/eggjs/egg-session/blob/master/test/fixtures/redis-session/app/controller/home.js

结合egg-userrole使用;

https://github.com/koajs/koa-roles

https://github.com/eggjs/egg-userrole

https://github.com/miaojiuchen/eggjs-userrole-demo

# 参考链接

https://eggjs.org/zh-cn/core/cookie-and-session.html

上次更新: 2022/04/15, 05:41:27
×