今天来学习下常见的鉴权方式,从最开始了解的最简单的 Cookie 验证,到后来的 Session-cookie、Token 等,整体来说,安全性越来越高。当然还是对部分方式了解不是很深,这里做个简单总结,查漏补缺。

目前我们常用的鉴权有四种:

  1. HTTP Basic Authentication
  2. Session-cookie
  3. Token 验证
  4. OAuth(开放授权)

HTTP Basic Authentication

这种授权方式是浏览器遵守 HTTP 协议实现的基本授权方式,HTTP 协议进行通信的过程中,HTTP 协议定义了基本认证认证允许 HTTP 服务器对客户端进行用户身份证的方法。

认证过程

  • 1️⃣ 客户端向服务器请求数据,请求的内容可能是一个网页或者是一个 ajax 异步请求,此时,假设客户端尚未被验证,则客户端提供如下请求至服务器:
1
2
Get /index.html HTTP/1.0
Host:www.google.com
  • 2️⃣ 服务器向客户端发送验证请求代码 401,(WWW-Authenticate: Basic realm="google.com" 这句话是关键,如果没有客户端不会弹出用户名和密码输入界面)服务器返回的数据如下:
1
2
3
4
5
HTTP/1.0 401 Unauthorised
Server: SokEvo/1.0
WWW-Authenticate: Basic realm="google.com"
Content-Type: text/html
Content-Length: xxx
  • 3️⃣ 当符合 http1.0 或 1.1 规范的客户端(如 IE,FIREFOX)收到 401 返回值时,将自动弹出一个登录窗口,要求用户输入用户名和密码。

  • 4️⃣ 用户输入用户名和密码后,将用户名及密码以 BASE64 加密方式加密,并将密文放入前一条请求信息中,则客户端发送的第一条请求信息则变成如下内容:

1
2
3
Get /index.html HTTP/1.0
Host:www.google.com
Authorization: Basic d2FuZzp3YW5n

d2FuZzp3YW5n 表示加密后的用户名及密码(用户名:密码 然后通过 base64 加密,加密过程是浏览器默认的行为,不需要我们人为加密,我们只需要输入用户名密码即可)
  • 5️⃣ 服务器收到上述请求信息后,将 Authorization 字段后的用户信息取出、解密,将解密后的用户名及密码与用户数据库进行比较验证,如用户名及密码正确,服务器则根据请求,将所请求资源发送给客户端。

客户端未未认证的时候,会弹出用户名密码输入框,这个时候请求时属于 pending 状态,这个时候其实服务当用户输入用户名密码的时候客户端会再次发送带 Authentication 头的请求。

请求认证 👇
image

认证成功 👇
image

完整代码

Server.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
let express = require('express');
let app = express();

app.use(express.static(__dirname + '/public'));
app.get('/Authentication_base', function(req, res) {
console.log('req.headers.authorization:', req.headers);
if (!req.headers.authorization) {
res.set({
'WWW-Authenticate': 'Basic realm="wang"'
});
res.status(401).end();
} else {
let base64 = req.headers.authorization.split(' ')[1];
let userPass = new Buffer(base64, 'base64').toString().split(':');
let user = userPass[0];
let pass = userPass[1];
if (user === 'wang' && pass === 'wang') {
res.end('OK');
} else {
res.status(401).end();
}
}
});

app.listen(9090);

index.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<title>HTTP Basic Authentication</title>
</head>
<body>
<div></div>
<script src="js/jquery-3.2.1.js"></script>
<script>
$(function() {
send('./Authentication_base');
});
var send = function(url) {
$.ajax({
url: url,
method: 'GET'
});
};
</script>
</body>
</html>

当然有登陆就有注销,我们会发现当我们认证成功后每次请求请求头都会带上 Authentication 及里面的内容,那么如何做到让这次登陆失效的?

