cookies
399
2-ui/5-data-storage/01-cookie/article.md
Normal file
|
@ -0,0 +1,399 @@
|
||||||
|
# Cookies, document.cookie
|
||||||
|
|
||||||
|
Cookies allow to store small pieces of data directly in the browser. They are not part of Javascript, but rather part of HTTP, defined by [RFC 6265](https://tools.ietf.org/html/rfc6265) specification.
|
||||||
|
|
||||||
|
Most of the time, cookies are set by a webserver, but Javascript can access them too.
|
||||||
|
|
||||||
|
One of the most widespread use of cookies is authentication:
|
||||||
|
|
||||||
|
1. Upon sign in, the server sets `Set-Cookie` HTTP-header with a cookie with "session id".
|
||||||
|
2. The browser stores it.
|
||||||
|
3. The browser sends it over the net in `Cookie` HTTP-header for every request to the domain that set it. So the server knows who made the request.
|
||||||
|
|
||||||
|
The browser provides a special accessor `document.cookie` for cookies.
|
||||||
|
|
||||||
|
There are many tricky things about cookies and their options, how to set them right. In this chapter we'll cover them in detail.
|
||||||
|
|
||||||
|
## Reading from document.cookie
|
||||||
|
|
||||||
|
```online
|
||||||
|
Do you have any cookies on this site? Let's see:
|
||||||
|
```
|
||||||
|
|
||||||
|
```offline
|
||||||
|
Assuming you're on a website, it's possible to see the cookies, like this:
|
||||||
|
```
|
||||||
|
|
||||||
|
```js run
|
||||||
|
// At javascript.info, we use Google Analytics for statistics,
|
||||||
|
// so there should be some cookies from there.
|
||||||
|
alert( document.cookie ); // cookie1=value1; cookie2=value2;...
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
The string consist of `name=value` pairs, delimited by `; `. So, to find a particular cookie, we can split `document.cookie` by `; `, and then find the right key. We can use either a regular expression or array functions to do that. At the end of the chapter you'll find a few functions to manipulate cookies.
|
||||||
|
|
||||||
|
## Writing to document.cookie
|
||||||
|
|
||||||
|
The `document.cookie` is writable. But it's not a data property, but rather an accessor.
|
||||||
|
|
||||||
|
**A write operation to `document.cookie` passes through the browser that updates cookies mentioned in it, but doesn't touch other cookies.**
|
||||||
|
|
||||||
|
For instance, this call sets a cookie with the name `user` and value `John`:
|
||||||
|
|
||||||
|
```js run
|
||||||
|
document.cookie = "user=John";
|
||||||
|
alert(document.cookie);
|
||||||
|
```
|
||||||
|
|
||||||
|
If you run it, then probably you'll see multiple cookies. Only the cookie named `user` was altered.
|
||||||
|
|
||||||
|
Technically, name and value can have any characters, but then they should be escaped using a built-in `encodeURIComponent` function:
|
||||||
|
|
||||||
|
```js run
|
||||||
|
let name = "<>";
|
||||||
|
let value = "="
|
||||||
|
// encodes the cookie as %3C%3E=%3D
|
||||||
|
document.cookie = encodeURIComponent(name) + '=' + encodeURIComponent(value);
|
||||||
|
alert(document.cookie); // ...; %3C%3E=%3D
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
```warn header="Limitations"
|
||||||
|
There are few limitations:
|
||||||
|
- The `name=value` pair together (after `encodeURIComponent`) should not exceed 4kb. So we can't store anything huge in a cookie.
|
||||||
|
- The total number of cookies per domain is limited to 30-50, depending on a browser.
|
||||||
|
```
|
||||||
|
|
||||||
|
Cookies have several options, many of them are important and should be set.
|
||||||
|
|
||||||
|
The options are listed after `key=value`, delimited by `;`, for instance:
|
||||||
|
|
||||||
|
```js run
|
||||||
|
document.cookie = "user=John; path=/; expires=Tue, 19 Jan 2038 03:14:07 GMT"
|
||||||
|
```
|
||||||
|
|
||||||
|
## path
|
||||||
|
|
||||||
|
- **`path=/mypath`**
|
||||||
|
|
||||||
|
The url path prefix, where the cookie is accessible. By default, it's the current path.
|
||||||
|
|
||||||
|
If a cookie is set with `path=/mypath`, it's visible at `/mypath` and `/mypath/page`, but not at `/page` or `/mypathpage`.
|
||||||
|
|
||||||
|
Usually, we set `path=/` to make the cookie accessible from all website pages.
|
||||||
|
|
||||||
|
Please note: the path must be absolute (start with `/`).
|
||||||
|
|
||||||
|
## domain
|
||||||
|
|
||||||
|
- **`domain=site.com`**
|
||||||
|
|
||||||
|
Domain where the cookie is accessible.
|
||||||
|
|
||||||
|
By default, cookie is accessible only at the domain that set it. So, if we set a cookie at `site.com`, we won't get it `other.com`. That's natural, as `other.com` is another site.
|
||||||
|
|
||||||
|
What's more tricky, we won't get it at a subdomain `forum.site.com`:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// at site.com
|
||||||
|
document.cookie = "user=John"
|
||||||
|
|
||||||
|
// at forum.site.com
|
||||||
|
alert(document.cookie); // no user
|
||||||
|
```
|
||||||
|
|
||||||
|
**There's no way to let a cookie be accessible from another 2nd-level domain, so `other.com` will never receive a cookie set at `site.com`.**
|
||||||
|
|
||||||
|
It's a safety restriction, to allow us to store sensitive data in cookies.
|
||||||
|
|
||||||
|
For subdomains like `forum.site.com` that's possible. If we'd like a subdomain to access the cookie, we should set the `domain` to it. Or, much more common that we'd like any subdomain `*.site.com` to access the cookie, then we should set `domain=site.com`:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// at site.com, make the cookie accessible on any subdomain:
|
||||||
|
document.cookie = "user=John; domain=site.com"
|
||||||
|
|
||||||
|
// at forum.site.com
|
||||||
|
alert(document.cookie); // with user
|
||||||
|
```
|
||||||
|
|
||||||
|
For historical reasons, `domain=.site.com` (a dot at the start) also works this way.
|
||||||
|
|
||||||
|
## expires, max-age
|
||||||
|
|
||||||
|
By default, if a cookie doesn't have one of these options, it disappears when the browser is closed. Such cookies are called "session cookies"
|
||||||
|
|
||||||
|
To let cookies survive browser close, we can set either `expires` or `max-age` option.
|
||||||
|
|
||||||
|
- **`expires=Tue, 19 Jan 2038 03:14:07 GMT`**
|
||||||
|
|
||||||
|
Cookie expiration date, when the browser will delete it automatically.
|
||||||
|
|
||||||
|
The date must be exactly in this format, in GMT timezone. We can use `date.toUTCString` to get it. For instance, we can set the cookie to expire in 1 day:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// +1 day from now
|
||||||
|
let date = new Date(Date.now() + 86400e3);
|
||||||
|
date = date.toUTCString();
|
||||||
|
document.cookie = "user=John; expires=" + date;
|
||||||
|
```
|
||||||
|
|
||||||
|
If the date is in the past, the cookie will be deleted from the browser.
|
||||||
|
|
||||||
|
- **`max-age=3600`**
|
||||||
|
|
||||||
|
An alternative to `expires`, specifies the cookie expiration in seconds.
|
||||||
|
|
||||||
|
Can be either a number of seconds from the current moment, or zero/negative for immediate expiration (to remove the cookie):
|
||||||
|
|
||||||
|
```js
|
||||||
|
// cookie will die +1 hour from now
|
||||||
|
document.cookie = "user=John; max-age=3600";
|
||||||
|
|
||||||
|
// delete cookie (let it expire right now)
|
||||||
|
document.cookie = "user=John; max-age=0";
|
||||||
|
```
|
||||||
|
|
||||||
|
## secure
|
||||||
|
|
||||||
|
- **`secure`**
|
||||||
|
|
||||||
|
The cookie should be transferred only over HTTPS.
|
||||||
|
|
||||||
|
**By default if we set a cookie at `http://site.com`, then it also appears at `https://site.com` and vise versa.**
|
||||||
|
|
||||||
|
That is, cookies only check the domain, they do not distinguish between the protocols.
|
||||||
|
|
||||||
|
With this option, if a cookie is set while `https://site.com`, then it doesn't appear when the same site is accessed by HTTP, as `http://site.com`. So if a cookie has sensitive content that should never be sent over unencrypted HTTP, then the flag can prevent this.
|
||||||
|
|
||||||
|
```js
|
||||||
|
// set the cookie secure (only accessible if over HTTPS)
|
||||||
|
document.cookie = "user=John; secure";
|
||||||
|
```
|
||||||
|
|
||||||
|
## samesite
|
||||||
|
|
||||||
|
That's another security option, to protect from so-called XSRF (cross-site request forgery) attacks.
|
||||||
|
|
||||||
|
To understand when it's useful, let's introduce the following attack scenario.
|
||||||
|
|
||||||
|
### XSRF attack
|
||||||
|
|
||||||
|
Imagine, you are logged into the site `bank.com`. That is: you have an authentication cookie from that site. Your browser sends it to `bank.com` on every request, so that it recognizes you and performs all sensitive financial operations.
|
||||||
|
|
||||||
|
Now, while browsing the web in another window, you occasionally come to another site `evil.com`, that has a `<form action="https://bank.com/pay">` with hacker's account and JavaScript code that sends it automatically.
|
||||||
|
|
||||||
|
The form is submitted to the bank site, and your cookie is also sent, just because it's sent every time you visit `bank.com`. So the bank recognizes you and actually performs the payment.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
That's called a cross-site request forgery (or XSRF) attack.
|
||||||
|
|
||||||
|
Real banks are protected from it of course. All forms generated by `bank.com` have a special field, so called "xsrf protection token", that the evil page can't generate.
|
||||||
|
|
||||||
|
### Enter cookie samesite option
|
||||||
|
|
||||||
|
Now, cookie samesite option provides another way to protect from such attacks, that (in theory) should not require "xsrf protection tokens".
|
||||||
|
|
||||||
|
It has two possible values:
|
||||||
|
|
||||||
|
- **`samesite=strict`, same as `samesite` without value**
|
||||||
|
|
||||||
|
A cookie with `samesite=strict` is never sent if the user comes from outside the site.
|
||||||
|
|
||||||
|
In other words, whether a user follows a link from the mail or submits a form from `evil.com`, for any operation that comes from another domain, the cookie is not sent. Then the XSRF attack will fail, as `bank.com` will not recognize the user without the cookie, and will not proceed with the payment.
|
||||||
|
|
||||||
|
The protection is quite reliable. Only operations originating from `bank.com` will send cookies.
|
||||||
|
|
||||||
|
Although, there's a small inconvenience.
|
||||||
|
|
||||||
|
When a user follows a legitimate link to `bank.com`, like from their own notes, they'll be surprised that `bank.com` does not recognize them. Indeed, `samesite=strict` cookies are not sent in that case.
|
||||||
|
|
||||||
|
We could work around that by using two cookies: one for "general recognition", only for the purposes of saying: "Hello, John", and the other one for data-changing operations with `samesite=strict`.
|
||||||
|
|
||||||
|
Then a person coming from outside of the site will see a welcome, but payments must be initiated from the bank website.
|
||||||
|
|
||||||
|
- **`samesite=lax`**
|
||||||
|
|
||||||
|
Another approach to keep user experience is to use `samesite=lax`, a more relaxed value.
|
||||||
|
|
||||||
|
Lax mode, just like `strict`, forbids the browser to send cookies when coming from outside the site, but adds an exception.
|
||||||
|
|
||||||
|
A `samesite=lax` cookie is sent if both of these conditions are true:
|
||||||
|
1. The HTTP method is "safe" (e.g. GET, but not POST).
|
||||||
|
|
||||||
|
The full list safe of HTTP methods is in the [RFC7231 specification](https://tools.ietf.org/html/rfc7231). Basically, these are the methods that should be used for reading, but not writing the data. They must not perform any data-changing operations. Following a link is always GET, the safe method.
|
||||||
|
|
||||||
|
2. The operation performs top-level navigation (changes URL in the browser address bar).
|
||||||
|
|
||||||
|
That's usually true, but if the navigation is performed in an `<iframe>`, then it's not top-level. Also, AJAX requests do not get in.
|
||||||
|
|
||||||
|
So, what `samesite=lax` does is basically allows a most common "open URL" operation to bring cookies. Something more complicated, like AJAX request from another site or a form submittion loses cookies.
|
||||||
|
|
||||||
|
If that's fine for you, then adding `samesite=lax` will probably not break the user experience and add protection.
|
||||||
|
|
||||||
|
Overall, `samesite` is great, but it has an important drawback:
|
||||||
|
- `samesite` is not supported, ignored by old browsers (like year 2017).
|
||||||
|
|
||||||
|
**So if we solely rely on `samesite` to provide protection, then old browsers will be totally vulnerable.**
|
||||||
|
|
||||||
|
But we surely can use `samesite` together with other protection measures, like xsrf tokens, to add an additional layer of defence.
|
||||||
|
|
||||||
|
## httpOnly
|
||||||
|
|
||||||
|
This option has nothing to do with Javascript, but we have to mention it for completeness.
|
||||||
|
|
||||||
|
Only the server, when it uses `Set-Cookie` to set a cookie, may set the `httpOnly` option.
|
||||||
|
|
||||||
|
This option forbids any JavaScript access to the cookie. We can't see such cookie or manipulate it using `document.cookie`.
|
||||||
|
|
||||||
|
That's used as a precaution measure, to protect from certain attacks when a hacker injects his own Javascript code into a page and waits for a user to visit that page. That shouldn't be possible at all, a hacker should not be able to inject their code into our site, but there may be bugs that let hackers do it.
|
||||||
|
|
||||||
|
|
||||||
|
Normally, if such thing happens, then s hacker's code would execute and gain access to `document.cookie` with user cookies, containing authentication information.
|
||||||
|
|
||||||
|
But if a cookie is `httpOnly`, then `document.cookie` doesn't see it, so it is protected.
|
||||||
|
|
||||||
|
## Appendix: Third-party cookies
|
||||||
|
|
||||||
|
A cookie is called "third-party" if it's placed by domain other than the user is visiting.
|
||||||
|
|
||||||
|
For instance:
|
||||||
|
1. A page at `site.com` loads an banner from another site: `<img src="https://ads.com/banner.png">`.
|
||||||
|
2. Along with the banner, the remote server at `ads.com` may set `Set-Cookie` header with cookie like `id=1234`. Such cookie originates from `ads.com` domain, and will only be visible at `ads.com`:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
3. Next time when `ads.com` is accessed, the remote server gets the `id` cookie and recognizes the user:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
4. What's even more important, when the users moves from `site.com` to another site `other.com` that also has a banners, then `ads.com` gets the cookie, as it belongs to `ads.com`, thus recognizing the visitor and tracking him as he moves between sites:
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
Third-party cookies are traditionally used for tracking and ads services, due to their nature. They are bound to the originating domain, so `ads.com` can track the same user between different sites, if they all access it.
|
||||||
|
|
||||||
|
Naturally, some people don't like being tracked, so browsers allow to disable such cookies.
|
||||||
|
|
||||||
|
Also, some modern browsers employ special policies for such cookies:
|
||||||
|
- Safari does not allow third-party cookies at all.
|
||||||
|
- Firefox comes with a "black list" of third-party domains where it blocks third-party cookies.
|
||||||
|
|
||||||
|
|
||||||
|
```smart
|
||||||
|
If we load a script from a third-party domain, like `<script src="https://google-analytics.com/analytics.js">`, and that script uses `document.cookie` to set a cookie, then such cookie is not third-party.
|
||||||
|
|
||||||
|
If a script sets a cookie, then no matter where the script came from -- ito belongs to the domain of the current webpage.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Appendix: GDPR
|
||||||
|
|
||||||
|
This topic is not related to JavaScript at all, just something to keep in mind when setting cookies.
|
||||||
|
|
||||||
|
There's a legislation in Europe called GDPR, that enforces a set of rules for websites to respect users' privacy. And one of such rules is to require an explicit permission for tracking cookies from a user.
|
||||||
|
|
||||||
|
Please note, that's only about tracking/identifying cookies.
|
||||||
|
|
||||||
|
So, if we set a cookie that just saves some information, but neither tracks nor identifies the user, then we are free to do it.
|
||||||
|
|
||||||
|
But if we are going to set a cookie with an authentication session or a tracking id, then a user must allow that.
|
||||||
|
|
||||||
|
Websites generally have two variants of following GDPR. You must have seen them both already in the web:
|
||||||
|
|
||||||
|
1. If a website wants to set tracking cookies only for authenticated users.
|
||||||
|
|
||||||
|
To do so, the registration form should have a checkbox like "accept the privacy policy", the user must check it, and then the website is free to set auth cookies.
|
||||||
|
|
||||||
|
2. If a website wants to set tracking cookies for everyone.
|
||||||
|
|
||||||
|
To do so legally, a website shows a modal "splash screen" for newcomers, and require them to agree for cookies. Then the website can set them. That can be disturbing for a new visitor though.
|
||||||
|
|
||||||
|
|
||||||
|
GDPR is not only about cookies, but about other privacy-related issues too, but that's too much beyond our scope.
|
||||||
|
|
||||||
|
## Appendix: Cookie functions
|
||||||
|
|
||||||
|
Here's a small set of functions to work with cookies, more conveinent than a manual modification of `document.cookie`.
|
||||||
|
|
||||||
|
There exist many cookie libraries for that, so these are for demo purposes. Fully working though:
|
||||||
|
|
||||||
|
|
||||||
|
### getCookie(name)
|
||||||
|
|
||||||
|
The shortest way to access cookie is to use a [regular expression](info:regular-expressions).
|
||||||
|
|
||||||
|
The function `getCookie(name)` returns the cookie with the given `name`:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// returns the cookie with the given name,
|
||||||
|
// or undefined if not found
|
||||||
|
function getCookie(name) {
|
||||||
|
let matches = document.cookie.match(new RegExp(
|
||||||
|
"(?:^|; )" + name.replace(/([\.$?*|{}\(\)\[\]\\\/\+^])/g, '\\$1') + "=([^;]*)"
|
||||||
|
));
|
||||||
|
return matches ? decodeURIComponent(matches[1]) : undefined;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Here the regexp is generated dynamically, to match `; name=<value>`.
|
||||||
|
|
||||||
|
Please note that a cookie value can be an arbitrary string. If it contains characters that break the formatting, for instance spaces or `;`, such characters are encoded.
|
||||||
|
|
||||||
|
To decode them, we need to use a built-in `decodeURIComponent` function, the function does it also.
|
||||||
|
|
||||||
|
### setCookie(name, value, options)
|
||||||
|
|
||||||
|
Sets the cookie `name` to the given `value` with `path=/` by default (can be modified to add other defaults):
|
||||||
|
|
||||||
|
```js run
|
||||||
|
function setCookie(name, value, options = {}) {
|
||||||
|
|
||||||
|
options = {
|
||||||
|
path: '/',
|
||||||
|
// add other defaults here if necessary
|
||||||
|
...options
|
||||||
|
};
|
||||||
|
|
||||||
|
if (options.expires.toUTCString) {
|
||||||
|
options.expires = options.expires.toUTCString();
|
||||||
|
}
|
||||||
|
|
||||||
|
let updatedCookie = encodeURIComponent(name) + "=" + encodeURIComponent(value);
|
||||||
|
|
||||||
|
for (let optionKey in options) {
|
||||||
|
updatedCookie += "; " + optionKey;
|
||||||
|
let optionValue = options[optionKey];
|
||||||
|
if (optionValue !== true) {
|
||||||
|
updatedCookie += "=" + optionValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.cookie = updatedCookie;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Example of use:
|
||||||
|
setCookie('user', 'John', {secure: true, 'max-age': 3600});
|
||||||
|
```
|
||||||
|
|
||||||
|
### deleteCookie(name)
|
||||||
|
|
||||||
|
To delete a cookie, we can call it with a negative expiration date:
|
||||||
|
|
||||||
|
```js
|
||||||
|
function deleteCookie(name) {
|
||||||
|
setCookie(name, "", {
|
||||||
|
'max-age': -1
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```warn header="Updating or deleting must use same path and domain"
|
||||||
|
Please note: when we update or delete a cookie, we should use exactly the same path and domain options as when we set it.
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
[TODO]
|
BIN
2-ui/5-data-storage/01-cookie/cookie-third-party-2.png
Normal file
After Width: | Height: | Size: 18 KiB |
BIN
2-ui/5-data-storage/01-cookie/cookie-third-party-2@2x.png
Normal file
After Width: | Height: | Size: 42 KiB |
BIN
2-ui/5-data-storage/01-cookie/cookie-third-party-3.png
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
2-ui/5-data-storage/01-cookie/cookie-third-party-3@2x.png
Normal file
After Width: | Height: | Size: 47 KiB |
BIN
2-ui/5-data-storage/01-cookie/cookie-third-party.png
Normal file
After Width: | Height: | Size: 21 KiB |
BIN
2-ui/5-data-storage/01-cookie/cookie-third-party@2x.png
Normal file
After Width: | Height: | Size: 48 KiB |
BIN
2-ui/5-data-storage/01-cookie/cookie-xsrf.png
Normal file
After Width: | Height: | Size: 21 KiB |
BIN
2-ui/5-data-storage/01-cookie/cookie-xsrf@2x.png
Normal file
After Width: | Height: | Size: 50 KiB |
45
2-ui/5-data-storage/01-cookie/cookie.js
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
// возвращает cookie с именем name, если есть, если нет, то undefined
|
||||||
|
function getCookie(name) {
|
||||||
|
var matches = document.cookie.match(new RegExp(
|
||||||
|
"(?:^|; )" + name.replace(/([\.$?*|{}\(\)\[\]\\\/\+^])/g, '\\$1') + "=([^;]*)"
|
||||||
|
));
|
||||||
|
return matches ? decodeURIComponent(matches[1]) : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// устанавливает cookie с именем name и значением value
|
||||||
|
// options - объект с свойствами cookie (expires, path, domain, secure)
|
||||||
|
function setCookie(name, value, options) {
|
||||||
|
options = options || {};
|
||||||
|
|
||||||
|
var expires = options.expires;
|
||||||
|
|
||||||
|
if (typeof expires == "number" && expires) {
|
||||||
|
var d = new Date();
|
||||||
|
d.setTime(d.getTime() + expires * 1000);
|
||||||
|
expires = options.expires = d;
|
||||||
|
}
|
||||||
|
if (expires && expires.toUTCString) {
|
||||||
|
options.expires = expires.toUTCString();
|
||||||
|
}
|
||||||
|
|
||||||
|
value = encodeURIComponent(value);
|
||||||
|
|
||||||
|
var updatedCookie = name + "=" + value;
|
||||||
|
|
||||||
|
for (var propName in options) {
|
||||||
|
updatedCookie += "; " + propName;
|
||||||
|
var propValue = options[propName];
|
||||||
|
if (propValue !== true) {
|
||||||
|
updatedCookie += "=" + propValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.cookie = updatedCookie;
|
||||||
|
}
|
||||||
|
|
||||||
|
// удаляет cookie с именем name
|
||||||
|
function deleteCookie(name) {
|
||||||
|
setCookie(name, "", {
|
||||||
|
expires: -1
|
||||||
|
})
|
||||||
|
}
|
BIN
2-ui/5-data-storage/01-cookie/safari-nocookie.png
Normal file
After Width: | Height: | Size: 27 KiB |
BIN
2-ui/5-data-storage/01-cookie/safari-nocookie@2x.png
Normal file
After Width: | Height: | Size: 71 KiB |
381
2-ui/5-data-storage/02-localstorage/article.md
Normal file
|
@ -0,0 +1,381 @@
|
||||||
|
# Cookies, document.cookie
|
||||||
|
|
||||||
|
Cookies allow to store small pieces of data directly in the browser. They are not part of Javascript, but rather part of HTTP, defined by [RFC 6265](https://tools.ietf.org/html/rfc6265) specification.
|
||||||
|
|
||||||
|
We can use Javascript or server-side headers to get/set cookies.
|
||||||
|
|
||||||
|
Then the browser sends them in `Cookie` header of every request to the domain that set the cookie, but not to other domains.
|
||||||
|
|
||||||
|
So, cookies are mainly used for authorization and user-tracking. We set a cookie, and then get it at new visits of the same user, so we know who he is.
|
||||||
|
|
||||||
|
The browser provides a special accessor `document.cookie` for that. Managing it manually is quite a tedious task, so we'll compose a few several functions for that.
|
||||||
|
|
||||||
|
## Reading from document.cookie
|
||||||
|
|
||||||
|
```online
|
||||||
|
Do you have any cookies on that site? Let's see:
|
||||||
|
```
|
||||||
|
|
||||||
|
```offline
|
||||||
|
Assuming you're on a website, it's possible to see the cookies, like this:
|
||||||
|
```
|
||||||
|
|
||||||
|
```js run
|
||||||
|
alert( document.cookie ); // cookie1=value1; cookie2=value2;...
|
||||||
|
```
|
||||||
|
|
||||||
|
The string consist of `name=value` pairs, delimited by `; `. So, to find a particular cookie, we can split `document.cookie` by `; `, and then find the right key. We can use either a regular expression or arrays for that.
|
||||||
|
|
||||||
|
## getCookie(name)
|
||||||
|
|
||||||
|
The shortest way to access cookie is to use a [regular expression](info:regular-expressions).
|
||||||
|
|
||||||
|
The function `getCookie(name)` returns the cookie with the given `name`:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// returns the cookie with the given name,
|
||||||
|
// or undefined if not found
|
||||||
|
function getCookie(name) {
|
||||||
|
let matches = document.cookie.match(new RegExp(
|
||||||
|
"(?:^|; )" + name.replace(/([\.$?*|{}\(\)\[\]\\\/\+^])/g, '\\$1') + "=([^;]*)"
|
||||||
|
));
|
||||||
|
return matches ? decodeURIComponent(matches[1]) : undefined;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Here the regexp is generated dynamically, to match `; name=<value>`.
|
||||||
|
|
||||||
|
Please note that a cookie value can be an arbitrary string. If it contains characters that break the formatting, for instance spaces or `;`, such characters are encoded.
|
||||||
|
|
||||||
|
To decode them, we need to use a built-in `decodeURIComponent` function, the function does it also.
|
||||||
|
|
||||||
|
## Writing to document.cookie
|
||||||
|
|
||||||
|
The `document.cookie` is writable. But it's not a data property, but rather an accessor.
|
||||||
|
|
||||||
|
**A write operation to `document.cookie` passes through the browser that updates cookies mentioned in it, but doesn't touch other cookies.**
|
||||||
|
|
||||||
|
For instance, this call sets a cookie with the name `user` and value `John`:
|
||||||
|
|
||||||
|
```js run
|
||||||
|
document.cookie = "user=John";
|
||||||
|
alert(document.cookie);
|
||||||
|
```
|
||||||
|
|
||||||
|
If you run it, then probably you'll see multiple cookies. Only the cookie named `user` was altered.
|
||||||
|
|
||||||
|
Cookies have several options, many of them are important and should be set.
|
||||||
|
|
||||||
|
The options are listed after `key=value`, delimited by `;`, for instance:
|
||||||
|
|
||||||
|
```js run
|
||||||
|
document.cookie = "user=John; path=/; expires=Tue, 19 Jan 2038 03:14:07 GMT"
|
||||||
|
```
|
||||||
|
|
||||||
|
## path
|
||||||
|
|
||||||
|
- **`path=/mypath`**
|
||||||
|
|
||||||
|
The url path prefix, where the cookie is accessible. By default, it's the current path.
|
||||||
|
|
||||||
|
Usually, we should set `path=/` to make the cookie accessible from all website pages.
|
||||||
|
|
||||||
|
Please note: the path must be absolute.
|
||||||
|
|
||||||
|
## domain
|
||||||
|
|
||||||
|
- **`domain=site.com`**
|
||||||
|
|
||||||
|
Domain where the cookie is accessible.
|
||||||
|
|
||||||
|
By default, cookie is accessible only at the domain that set it. So, if we set a cookie at `site.com`, we won't get it at `forum.site.com`.
|
||||||
|
|
||||||
|
```js
|
||||||
|
// at site.com
|
||||||
|
document.cookie = "user=John"
|
||||||
|
|
||||||
|
// at forum.site.com
|
||||||
|
alert(document.cookie); // no user
|
||||||
|
```
|
||||||
|
|
||||||
|
If we'd like subdomains to access the cookie, we should set `domain=site.com` explicitly. For historical reasons, `domain=.site.com` (a dot at the start) also works this way:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// at site.com
|
||||||
|
document.cookie = "user=John; domain=site.com"
|
||||||
|
|
||||||
|
// at forum.site.com
|
||||||
|
alert(document.cookie); // with user
|
||||||
|
```
|
||||||
|
|
||||||
|
## expires, max-age
|
||||||
|
|
||||||
|
By default, a cookie disappears when the browser is closed. They are called "session cookies"
|
||||||
|
|
||||||
|
To change it, we must set either `expires` or `max-age` option.
|
||||||
|
|
||||||
|
- **`expires=Tue, 19 Jan 2038 03:14:07 GMT`**
|
||||||
|
|
||||||
|
Cookie expiration date (when it dies automatically).
|
||||||
|
|
||||||
|
The date must be exactly in this format, in GMT timezone. We can use `date.toUTCString` to get it. For instance, we can set the cookie to expire in 1 day:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// +1 day from now
|
||||||
|
let date = new Date(Date.now() + 86400e3);
|
||||||
|
date = date.toUTCString();
|
||||||
|
document.cookie = "user=John; expires=" + date;
|
||||||
|
```
|
||||||
|
|
||||||
|
If the date is in the past, the cookie will be deleted from the browser.
|
||||||
|
|
||||||
|
- **`max-age=3600`**
|
||||||
|
|
||||||
|
An alternative to `expires`, specifies the cookie expiration in seconds.
|
||||||
|
|
||||||
|
Can be either a number of seconds from the current moment, or `0` for immediate expiration (to remove the cookie):
|
||||||
|
|
||||||
|
```js
|
||||||
|
// cookie will die +1 hour from now
|
||||||
|
document.cookie = "user=John; max-age=3600";
|
||||||
|
|
||||||
|
// delete cookie (let it expiree right now)
|
||||||
|
document.cookie = "user=John; max-age=0";
|
||||||
|
```
|
||||||
|
|
||||||
|
## secure
|
||||||
|
|
||||||
|
- **`secure`**
|
||||||
|
|
||||||
|
The cookie should be transferred only over HTTPS.
|
||||||
|
|
||||||
|
That's important, by default if we set a cookie at `http://site.com`, then it also appears at `https://site.com` and vise versa.
|
||||||
|
|
||||||
|
With this option, if a cookie is set at `https://site.com`, the browser does not set it to `http://site.com`. So if a cookie has sensitive content, like authorization, then it's more secure.
|
||||||
|
|
||||||
|
```js
|
||||||
|
// set the cookie secure (only sent with HTTPS requests)
|
||||||
|
document.cookie = "user=John; secure";
|
||||||
|
```
|
||||||
|
|
||||||
|
## samesite
|
||||||
|
|
||||||
|
That's another security option, to protect from so-called XSRF (cross-site request forgery) attacks.
|
||||||
|
|
||||||
|
To understand when it's useful, let's introduce the following attack scenario.
|
||||||
|
|
||||||
|
### XSRF attack
|
||||||
|
|
||||||
|
Imagine, you are logged into the site `bank.com`. That is: you have an authentication cookie from that site. Your browser sends it to `bank.com` on every request, so that it recognizes you and performs all sensitive financial operations.
|
||||||
|
|
||||||
|
Now, while browsing the web in another window, you occasionally come to another site `evil.com`, that has a `<form action="https://bank.com/pay">` with parameters containing hacker's account and JavaScript code that sends it automatically.
|
||||||
|
|
||||||
|
The form is submitted to the bank site, and your cookie is also sent, just because it's sent every time you visit `bank.com`. So that the bank actually performs the payment.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
That's called a cross-site request forgery (or XSRF) attack.
|
||||||
|
|
||||||
|
Real banks are protected from it of course. All forms generated by `bank.com` have a special field, so called "xsrf protection token", that the evil page can't generate.
|
||||||
|
|
||||||
|
### Enter cookie samesite option
|
||||||
|
|
||||||
|
Now, cookie samesite option provides another way to protect from such attacks, that does not require "xsrf protection tokens".
|
||||||
|
|
||||||
|
It has two possible values:
|
||||||
|
|
||||||
|
- **`samesite=strict`, same as `samesite` without value**
|
||||||
|
|
||||||
|
A cookie with `samesite=strict` is never sent if the user comes from outside the site.
|
||||||
|
|
||||||
|
In other words, whether a user follows a link from the mail or submits a form from `evil.com`, the cookie is not sent. Then `bank.com` will not recognize the user, and will not proceed with the payment.
|
||||||
|
|
||||||
|
The protection is indeed reliable. Only operations originating from `bank.com` will send cookies. Although, it may be a little too strict.
|
||||||
|
|
||||||
|
When a user follows a legitimate link to `bank.com`, like from their own notes, they'll be surprised that `bank.com` does not recognize them. Indeed, `samesite=strict` cookies are nots sent in that case.
|
||||||
|
|
||||||
|
So there's another, more relaxed value.
|
||||||
|
|
||||||
|
- **`samesite=lax`**
|
||||||
|
|
||||||
|
Lax mode, just like `strict`, forbids the browser to send cookies when coming from outside the site, but adds an exception.
|
||||||
|
|
||||||
|
A `samesite=lax` cookie is sent if both of these conditions are true:
|
||||||
|
1. The HTTP method is "safe" (e.g. GET, but not POST).
|
||||||
|
|
||||||
|
The full list safe of HTTP methods is in the [RFC7231 specification](https://tools.ietf.org/html/rfc7231). Basically, these are the methods that should be used for reading, but not writing the data. They must not perform any data-changing operations. Following a link is always GET, the safe method.
|
||||||
|
|
||||||
|
2. The operation performs top-level navigation (changes URL in the browser address bar).
|
||||||
|
|
||||||
|
That's usually true, but if the navigation is performed in an `<iframe>`, then it's not top-level. Also, AJAX requests do not get in.
|
||||||
|
|
||||||
|
So, what `samesite=lax` does is basically allows a most common "open URL" operation to bring cookies. Something more complicated, like AJAX request from another site or a form submittion, passes through, but loses cookies by the way.
|
||||||
|
|
||||||
|
If that's fine for you, then adding `samesite=lax` will probably not break the user experience and add protection.
|
||||||
|
|
||||||
|
Overall, `samesite` is great, but it has an important drawback:
|
||||||
|
- `samesite` is not supported, ignored by old browsers (like year 2017).
|
||||||
|
|
||||||
|
So if we solely rely on `samesite` to provide protection, then old browsers will be totally vulnerable.
|
||||||
|
|
||||||
|
But we surely can use `samesite` together with other protection measures, like xsrf tokens, to add an additional layer of protection.
|
||||||
|
|
||||||
|
## setCookie(name, value, options)
|
||||||
|
|
||||||
|
If we gather all options together, here's a small function that sets the cookie `name` to the given `value` with reasonable default options:
|
||||||
|
Если собрать все настройки воедино, вот такая функция ставит куки:
|
||||||
|
|
||||||
|
```js
|
||||||
|
function setCookie(name, value, options = {}) {
|
||||||
|
|
||||||
|
options = {
|
||||||
|
path: '/',
|
||||||
|
// add other defaults here if necessary
|
||||||
|
...options
|
||||||
|
};
|
||||||
|
|
||||||
|
if (options.expires.toUTCString) {
|
||||||
|
options.expires = options.expires.toUTCString();
|
||||||
|
}
|
||||||
|
|
||||||
|
options.value = encodeURIComponent(options.value);
|
||||||
|
|
||||||
|
let updatedCookie = encodeURIComponent(name) + "=" + encodeURIComponent(value);
|
||||||
|
|
||||||
|
for (let optionKey in options) {
|
||||||
|
updatedCookie += "; " + optionKey;
|
||||||
|
let optionValue = options[optionKey];
|
||||||
|
if (optionValue !== true) {
|
||||||
|
updatedCookie += "=" + optionValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.cookie = updatedCookie;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Аргументы:
|
||||||
|
|
||||||
|
name
|
||||||
|
: название cookie
|
||||||
|
|
||||||
|
value
|
||||||
|
: значение cookie (строка)
|
||||||
|
|
||||||
|
options
|
||||||
|
: Объект с дополнительными свойствами для установки cookie:
|
||||||
|
|
||||||
|
expires
|
||||||
|
: Время истечения cookie. Интерпретируется по-разному, в зависимости от типа:
|
||||||
|
|
||||||
|
- Число -- количество секунд до истечения. Например, `expires: 3600` -- кука на час.
|
||||||
|
- Объект типа [Date](https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Global_Objects/Date) -- дата истечения.
|
||||||
|
- Если expires в прошлом, то cookie будет удалено.
|
||||||
|
- Если expires отсутствует или `0`, то cookie будет установлено как сессионное и исчезнет при закрытии браузера.
|
||||||
|
|
||||||
|
path
|
||||||
|
: Путь для cookie.
|
||||||
|
|
||||||
|
domain
|
||||||
|
: Домен для cookie.
|
||||||
|
|
||||||
|
secure
|
||||||
|
: Если `true`, то пересылать cookie только по защищенному соединению.
|
||||||
|
|
||||||
|
|
||||||
|
## Функция deleteCookie(name)
|
||||||
|
|
||||||
|
Здесь всё просто -- удаляем вызовом `setCookie` с датой в прошлом.
|
||||||
|
|
||||||
|
```js
|
||||||
|
function deleteCookie(name) {
|
||||||
|
setCookie(name, "", {
|
||||||
|
expires: -1
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Сторонние cookie
|
||||||
|
|
||||||
|
При работе с cookie есть важная тонкость, которая касается внешних ресурсов.
|
||||||
|
|
||||||
|
Теоретически, любой ресурс, который загружает браузер, может поставить cookie.
|
||||||
|
|
||||||
|
Например:
|
||||||
|
|
||||||
|
- Если на странице есть `<img src="http://mail.ru/counter.gif">`, то вместе с картинкой в ответ сервер может прислать заголовки, устанавливающие cookie.
|
||||||
|
- Если на странице есть `<iframe src="http://facebook.com/button.php">`, то во-первых сервер может вместе с `button.php` прислать cookie, а во-вторых JS-код внутри ифрейма может записать в `document.cookie`
|
||||||
|
|
||||||
|
При этом cookie будут принадлежать тому домену, который их поставил. То есть, на `mail.ru` для первого случая, и на `facebook.com` во втором.
|
||||||
|
|
||||||
|
**Такие cookie, которые не принадлежат основной странице, называются "сторонними" (3rd party) cookies. Не все браузеры их разрешают.**
|
||||||
|
|
||||||
|
Как правило, в настройках браузера можно поставить "Блокировать данные и файлы cookie сторонних сайтов" (Chrome).
|
||||||
|
|
||||||
|
**В Safari такая настройка включена по умолчанию и выглядит так:**
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Тс-с-с. Большой брат смотрит за тобой.
|
||||||
|
|
||||||
|
Цель этого запрета -- защитить посетителей от слежки со стороны рекламодателей, которые вместе с картинкой-баннером присылают и куки, таким образом помечая посетителей.
|
||||||
|
|
||||||
|
Например, на многих сайтах стоят баннеры и другая реклама Google Ads. При помощи таких cookie компания Google будет знать, какие именно сайты вы посещаете, сколько времени вы на них проводите и многое другое.
|
||||||
|
|
||||||
|
Как? Да очень просто -- на каждом сайте загружается, к примеру, картинка с рекламой. При этом баннер берётся с домена, принадлежащего Google. Вместе с баннером Google ставит cookie со специальным уникальным идентификатором.
|
||||||
|
|
||||||
|
Далее, при следующем запросе на баннер, браузер пошлёт стандартные заголовки, которые включают в себя:
|
||||||
|
|
||||||
|
- Cookie с домена баннера, то есть уникальный идентификатор, который был поставлен ранее.
|
||||||
|
- Стандартный заголовок Referrer (его не будет при HTTPS!), который говорит, с какого сайта сделан запрос. Да, впрочем, Google и так знает, с какого сайта запрос, ведь идентификатор сайта есть в URL.
|
||||||
|
|
||||||
|
Так что Google может хранить в своей базе, какие именно сайты из тех, на которых есть баннер Google, вы посещали, когда вы на них были, и т.п. Этот идентификатор легко привязывается к остальной информации от других сервисов, и таким образом картина слежки получается довольно-таки глобальной.
|
||||||
|
|
||||||
|
Здесь я не утверждаю, что в конкретной компании Google всё именно так... Но во-первых, сделать так легко, во-вторых идентификаторы действительно ставятся, а в-третьих, такие знания о человеке позволяют решать, какую именно рекламу и когда ему показать. А это основная доля доходов Google, благодаря которой корпорация существует.
|
||||||
|
|
||||||
|
Возможно, компания Apple, которая выпустила Safari, поставила такой флаг по умолчанию именно для уменьшения влияния Google?
|
||||||
|
|
||||||
|
### А если очень надо?
|
||||||
|
|
||||||
|
Итак, Safari запрещает сторонние cookie по умолчанию. Другие браузеры предоставляют такую возможность, если посетитель захочет.
|
||||||
|
|
||||||
|
**А что, если ну очень надо поставить стороннюю cookie, и чтобы это было надёжно?**
|
||||||
|
|
||||||
|
Такая задача действительно возникает, например, в системе кросс-доменной авторизации, когда есть несколько доменов 2-го уровня, и хочется, чтобы посетитель, который входит в один сайт, автоматически распознавался во всей сетке. При этом cookie для авторизации ставятся на главный домен -- "мастер", а остальные сайты запрашивают их при помощи специального скрипта (и, как правило, копируют к себе для оптимизации, но здесь это не суть).
|
||||||
|
|
||||||
|
Ещё пример -- когда есть внешний виджет, например, `iframe` с информационным сервисом, который можно подключать на разные сайты. И этот `iframe` должен знать что-то о посетителе, опять же, авторизация или какие-то настройки, которые хорошо бы хранить в cookie.
|
||||||
|
|
||||||
|
Есть несколько способов поставить 3rd-party cookie для Safari.
|
||||||
|
|
||||||
|
Использовать ифрейм.
|
||||||
|
: Ифрейм является полноценным окном браузера. В нём должна быть доступна вся функциональность, в том числе cookie. Как браузер решает, что ифрейм "сторонний" и нужно запретить для него и его скриптов установку cookie? Критерий таков: "в ифрейме нет навигации". Если навигация есть, то ифрейм признаётся полноценным окном.
|
||||||
|
|
||||||
|
Например, в сторонний `iframe` можно сделать POST. И тогда, в ответ на POST, сервер может поставить cookie. Или прислать документ, который это делает. Ифрейм, в который прошёл POST, считается родным и надёжным.
|
||||||
|
|
||||||
|
Popup-окно
|
||||||
|
: Другой вариант -- использовать popup, то есть при помощи `window.open` открывать именно окно со стороннего домена, и уже там ставить cookie. Это тоже работает.
|
||||||
|
|
||||||
|
Редирект
|
||||||
|
: Ещё одно альтернативное решение, которое подходит не везде - это сделать интеграцию со сторонним доменом, такую что на него можно сделать редирект, он ставит cookie и делает редирект обратно.
|
||||||
|
|
||||||
|
## Дополнительно
|
||||||
|
|
||||||
|
- На Cookie наложены ограничения:
|
||||||
|
- Имя и значение (после `encodeURIComponent`) вместе не должны превышать 4кб.
|
||||||
|
- Общее количество cookie на домен ограничено 30-50, в зависимости от браузера.
|
||||||
|
- Разные домены 2-го уровня полностью изолированы. Но в пределах доменов 3-го уровня куки можно ставить свободно с указанием `domain`.
|
||||||
|
- Сервер может поставить cookie с дополнительным флагом `HttpOnly`. Cookie с таким параметром передаётся только в заголовках, оно никак не доступно из JavaScript.
|
||||||
|
|
||||||
|
- Иногда посетители отключают cookie. Отловить это можно проверкой свойства [navigator.cookieEnabled](https://developer.mozilla.org/en-US/docs/DOM/window.navigator.cookieEnabled)
|
||||||
|
|
||||||
|
```js run
|
||||||
|
if (!navigator.cookieEnabled) {
|
||||||
|
alert( 'Включите cookie для комфортной работы с этим сайтом' );
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
...Конечно, предполагается, что включён JavaScript. Впрочем, посетитель без JS и cookie с большой вероятностью не человек, а бот.
|
||||||
|
|
||||||
|
## Итого
|
||||||
|
|
||||||
|
Файл с функциями для работы с cookie: [cookie.js](cookie.js).
|
BIN
2-ui/5-data-storage/02-localstorage/cookie-xsrf.png
Normal file
After Width: | Height: | Size: 21 KiB |
BIN
2-ui/5-data-storage/02-localstorage/cookie-xsrf@2x.png
Normal file
After Width: | Height: | Size: 50 KiB |
45
2-ui/5-data-storage/02-localstorage/cookie.js
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
// возвращает cookie с именем name, если есть, если нет, то undefined
|
||||||
|
function getCookie(name) {
|
||||||
|
var matches = document.cookie.match(new RegExp(
|
||||||
|
"(?:^|; )" + name.replace(/([\.$?*|{}\(\)\[\]\\\/\+^])/g, '\\$1') + "=([^;]*)"
|
||||||
|
));
|
||||||
|
return matches ? decodeURIComponent(matches[1]) : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// устанавливает cookie с именем name и значением value
|
||||||
|
// options - объект с свойствами cookie (expires, path, domain, secure)
|
||||||
|
function setCookie(name, value, options) {
|
||||||
|
options = options || {};
|
||||||
|
|
||||||
|
var expires = options.expires;
|
||||||
|
|
||||||
|
if (typeof expires == "number" && expires) {
|
||||||
|
var d = new Date();
|
||||||
|
d.setTime(d.getTime() + expires * 1000);
|
||||||
|
expires = options.expires = d;
|
||||||
|
}
|
||||||
|
if (expires && expires.toUTCString) {
|
||||||
|
options.expires = expires.toUTCString();
|
||||||
|
}
|
||||||
|
|
||||||
|
value = encodeURIComponent(value);
|
||||||
|
|
||||||
|
var updatedCookie = name + "=" + value;
|
||||||
|
|
||||||
|
for (var propName in options) {
|
||||||
|
updatedCookie += "; " + propName;
|
||||||
|
var propValue = options[propName];
|
||||||
|
if (propValue !== true) {
|
||||||
|
updatedCookie += "=" + propValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
document.cookie = updatedCookie;
|
||||||
|
}
|
||||||
|
|
||||||
|
// удаляет cookie с именем name
|
||||||
|
function deleteCookie(name) {
|
||||||
|
setCookie(name, "", {
|
||||||
|
expires: -1
|
||||||
|
})
|
||||||
|
}
|
BIN
2-ui/5-data-storage/02-localstorage/safari-nocookie.png
Normal file
After Width: | Height: | Size: 27 KiB |
BIN
2-ui/5-data-storage/02-localstorage/safari-nocookie@2x.png
Normal file
After Width: | Height: | Size: 71 KiB |
2
2-ui/5-data-storage/index.md
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
|
||||||
|
# Storing data in the browser
|