登录凭证

登录成功返回凭证: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
1
2
npm init -y
npm install koa koa-router
  1. 安装JWT库:jsonwebtoken
1
npm i jsonwebtoken
  1. 引入jwt,调用jwt.sign(用户信息,秘钥,{过期时间:xx秒})方法获取token
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是否正确/过期;
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. 生成私钥
1
2
3
// 随便新建一个文件夹 git bash here
openssl
genrsa -out private.key

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

Koa颁发与验证Token

  1. 读取上面生成的私钥与公钥,作为加密与解密的key
  2. 这里不是默认的SHA256,所以加密和解密的时候我们要指定我们的加密算法;(RS256)
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、单点登录