网上查了半天,目前最有效的方式就是在注销操作的时候,专门在服务器设置一个专门的注销账号,当接收到的 Authentication 信息为注销用户名密码的时候纠就带便注销成功了,而客户端在注销操作的时候,手动的的去修改请求头重的 Authentication,将他设置未服务器默认的注销账号和密码。

通过上面的简单讲解 其实我们已经可以返现这种验证方式的缺陷加密方式简单,仅仅是 base64 加密,这种加密方式是可逆的。同时在每个请求的头上都会附带上用户名和密码信息,这样在外网是很容易被嗅探器探测到的。

正式因为这样,这种加密方式一般多被用在内部安全性要求不高的的系统上,只是相对的多,总的来说现在使用这种鉴权比较少了。如果项目需要部署在公网上,这种方式不推荐,当然你也可以和 SSL 来加密传输,这样会好一点,这个如果我后面有时间来研究一下。

第二种这个方式是利用服务器端的 session(会话)和浏览器端的 cookie 来实现前后端的认证,由于 http 请求时是无状态的,服务器正常情况下是不知道当前请求之前有没有来过,这个时候我们如果要记录状态,就需要在服务器端创建一个会话 (seesion), 将同一个客户端的请求都维护在各自得会会话中,每当请求到达服务器端的时候,先去查一下该客户端有没有在服务器端创建 seesion,如果有则已经认证成功了,否则就没有认证。

认证过程

  1. 服务器在接受客户端首次访问时在服务器端创建 seesion,然后保存 seesion(我们可以将 seesion 保存在内存中,也可以保存在 redis 中,推荐使用后者),然后给这个 session 生成一个唯一的标识字符串, 然后在响应头中种下这个唯一标识字符串。
  2. 签名。这一步只是对 sid 进行加密处理,服务端会根据这个 secret 密钥进行解密。(非必需步骤)
  3. 浏览器中收到请求响应的时候会解析响应头,然后将 sid 保存在本地 cookie 中,浏览器在下次 http 请求的请求头中会带上该域名下的 cookie 信息。
  4. 服务器在接受客户端请求时会去解析请求头 cookie 中的 sid,然后根据这个 sid 去找服务器端保存的该客户端的 session,然后判断该请求是否合法。

完整代码

server.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
var express = require('express');
var RedisStore = require('connect-redis')(express.session);
var app = express();
var secret = 'ifyour';
// 设置 Cookie
app.use(express.cookieParser(secret));

// 设置 Session
app.use(
express.session({
store: new RedisStore({
host: '127.0.0.1',
port: 6379,
db: 'session_db'
}),
secret: secret
})
);

app.get('/', function(req, res) {
var session = req.session;
session.time = session.time || 0;
var n = session.time++;
res.send('hello, session id:' + session.id + 'count:' + n);
});

app.listen(9080);

Token 验证

认证过程

  1. 客户端使用用户名跟密码请求登录
  2. 服务端收到请求,去验证用户名与密码
  3. 验证成功后,服务端会签发一个 Token,再把这个 Token 发送给客户端
  4. 客户端收到 Token 以后可以把它存储起来,比如放在 Cookie 里或者 LocalStorage 里
  5. 客户端每次向服务端请求资源的时候需要带着服务端签发的 Token
  6. 服务端收到请求,然后去验证客户端请求里面带着的 Token,如果验证成功,就向客户端返回请求的数据

总的来说就是客户端在首次登陆以后,服务端再次接收 http 请求的时候,就只认 token 了,请求只要每次把 token 带上就行了,服务器端会拦截所有的请求,然后校验 token 的合法性,合法就放行,不合法就返回 401(鉴权失败)。

