---------------------------------------------------------------------------------------------单点登陆原理-----------------------------------------------------------------------------------------------------------------------------
1:http无状态协议:
web采用客户端-服务端架构,http做为通信协议,浏览器的每一次请求,服务器会独自处理,
不与之间之后的请求产生关联。
三次请求/响应之间没有任何关系:
浏览器 客户端
------------------------
| |
-------第(1)次请求-------->
| |
<------第(1)次响应--------
| |
| |
| |
-------第(2)次请求-------->
| |
<------第(3)次响应---------
| |
| |
| |
-------第(2)次请求-------->
| |
<------第(3)次响应---------
| |
| |
------------------------
但这也同时意味着,任何用户都能通过浏览器访问服务器资源,如果想保护服务器的某些资源,
必须限制浏览器请求;要限制浏览器请求,必须鉴别浏览器请求,响应合法请求,忽略非法请求;
要鉴别浏览器请求,必须清楚浏览器请求状态。
既然http协议无状态,那就让服务器和浏览器共同维护一个状态吧!这就是会话机制
2: 会话机制:
浏览器第一次请求服务器,服务器创建一个会话,并将会话的id作为响应的一部分发送给浏览器,
浏览器存储会话id,并在后续第二次和第三次请求中带上会话id,
服务器取得请求中的会话id就知道是不是同一个用户了,这个过程用下图说明,后续请求与第一次请求产生了关联:
浏览器 客户端
------------------------
| |
-------第(1)次请求-------->---------------[创建会话]
| | |
| |<------------------------|
| |
| |
<------第(1)次响应------------->(会话id)
| |
|-------------| |
| 保存会话(id)| |
| <-------| |
| |
-------第(2)次请求-------------->(会话id)
| |
<------第(3)次响应---------
| |
| |
| |
-------第(2)次请求--------------->(会话id)
| |
<------第(3)次响应---------
| |
| |
------------------------
服务端在内存中保存会话对象,浏览器怎么保存会话id:
请求参数:
将会话id作为一个请求参数,服务器接收请求自然能解析参数获得会话信息,并借此来判断是否来自同一
会话,很明显,这种方式不靠谱。
cookie:
那就浏览器自己来维护这个会话id,每次发送http请求时浏览器自动发送
会话id,cookie机制正好可以来处理这件事。cookie时浏览器用来存储少量
数据的一种机制,数据以Key:value的形式存储,浏览器请求时自动附带cookie信息。
tomcat会话机制当然也实现了cookie,访问tomcat服务器时,
浏览器中可以看到一个名为“JSESSIONID”的cookie,这就是tomcat会话机制维护的会话id.
浏览器 客户端
------------------------------
| |
-------第(1)次请求--------------->---------------[创建会话]
| | |
| |<------------------------|
| |
| |
<------第(1)次响应---------------(cookie:JSsessionid)
| |
|----------------------| |
| 设置cookie(jssessionid) |
| <----------------| |
| |
-------第(2)次请求--------------->(cookie:JSsessionid)
| |
<------第(3)次响应---------------
| |
| |
| |
-------第(2)次请求-------------->(cookie:JSsessionid)
| |
<------第(3)次响应---------------
| |
| |
-------------------------------
3: 单点登录:
什么是单点登录:
单点登录全称Single Sign On(以下简称SSO),
是指在多系统应用群中登录一个系统,便可在其他所有系统中得到授权而无需再次登录,
包括单点登录与单点注销两部分
登录:
相比于单系统登录,sso需要一个独立的认证中心,
只有认证中心能接受用户的用户名密码等安全信息,
其他系统不提供登录入口,只接受认证中心的间接授权。
间接授权通过令牌实现,sso认证中心验证用户的用户名密码没问题,创建授权令牌,
在接下来的跳转过程中,授权令牌作为参数发送给各个子系统,子系统拿到令牌,即得到了授权,
可以借此创建局部会话,局部会话登录方式与单系统的登录方式相同。
图示:
浏览器 系统1 系统2 sso认证中心
*-----------------*--------------------*-----------------*-
| | | |
| | | |
| | | |
---访问(系统1)------> | |
| | | |
| |----------------- | |
| | | | |
| <----验证未登陆--- | |
| | | |
| | | |
| |----------跳转(系统1的地址)------------>
| | | |------------------
| | | | |
| | | <----验证未登陆-----
| | | |
<-----------登陆页面(系统1的地址)--------------------------
| | |
------------登陆(usrname,password,系统1的地址)----------->
| | | |-------------------
| | | | |
| | | <---验证成功---------
| | | |
| | | |-------------------
| | | | |
| | | <---创建全局会话-----
| | | |
| | | |-------------------
| | | | |
| | | <---创建授权令牌-----
| <----------跳转(令牌)-------------------
| | | |
| | | |
| -----------校验(令牌)------------------>
| | | |
| | | |-------------------
| | | | |
| | | <-----令牌有效-------
| | | |
| | | |-------------------
| | | | |
| | | <-----注册系统--------
| | | |
| | | |
| <---------令牌有效----------------------
| | | |
| |--------------------------- |
| | |----令牌 |
| |<---创建局部会话信息------- |
| | | |
|<--受保护资源-----| | |
|
|
|
|-----------------访问----------------->| |
| | |
| | --验证未登陆 |
| | | |
| |<------------ |
| | |
| |-------调转----->
| | |-----------------
| | | |
| | |<---验证已登陆---
| | |
| | |
| | |
| |--校验令牌------->
| | |
| | |------------------
| | | |
| | |<令牌有效---------
| | |
| | |------------------
| |<---------------| |
| | |<--注册系统-------
| | |
| | |
| | |
| |<-令牌有效------|
| | |
| | |
| | |
| |------------
| | |
| |<-----创建局部会话(令牌)
| |
| |
| |
|<-----------受保护资源-----------------|
解释:
1:用户访问系统1的受保护资源,系统1发现用户未登录,跳转至sso认证中心,并将自己的地址作为参数
2:sso认证中心发现用户未登录,将用户引导至登录页面
3:用户输入用户名密码提交登录申请
4:sso认证中心校验用户信息,创建用户与sso认证中心之间的会话,称为全局会话,同时创建授权令牌
5:sso认证中心带着令牌跳转会最初的请求地址(系统1)
6:系统1拿到令牌,去sso认证中心校验令牌是否有效
7:sso认证中心校验令牌,返回有效,注册系统1
8:系统1使用该令牌创建与用户的会话,称为局部会话,返回受保护资源
9:用户访问系统2的受保护资源
10:系统2发现用户未登录,跳转至sso认证中心,并将自己的地址作为参数
12:sso认证中心发现用户已登录,跳转回系统2的地址,并附上令牌
13:系统2拿到令牌,去sso认证中心校验令牌是否有效
14:sso认证中心校验令牌,返回有效,注册系统2
15:系统2使用该令牌创建与用户的局部会话,返回受保护资源
16:用户登录成功之后,会与sso认证中心及各个子系统建立会话,用户与sso认证中心建立的会话称为全局会话,用户与各个子系统建立的会话称为局部会话,
局部会话建立之后,用户访问子系统受保护资源将不再通过sso认证中心,全局会话与局部会话有如下约束关系
17:局部会话存在,全局会话一定存在
18:全局会话存在,局部会话不一定存在
19:全局会话销毁,局部会话必须销毁
注销:
单点登录自然也要单点注销,在一个子系统中注销,所有子系统的会话都将被销毁。
图示:
浏览器 系统1 系统2 sos认证中心
| | | |
| | | |
---------------------------------------------------------------------
| | | |
| | | |
| | | |
---销毁请求(id)--------->
| | | |
| |----------- | |
| | | | |
| |<-取出令牌(会话id)
| | |
| | | |
---------------------------------------------->
| | | |
| | | |----------------
| | | | |
| | | |<--校验令牌有效(令牌)
| | | |
| | | |
| | | |-----------------
| | | | |
| | | |<--销毁全局会话--(令牌)
| | | |
| | | |-----------------
| | | | |
| | | |<-取出注册系统---(令牌)
| | | |
| | | |
| | |<--销毁局部会话令牌-----|
| | | |
| |<---销毁全局会话令牌-------------------------|
| | | |
|<----------------------------登陆页面--------------------------------|
解释:
1:sso认证中心一直监听全局会话的状态,一旦全局会话销毁,监听器将通知所有注册系统执行注销操作
2:用户向系统1发起注销请求
3:系统1根据用户与系统1建立的会话id拿到令牌,向sso认证中心发起注销请求
4:sso认证中心校验令牌有效,销毁全局会话,同时取出所有用此令牌注册的系统地址
5:sso认证中心向所有注册系统发起注销请求
6:各注册系统接收sso认证中心的注销请求,销毁局部会话
7:sso认证中心引导用户至登录页面
4:部署图:
单点登录涉及sso认证中心与众子系统,子系统与sso认证中心需要通信以交换令牌、校验令牌及发起注销请求,
因而子系统必须集成sso的客户端,sso认证中心则是sso服务端,
整个单点登录过程实质是sso客户端与服务端通信的过程。
系统1-------httpClient---------------sso认证中心---------------httpClient---------系统2
| | |
| | |
| | |
| | |
-------------------------------------------------------------------------------------
防火墙
-------------------------------------------------------------------------------------
- |
- |
- |
- |
点击登陆应用, |
用户访问部署了单点登录的多系统应用群, |
一次登录,到处使用 |
|
|
|
--------------
用户电脑
5:实现:
1:拦截子系统未登录用户请求,跳转至sso认证中心
2:接收并存储sso认证中心发送的令牌
3:与sso-server通信,校验令牌的有效性
4:建立局部会话
5:拦截用户注销请求,向sso认证中心发送注销请求
6:接收sso认证中心发出的注销请求,销毁局部会话sso-server
7:验证用户的登录信息
8:创建全局会话
9:创建授权令牌
10:与sso-client通信发送令牌
11:校验sso-client令牌有效性
12:系统注册
13:接收sso-client注销请求,注销所有会话
第一步:sos-client拦截未登录请求。
第二步:sos-aerver拦截未登录入请求。
第三步:sso-server验证用户登录信息。
第四步:sso-server创建授权令牌。
第五步:sso-client取的令牌并校验。
第六步:sso-server接收并处理校验令牌请求。
第七步:sso-client校验令牌成功创建局部会话。
第八步:注销过程。
6:代码:
/**
* Created by Mloong on 2018/7/30
* 实现单点登录
*/
var express = require('express');
var app = express();
var bodyparser = require('body-parser');
var crypto = require('crypto');
var session = require('express-session');
var cookie = require('cookie-parser');
var path = require('path');
var multipart = require('connect-multiparty');
var multipartMiddleware = multipart();
app.use(bodyparser.json());
var mdb;
/**
* session,cookie中间件。
*/
app.use(cookie());
app.use(session({
secret: 'secret', // 对session id 相关的cookie 进行签名
resave: true,
saveUninitialized: false, // 是否保存未初始化的会话
cookie: {
maxAge: 1000 * 60 * 3 // 设置 session 的有效时间,单位毫秒
}
}));
//app.set('tem', __dirname); //设置模板的目录
//app.set('view engine', 'html'); // 设置解析模板文件类型:这里为html文件
//app.engine('html', require('ejs').__express); // 使用ejs引擎解析html文件中ejs语法
//app.use(bodyparser.json()); // 使用bodyparder中间件,
//app.use(bodyparser.urlencoded({ extended: true }));
/**
* 连接mongodb
*/
var MongoClient = require('mongodb').MongoClient;
var url = "mongodb://localhost:27017/runoob";
/**
* 生成令牌
* 生成token
* @return {string} return 返回值
* */
function genToken()
{
var buf = crypto.randomBytes(12);
var token = buf.toString('hex');
return token;
}
/**
* 请求数据库
*/
MongoClient.connect(url, function (err, db)
{
if (err) throw err;
var dbo = db.db("runoob");
mdb = dbo;
});
/**
* 注册
*/
app.get('/register', function (req, res)
{
res.sendFile(path.join(__dirname, './public/templates', 'register.html'));
});
app.post('/register', multipartMiddleware, function (req, res)
{
var username = req.body.user;
var password = req.body.pwd;
console.log(username);
mdb.collection('user').findOne({username: username}, function (err, result)
{
if (err) throw err;
if (result)
{
res.json({
ret_code: 1,
ret_msg: '用户名已存在请更换用户名!'
});
}
else
{
mdb.collection('user').insertOne({usernaem: username, password: password}, function (err, result)
{
if (err) throw err;
res.redirect('/login');
});
}
});
}
);
/**
* 登录
*/
app.get('/login', function (req, res)
{
res.sendFile(path.join(__dirname, './public/templates', 'login.html'));
});
app.post('/login', function (req, res)
{
var username = req.body.user;
var password = req.body.pwd;
mdb.collection("user").findOne({username: username, password: password}, function (err, result)
{
if (err) throw err;
if (result)
{
var ticket = genToken();
mdb.collection('token').insertOne({ticket: ticket}, function (err, Lresult) {});
req.session.ticket = ticket;
res.cookie.ticket = ticket;
res.redirect('/index');
} else
{
res.json({
ret_code: 1,
ret_msg: '用户名或密码错误!'
});
}
});
});
/**
* 认证中心
*/
app.get('/authentication', function (req, res)
{
if (req.session.ticket)
{
console.log("进入认证");
var url = req.query.callback;
var token = req.session.ticket;
url = console.log(url + "?token=" + token);
res.redirect(url);
}
else
{
res.redirect('/login');
}
});
/**
* 首页
*/
app.get('/index', function (req, res)
{
if (req.session.ticket)
{
res.sendFile(path.join(__dirname, './public/templates', 'index.html'));
}
else
{
res.redirect('/login');
}
});
/**
* 注销
*/
app.post('/cancellation', function (req, res)
{
var token = req.session.ticket;
delete req.session.ticket;
mdb.collection('user').removeOne({ticket: token}, function (ree, result)
{
if (err) throw err;
res.redirect('/login');
});
});
var server = app.listen(8881, function ()
{
var host = server.address().address;
var port = server.address().port;
console.log("访问地址为 http://%s:%s", host, port);
});