登录凭证

登录成功返回凭证:cookie+session 或者是 Token令牌;现在基本都是 Token令牌作为登录凭证;

为什么需要登录凭证呢?

web 开发中,我们使用最多的协议是 http,但是 http 是一个无状态的协议。那什么叫做无状态协议呢?

  • 举个例子:
    • 我们登录了一个网站 www.fsllala.top;
    • 登录的时候我们需要输入用户名和密码:比如用户名 forward,密码:Forward666;
    • 登录成功之后,我们要以 forward 的身份去访问其他的数据和资源,还是通过 http 请求去访问。
      • fsllala 的服务器会问:你谁呀?
      • forward 说:我是 forward 呀,刚刚登录过呀;
      • fsllala:怎么证明你刚刚登录过呀?
      • forward 说:这。。。,http 没有告诉你吗?
      • fsllala:http 的每次请求对我来说都是一个单独的请求,和之前请求过什么没有关系。

cookie

cookie

看到了吧?这就是 http 的无状态,也就是服务器不知道你上一步做了什么,我们必须得有一个办法可以证明我们登录过;

  • 那如何证明刚才就是我登录的啊?
    • 登陆成功的时候服务器给我们发过来一个登录凭证,访问其他资源的时候,通过这个登录凭证来证明刚才就是我登录的,而且我能访问哪些资源,不能访问哪些资源,都可以通过登录凭证来决定;

cookie

  • 目前登录凭证有两种:
    • cookie+session(逐渐淘汰)
    • Token 令牌
  1. 在提供标记的接口,通过 HTTP 返回头的 Set-Cookie 字段,直接「种」到浏览器上;
  2. 浏览器发起请求时,会自动把 cookie 通过 HTTP 请求头的 Cookie 字段,带给接口;

配置

  1. Domain / Path

你不能拿清华的校园卡进北大。

​ cookie 是要限制::「空间范围」:: 的,通过 Domain(域)/ Path(路径)两级。

Domain 属性指定浏览器发出 HTTP 请求时,哪些域名要附带这个 Cookie。如果没有指定该属性,浏览器会默认将其设为当前 URL 的一级域名,比如 www.example.com 会设为 example.com,而且以后如果访问 example.com 的任何子域名,HTTP 请求也会带上这个 Cookie。如果服务器在 Set-Cookie 字段指定的域名,不属于当前域名,浏览器会拒绝这个 Cookie。Path 属性指定浏览器发出 HTTP 请求时,哪些路径要附带这个 Cookie。只要浏览器发现,Path 属性是 HTTP 请求路径的开头一部分,就会在头信息里面带上这个 Cookie。比如,PATH 属性是 /,那么请求 /docs 路径也会包含该 Cookie。当然,前提是域名必须一致。—— Cookie — JavaScript 标准参考教程(alpha)

  1. Expires / Max-Age

    你毕业了卡就不好使了。

    cookie 还可以限制::「时间范围」::,通过 Expires、Max-Age 中的一种。

    Expires 属性指定一个具体的到期时间,到了指定时间以后,浏览器就不再保留这个 Cookie。它的值是 UTC 格式。如果不设置该属性,或者设为 null,Cookie 只在当前会话(session)有效,浏览器窗口一旦关闭,当前 Session 结束,该 Cookie 就会被删除。另外,浏览器根据本地时间,决定 Cookie 是否过期,由于本地时间是不精确的,所以没有办法保证 Cookie 一定会在服务器指定的时间过期。Max-Age 属性指定从现在开始 Cookie 存在的秒数,比如 60 * 60 * 24 * 365(即一年)。过了这个时间以后,浏览器就不再保留这个 Cookie。如果同时指定了 Expires 和 Max-Age,那么 Max-Age 的值将优先生效。如果 Set-Cookie 字段没有指定 Expires 或 Max-Age 属性,那么这个 Cookie 就是 Session Cookie,即它只在本次对话存在,一旦用户关闭浏览器,浏览器就不会再保留这个 Cookie。—— Cookie — JavaScript 标准参考教程(alpha)

  2. Secure / HttpOnly

    有的学校规定,不带卡套不让刷(什么奇葩学校,假设);有的学校不让自己给卡贴贴纸。

    cookie 可以限制::「使用方式」::。

    Secure 属性指定浏览器只有在加密协议 HTTPS 下,才能将这个 Cookie 发送到服务器。另一方面,如果当前协议是 HTTP,浏览器会自动忽略服务器发来的 Secure 属性。该属性只是一个开关,不需要指定值。如果通信是 HTTPS 协议,该开关自动打开。HttpOnly 属性指定该 Cookie 无法通过 JavaScript 脚本拿到,主要是 Document.cookie 属性、XMLHttpRequest 对象和 Request API 都拿不到该属性。这样就防止了该 Cookie 被脚本读到,只有浏览器发出 HTTP 请求时,才会带上该 Cookie。—— Cookie — JavaScript 标准参考教程(alpha)

session