乍的一看好像和前面的 seesion-cookie 有点像,seesion-cookie 是通过 seesionid 来作为浏览器和服务端的链接桥梁,而 token 验证方式貌似是 token 来起到 seesionid 的角色。其实这两者差别是很大的。

  1. sessionid 是一个唯一标识的字符串,服务端是根据这个字符串,来查询在服务器端保持的 seesion,这里面才保存着用户的登陆状态。但是 token 本身就是一种登陆成功凭证,他是在登陆成功后根据某种规则生成的一种信息凭证,他里面本身就保存着用户的登陆状态。服务器端只需要根据定义的规则校验这个 token 是否合法就行。
  2. session-cookie 是需要 cookie 配合的,居然要 cookie,那么在 http 代理客户端的选择上就是只有浏览器了,因为只有浏览器才会去解析请求响应头里面的 cookie, 然后每次请求再默认带上该域名下的 cookie。但是我们知道 http 代理客户端不只有浏览器,还有原生 APP 等等,这个时候 cookie 是不起作用的,或者浏览器端是可以禁止 cookie 的 (虽然可以,但是这基本上是属于吃饱没事干的人干的事),但是 token 就不一样,他是登陆请求在登陆成功后再请求响应体中返回的信息,客户端在收到响应的时候,可以把他存在本地的 cookie、storage,或者内存中,然后再下一次请求的请求头重带上这个 token 就行了。简单点来说 cookie-session 机制他限制了客户端的类型,而 token 验证机制丰富了客户端类型。
  3. 时效性。session-cookie 的 sessionid 是在登陆时生成的,而且在登出时一直不变,在一定程度上安全性就会降低,而 token 是可以在一段时间内动态改变的。
  4. 可扩展性。token 验证本身是比较灵活的,一是 token 的解决方案有许多,常用的是 JWT,二来我们可以基于 token 验证机制,专门做一个鉴权服务,用它向多个服务的请求进行统一鉴权。

下面就拿最常用的 JWT(JSON WEB TOKEN)来说:

JWT 是 Auth0 提出的通过对 JSON 进行加密签名来实现授权验证的方案,就是登陆成功后将相关信息组成 json 对象,然后对这个对象进行某中方式的加密,返回给客户端,客户端在下次请求时带上这个 token,服务端再收到请求时校验 token 合法性,其实也就是在校验请求的合法性。JWT 对象通常由三部分构成:

1️⃣ Headers: 包括类别(typ)、加密算法(alg)

1
2
3
4
{
"alg": "HS256",
"typ": "JWT"
}

2️⃣ Claims:包括需要传递的用户信息

1
2
3
4
5
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}

3️⃣ Signature:根据 alg 算法与私有秘钥进行加密得到的签名字串,这一段是最重要的敏感信息,只能在服务端解密

1
2
3
4
HMACSHA256(
base64UrlEncode(Headers) + '.' + base64UrlEncode(Claims),
SECREATE_KEY
);

编码之后的 JWT 看起来是这样的一串字符:

1
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

完整代码

auth.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
let jwt = require('jwt-simple');
let secret = 'wangyy';
let time = 10;
module.exports = {
/* 检验 token 合法性 */
validate: function(req, res, next) {
let token = req.body.token || req.headers['xssToken'];
if (token) {
let decodeToken = null;
try {
// 防止假冒 token 解析报错
decodeToken = jwt.decode(token, secret, 'HS256');
} catch (err) {
res.status(401).send('非法访问');
return;
}
let exp = decodeToken.exp;
if (!exp) {
res.status(401).send('非法访问');
}
let now = new Date().getTime();
if (exp > now + time * 60 * 1000) {
res.send({ code: '002', errorMsg: '授权超时' });
}
next();
} else {
res.status(401).send('非法访问');
}
},
/* 生成 token*/
makeToken() {
let Token = null;
let payload = {
time: new Date().getTime(),
exp: this.makeExp(time)
};
Token = jwt.encode(payload, secret, HS256);
return Token;
},
/* 生成 token 过期时间 */
makeExp: function(time) {
let stam = time601000;
}
};

Server.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let express = require('express');
let app = express();
let bodyParser = require('body-parser');
let auth = require('./lib/auth.js');
let chalk = require('chalk');

app.use(bodyParser.json());
app.post('/login', function(req, res, next) {
let Token = auth.makeToken();
res.json({ result: 'success', token: Token }, 200);
});
app.use('*', [auth.validate], function(req, res, next) {
res.send('success');
});

