HTTP is a stateless protocol.
But web applications often need to identify the sender of requests.
To solve this problem, HTTP protocol defines a header Cookie.
Web servers can use response header Set-Cookie
to send small data to clients.
Clients (e.g. web browsers) store the data according to protocol,
and attach the cookie data in future requests.
For security reason, browsers only attach cookies in the requests that are sent to the same domain.
Web servers can use Domain
and Path
attributes to define the scope of the cookie.
By using context.cookies
, we can easily and safely read/set cookies in controller.
exports.add = function* (ctx) {
let count = ctx.cookies.get('count');
count = count ? Number(count) : 0;
ctx.cookies.set('count', ++count);
ctx.body = count;
};
exports.remove = function* (ctx) {
ctx.cookies.set('count', null);
ctx.status = 204;
};
Modifying Cookie is done by setting Set-Cookie
header in HTTP responses.
Each Set-Cookie
creates a key-value pair in client.
Besides of setting the value of Cookie,
HTTP protocol supports more attributes to control the transfer, storage and permission of Cookie.
- maxAge (Number): set the lifetime of the cookie in milliseconds. It's the milliseconds since server's "Now". Client discards a cookie after specified lifetime.
- expires (Date): set the expiration time of the cookie. If
maxAge
is defined,expires
will be ignored. If neither is defined, Cookie will expire when client session expires, usually it's the time client closed. - path (String): set the path of the cookie. By default it's on root path (
/
), which means all URL under the current domain have access to the cookie. - domain (String): set the domain of the cookie. By default, it's not defined. If defined, only specified domain have access to the cookie.
- httpOnly (Boolean): set whether the cookie can be accessed by Javascript. By default it's
true
, which means Javascript cannot access the cookie. - secure (Boolean): set whether the cookie can only be accessed under HTTPS. See explanation for details. Beidou auto sets this value to true if the current request is sent over HTTPS.
In addition to these standard Cookie attributes, beidou supports 3 more parameters:
- overwrite (Boolean): set the way of handling same Cookie key. If true, earlier called values will be overwritten by the last call; otherwise HTTP response will contain multiple
Set-Cookie
headers with the same key. - signed (Boolean): set whether the cookie should be signed. If true, the value of the cookie will be signed. So that when the value is being read, server verifies the signature to prevent cookie values modified by client. By default it's true.
- encrypt(Boolean): set whether the cookie should be encrypted. It true, the cookie value will be encrypted before sending to clients so user clients cannot get raw text of the cookie. By default it's false.
When using Cookie, we need to have a clear idea of the purpose of the cookie, how long it needs to be stored in client, can it be accessed by JS, can it be modified by client.
By default, Cookie is signed but not encrypted, client can see raw content but cannot modify it (manually).
- If you need to allow JS to access and modify Cookie:
ctx.cookies.set(key, value, {
httpOnly: false,
signed: false,
});
- If you don't want to allow JS to access and modify Cookie:
ctx.cookies.set(key, value, {
httpOnly: true, // by default it's true
encrypt: true, // cookies are encrypted during network transmission
});
Note:
- Due to the uncertainty of client's implementation, to ensure Cookie can be stored successfully, it's recommended to encode cookie value in base64 or other codec.
- Due to the limitation of Cookie length on client side, do avoid using long Cookie. Generally speaking, no more than 4093 bytes. When Cookie value's length is greater than this value, beidou prints a warning in log.
As HTTP Cookie is sent over header,
we can use this method to easily retrieve the value of given key from Cookie.
If options.signed
and options.encrypt
has been configured to sign and encrypt Cookie,
the corresponding options also need to be used in get
method.
- If
signed
is true whenset
Cookie but false whenget
Cookie, beidou doesn't verify Cookie value, so the value could have been modified by client. - If
encrypt
is true whenset
Cookie but false whenget
Cookie, what you get is encrypted text rather than the raw plain text.
Since we need to sign and encrypt Cookie, a secret key is required.
In config/config.default.js
:
module.exports = {
keys: 'key1,key2',
};
keys
is defined as a string, several keys separated by commas.
When beidou processes Cookie:
- the first key is used in encryption and signature generation.
- to decrypt or verify signature, beidou iterates through all keys.
If you need to update Cookie secret key and don't want to invalidate existing clients' Cookie,
you can add new secret key at the front of keys
.
After some time, when existing Cookie has expired, delete the old secret keys.
In web applications, Cookie is usually used to identify users. So the concept of Session, which is built on top of Cookie, was created to specifically handle user identification.
Beidou built-in supports Session through egg-session plugin.
We can use context.session
to read or modify current user session.
exports.fetchPosts = function* (ctx) {
// get content from session
const userId = ctx.session.userId;
const posts = yield ctx.service.post.fetch(userId);
// modify session value
ctx.session.visited = ctx.session.visited ? ctx.session.visited++ : 1;
ctx.body = {
success: true,
posts,
};
};
It is very intuitive to use Session, simply get or set. To delete a session, set its value to null:
exports.deleteSession = function* (ctx) {
ctx.session = null;
};
Session is built on top of Cookie. By default, the content of Session is stored in a Cookie field as encrypted string. Every time a client sends requests to server, the cookie is attached in requests. Beidou passes decrypted cookie to server code. The default configuration of Session is:
exports.session = {
key: 'EGG_SESS',
maxAge: 24 * 3600 * 1000, // 1 day
httpOnly: true,
encrypt: true,
};
The attributes except of key
are all standard Cookie attributes.
key
is the key of the cookie that stores session content.
With default config, the session cookie is encrypted, not accessible to JS,
which ensures user cannot access or modify it.
Session is stored in Cookie by default. If a session is too big, there are some troubles.
- as mentioned above, clients usually have limitation on Cookie length. When session is too big, a client may refuse to store it.
- Cookie is attached in every request. If session is too big, the additional cost of sending session may be significant.
Beidou supports to store Session in other places.
To config it, you can simply set app.sessionStore
.
// app.js
module.exports = app => {
app.sessionStore = {
* get (key) {
// return value;
},
* set (key, value, maxAge) {
// set key to store
},
* destroy (key) {
// destroy key
},
};
};
The implementation of sessionStore
can also be encapsulated into a plugin.
For example, egg-session-redis stores Session in Redis.
To apply it, import egg-redis and egg-session-redis plugin in your application.
// plugin.js
exports.redis = {
enable: true,
package: 'egg-redis',
};
exports.sessionRedis = {
enable: true,
package: 'egg-session-redis',
};
Note: once you choose to store Session in external storage, it means your system heavily depends on the external storage. Once it's down, Session feature won't work. So it's recommended to put only necessary information in Session, keep Session minimum and use the default Cookie storage if possible. Do not put per-user's data cache in Session.
Session config has a attribute maxAge
, which controls global expiration time of all sessions of the application.
We often can see a Remember Me option on a lot of websites' login page.
If it's selected, Session of this logged in user can live longer.
This kind of per-user session expiration time can be set through context.session.maxAge
:
const ms = require('ms');
// login controller
exports.login = function* (ctx) {
const { username, password, rememberMe } = ctx.request.body;
const user = yield ctx.loginAndGetUser(username, password);
// set Session
this.session.user = user;
// if user selected `Remember Me`, set expiration time to 30 days
if (rememberMe) this.session.maxAge = ms('30d');
};
By default, if user requests don't result in modification of Session,
beidou doesn't extend expiration time of the session.
But in some scenarios, you may need to refresh expiration time every time user access the website,
so that users will only be logged out when they don't access website for long time.
This requirement can be done through context.session.save()
.
For example, we create a middleware in the application. It forces saving session in every request, in order to extend session's expiration time.
// app/middleware/save_session.js
module.exports = () => {
return function* (next) {
yield next;
// if Session is empty, do nothing
if (!this.session.populated) return;
this.session.save();
};
};
// config/config.default.js
// import the middleware in config file
exports.middleware = [ 'saveSession' ];