Session 是另一种记录客户状态的机制,不同的是 Cookie 保存在客户端浏览器中,而 Session 保存在服务器上。客户端浏览器访问服务器的时候,服务器把客户端信息以某种形式记录在服务器上。这就是 Session。客户端浏览器再次访问时只需要从该 Session 中查找该客户的状态就可以了。所以 cookie 不如 session 安全
session 基于 cookie,不能在客户端设置,只能在服务端设置;

cookie 和 session 缺点

  • Cookie 会被附加在每个 HTTP 请求中,所以无形中增加了流量(事实上某些请求是不需要的);
  • Cookie 是明文传递的,所以存在安全性的问题;
  • Cookie 的大小限制是 4KB,对于复杂的需求来说是不够的;
  • 对于浏览器外的其他客户端(比如 iOS、Android),必须手动的设置 cookie 和 session;(浏览器自动就带上了)
  • 对于分布式系统和服务器集群中如何可以保证其他系统也可以正确的解析 session?

Token

Token 是什么?

在目前的前后端分离的开发过程中,使用 token 来进行身份验证的是最多的情况。

  • token 可以翻译为令牌;
  • 也就是在验证了用户账号和密码正确的情况,给用户颁发一个令牌;
  • 这个令牌作为后续用户访问一些接口或者资源的凭证;
  • 我们可以根据这个凭证来判断用户是否有权限来访问;

所以 token 的使用应该分成两个重要的步骤:

  • 生成 token:登录的时候,颁发 token;
  • 验证 token:访问某些资源或者接口时,验证 token;

JWT 实现 Token 机制

JWT 生成的 Token 由三部分组成:

  1. header
  2. payload
  3. signature
  • header
    • alg:采用的加密算法,默认是 HMAC SHA256(HS256),采用同一个密钥进行 加密和解密;(对称加密)
    • typ:JWT,固定值,通常都写成 JWT 即可;
    • 会通过 base64Url 算法进行编码;
  • payload
    • 携带的数据,比如我们可以将用户的 id 和 name 放到 payload 中;
    • 默认也会携带 iat(issued at),令牌的签发时间;
    • 我们也可以设置过期时间:exp(expiration time);
    • 会通过 base64Url 算法进行编码;
  • signature
    • 设置一个 secretKey,通过将前两个的结果合并后进行 HMACSHA256 的算法;
    • HMACSHA256(base64Url(header)+.+base64Url(payload), secretKey);
    • 但是如果 secretKey 暴露是一件非常危险的事情,因为之后就可以模拟颁发 token, 也可以解密 token;(因为 SHA256 是对称加密,就一个秘钥;采用非对称加密更安全);

即:header 生成一个字符串,payload 生成一个字符串,signature 将前面两个结合在加密生成一个字符串,中间都用点连接:

即:token:header.payload.signature

token

Koa 颁发与验证 Token

  1. 安装 Koa
js
1
2
npm init -y
npm install koa koa-router
  1. 安装 JWT 库:jsonwebtoken
js
1
npm i jsonwebtoken
  1. 引入 jwt,调用 jwt.sign(用户信息,秘钥,{过期时间:xx秒}) 方法获取 token
js
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
27
28
29
30
31
32
33
34
35
36
const Koa = require("koa");
const Router = require("koa-router");

// 1. 引入jwt
const jwt = require("jsonwebtoken");

const app = new Koa();
const tokenRouter = new Router();

// 2. 设置私钥
const SECRECT_KEY = "abccba123";

// 登录接口
tokenRouter.post("/login", (ctx, next) => {
// 3.
/**
* 因为header部分没有用户数据,所以jwt默认给我们设置好了;
* payload中的令牌的签发时间,jwt默认设置了,请求接口的时间为签发时间;
* jwt.sign(用户信息,秘钥,{过期时间:xx秒})
*/
const user = { id: 110, name: "fsllala" };
const token = jwt.sign(user, SECRECT_KEY, {
expiresIn: 10,
});
ctx.body = token;
});

// 验证接口
tokenRouter.get("/verfify", (ctx, next) => {});

app.use(tokenRouter.routes());
app.use(tokenRouter.allowedMethods());

app.listen(3000, () => {
console.log(`server is running port is 3000`);
});
  1. 使用 postman 验证

  1. 获取 headers 中的 token,通过 jwt.verify(token,key) 验证 token 是否正确 / 过期;
js
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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
const Koa = require("koa");
const Router = require("koa-router");

// 1. 引入jwt
const jwt = require("jsonwebtoken");


const app = new Koa();
const tokenRouter = new Router();

// 2. 设置私钥
const SECRECT_KEY = "abccba123";

// 登录接口
tokenRouter.post("/login", (ctx, next) => {
// 3.
/**
* 因为header部分没有用户数据,所以jwt默认给我们设置好了;
* payload中的令牌的签发时间,jwt默认设置了,请求接口的时间为签发时间;
* jwt.sign(用户信息,秘钥,{过期时间:xx秒})
*/
const user = {
id: 110,
name: "fsllala"
};
const token = jwt.sign(user, SECRECT_KEY, {
expiresIn: 10,
})
ctx.body = token;
});