app.listen('9999');

OAuth(开放授权)

OAuth(开放授权)是一个开放标准,允许用户授权第三方网站访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方网站或分享他们数据的所有内容,为了保护用户数据的安全和隐私,第三方网站访问用户数据前都需要显式的向用户征求授权。我们常见的提供 OAuth 认证服务的厂商有支付宝、QQ、微信。

OAuth 协议又有 1.0 和 2.0 两个版本。相比较 1.0 版,2.0 版整个授权验证流程更简单更安全,也是目前最主要的用户身份验证和授权方式。

认证过程

下面是一张 Auth2.0 的流程图:

image

从图中我们可以看出,Auth2.0 流程分为六步:

1️⃣ 向用户请求授权,现在很多的网站在登陆的时候都有第三方登陆的入口,当我们点击等第三方入口时,第三方授权服务会引导我们进入第三方登陆授权页面。

image

通过第三方请求授权页面的浏览器地址栏地址可以看出:

1
https://graph.qq.com/oauth2.0/show?which=Login&display=pc&response_type=code&client_id=100270989&redirect_uri=https%3A%2F%2Fpassport.csdn.net%2Faccount%2Flogin%3Foauth_provider%3DQQProvider&state=test

这里的地址里面的 % 是浏览器强制编码后的显示我们可以使用 decodeURIComponent 进行解码,解码后是这样:

1
https://graph.qq.com/oauth2.0/show?which=Login&display=pc&response_type=code&client_id=100270989&redirect_uri=https://passport.csdn.net/account/login?oauth_provider=QQProvider&state=test

这个 url 地址我们可以看见 Auth2.0 常见的几个参数:

1
2
3
4
5
response_type:  返回类型
client_id: 第三方应用 id, 由授权服务器(qq)在第三方应用提交时颁发给第三方应用。
redirect_uri: 登陆成功重定向页面
oauth_provider: 第三方授权提供方
state: 由第三方应用给出的随机码

2️⃣ 返回用户凭证(code),并返回一个凭证(code),当用户点击授权并登陆后,授权服务器将生成一个用户凭证(code)。这个用户凭证会附加在重定向的地址 redirect_uri 的后面

1
https://passport.csdn.net/account/login?code=9e3efa6cea739f9aaab2&state=XXX

3️⃣ 请求授权服务器授权

经过第二部获取 code 后后面的工作就可以交给后台去处理的,和用户的交互就结束了。接下来我的需要获取凭证(Access Token),我们需要用他来向授权服务器获取用户信息等资源。 第三方应用后台通过第二步的凭证(code)向授权服务器请求凭证(Access Token),这时候需要以下几个信息:

  • client_id: 标识第三方应用的 id,由授权服务器在第三方应用提交时颁发给第三方应用
  • client_secret: 第三方应用和授权服务器之间的安全凭证,由授权服务器在第三方应用提交时颁发给第三方应用
  • code: 第一步中返回的用户凭证 redirect_uri 第一步生成用户凭证后跳转到第二步时的地址
  • state: 由第三方应用给出的随机码

4️⃣ 授权服务器同意授权后,返回一个资源访问的凭证(Access Token)。

5️⃣ 第三方应用通过第四步的凭证(Access Token)向资源服务器请求相关资源。

6️⃣ 资源服务器验证凭证(Access Token)通过后,将第三方应用请求的资源返回。

从用户角度来说,第三方授权可以让我们快速的登陆应用,无需进行繁琐的注册,同时不用记住各种账号密码。只需要记住自己常用的几个账号就 OK 了。 从产品经理的角度来所,这种授权方式提高用户的体验满意度。另一方面可以获取更多的用户。

总结

授权方式多种多样,主要还是要取决于我们对于产品的定位。如果我们的产品只是在企业内部使用,Token 和 Session-cookie 就可以满足我们的需求,如果是面向互联网的大众用户,那么第三方授权 OAuth 在用户体验度上会有一个很大的提升。