// 验证接口
tokenRouter.get("/verfify", (ctx, next) => {
// 4.需要拿到授权的token
// console.log(ctx.headers.authorization);
// Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MTEwLCJuYW1lIjoiZnNsbGFsYSIsImlhdCI6MTY3MzUyNDgwNiwiZXhwIjoxNjczNTI0ODE2fQ.EksolC0I72LPxH1lT3h-gm-cY10Q0eC2SaINKGCg77I
const authorization = ctx.headers.authorization;
// 5.此时的token多一个"Bearer ",去掉;
const token = authorization.replace("Bearer ", "");
// 6.认证token 通过 jwt.verify(token,key);
try {
const result = jwt.verify(token, SECRECT_KEY);
ctx.body = result;
} catch (error) {
ctx.body="无效的token~"
}

});


app.use(tokenRouter.routes());
app.use(tokenRouter.allowedMethods());

app.listen(3000, () => {
console.log(`server is running port is 3000`);
})
  1. 使用 postman 验证

token

上面案例中,使用的是 SHA256 对称加密来颁发与验证签名;秘钥一旦泄露,就会很危险;即使不泄露,颁发和验证的秘钥是一样的,在我验证 token 的时候,我就可以颁发 token,这样好多好多人都可以颁发 token

非对称加密

使用 privateKey 进行加密 (颁发),使用 publicKey 进行解密 (验证);这样 publicKey 是不能进行颁发 token 的;这样会更安全;

  • 前面我们说过,HS256 加密算法一单密钥暴露就是非常危险的事情:

    • 比如在分布式系统中,每一个子系统都需要获取到密钥;
    • 那么拿到这个密钥后这个子系统既可以发布另外,也可以验证令牌;
    • 但是对于一些资源服务器来说,它们只需要有验证令牌的能力就可以了;
  • 这个时候我们可以使用非对称加密,RS256:

    • 私钥(private key):用于发布令牌;
    • 公钥(public key):用于验证令牌;
  • 我们可以使用 openssl 来生成一对私钥和公钥:

    • 通过私钥生成公钥;(因为需要用公钥来解析私钥)

    • Mac 直接使用 terminal 终端即可;

    • Windows 默认的 cmd 终端是不能直接使用的,建议直接使用 git bash 终端;

生成私钥和公钥

  1. 生成私钥
js
1
2
3
// 随便新建一个文件夹 git bash here
openssl
genrsa -out private.key

  1. 根据私钥生成公钥
js
1
rsa -in private.key -pubout -out public.key

Koa 颁发与验证 Token

  1. 读取上面生成的私钥与公钥,作为加密与解密的 key
  2. 这里不是默认的 SHA256,所以加密和解密的时候我们要指定我们的加密算法;(RS256)
js
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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
const Koa = require("koa");
const Router = require("koa-router");

// 1. 引入jwt fs path
const jwt = require("jsonwebtoken");
const fs = require("fs");
const path = require("path");

const app = new Koa();
const tokenRouter = new Router();

// 2. 设置私钥
const PRIVATE_KEY = fs.readFileSync(path.resolve(__dirname, "./keys/private.key"));
const PUBLIC_KEY = fs.readFileSync(path.resolve(__dirname, "keys/public.key"));

// 登录接口
tokenRouter.post("/login", (ctx, next) => {
const user = {
id: 110,
name: "fsllala"
};
// 3.使用私钥加密,这里不是SHA256,所以我们要指定我们的加密算法;(RS256)
const token = jwt.sign(user, PRIVATE_KEY, {
expiresIn: 10,
algorithm: "RS256"
})
ctx.body = token;
});

// 验证接口
tokenRouter.get("/verfify", (ctx, next) => {
// 4.拿到授权的token
// console.log(ctx.headers.authorization);
// Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MTEwLCJuYW1lIjoiZnNsbGFsYSIsImlhdCI6MTY3MzUyNDgwNiwiZXhwIjoxNjczNTI0ODE2fQ.EksolC0I72LPxH1lT3h-gm-cY10Q0eC2SaINKGCg77I
const authorization = ctx.headers.authorization;
// 5.此时的token多一个"Bearer ",去掉;
const token = authorization.replace("Bearer ", "");
// 6.认证token 通过 jwt.verify(token,key); 使用公钥进行解密,并告诉其加密算法;
try {
const result = jwt.verify(token, PUBLIC_KEY, {
algorithms: ["RS256"] //这里algorithms是个复数,所以需要传入数组;
});
ctx.body = result;
} catch (error) {
ctx.body = "无效的token~"
}

});


app.use(tokenRouter.routes());
app.use(tokenRouter.allowedMethods());

app.listen(3000, () => {
console.log(`server is running port is 3000`);
})
  1. 使用 postman 验证

token

参考文献

关于 cookie

Cookie 和 Session 详解

前端鉴权必须了解的 5 个兄弟:cookie、session、token、jwt、单点登录