单点登录原理及实现

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u011350550/article/details/83017993

单点登录原理及实现

随着业务发展,公司业务会不断壮大,每个业务都会存在用户登录和权限验证,不可能要求用户每个业务网站都登录一次,这个时候,就需要单点登录功能。下面将先介绍基本概念,然后以百度(baidu.com)为例进行讲解,最后用一个小例子讲解如何实现(后台用node)。本文是假设读者对http协议等有基本了解。

什么是单点登录?

单点登录即用户在公司的某个网站登录后,访问该公司相关的网站都是已登录状态,无需再进行登录操作。例如我们登录百度首页(https://www.baidu.com/ )后,访问百度知道(https://zhidao.baidu.com/ ),就已经登录,无需再次登录该站点,这就是单点登录。这个功能很重要,以阿里巴巴来说,业务十分庞大,如果每个业务网站都要重新登录,恐怕用户要急眼。

要实现单点登录,就要验证该用户已登录,问题就回归到用户的登录验证问题,如何来确保该用户已登录。

用户登录和权限验证

先来看下服务是如何进行登录和权限验证的。我们登录一个网页,进行操作,多个http请求如下所示:



整个登录、权限验证、退出操作可以分为下面几个步骤:

1、用户输入用户名和密码,客户端将用户名和加密后的密码发送至服务端。

2、服务端成功验证后,会生成一个加密的会话id,把放入id列表,同时,服务器会返回请求,并在返回头中设置setCookie,让id存储在cookie中。

3、用户发送其它请求(请求会携带cookie中的id)至服务端,例如获取历史操作记录,查询客户账单等等。

4、服务端验证id的有效性,有效则返回正确信息。
5、用户发送退出登录操作。

6、服务端验证id,验证成功则从id列表中删除该id,或把该id置为过期。

7、用户再次发出查询账单请求。

8、服务端验证id,发现该id不存在或已过期,则提示用户重新登录。

从上述过程可以发现,用户的权限验证,就是会话id的生成和校验,只要多个站点可以共享这个id,既可以完成单点登录为什么这么说呢?原因如下:

假设公司有两个站点,aaa.com和bbb.com,按照现在的架构设计,会把公共的业务模块抽象出来,作为独立的服务。客户信息作为一个公共功能,自然是独立的。架构设计图如下所示:



aaa.com和bbb.com有独立的服务器,但向客户信息系统等公共模块是独立的,即客户端请求信息是,两个服务首相会向客户信息管理系统进行客户权限验证,通过验证才继续操作,返回客户需要的信息。

之前我们已经的出结论,用户的权限验证,就是会话id的验证。如果两个站点的会话id共享,只需登录一次,两个站点的权限验证都可以完成。即单点登录的关键在于两个站点如何实现会话id(cookie)的共享,下面,我们以百度为例,分析他们如何实现cookie共享的。

实例分析

以百度为例,我们登录百度首页(https://www.baidu.com/ )后,进入百度文库(https://wenku.baidu.com/ )、百度知道(https://zhidao.baidu.com/ )都是已登录状态,我们用chrome调试工具分别看到几个站点的cookie,发现有部分cookie是相同的。
实际上,baidu.com为一级域名,其它都是二级域名,cookie可以在二级域名间共享,只需对cookie的domain进行设置即可。

上面这种情况比较可以共享cookie,如果你们公司的各个业务站点一级域名一致,可以使用该方法实现单点登录。但是,当站点之前不是同域,以及域名也不同时,是无法共享的。这是浏览器的同源策略引起的,目的是保护用户信息安全,防止不法分子窃取客户信息,伪装用户进行操作。

我们以百度(https://www.baidu.com/ ),hao123(https://www.hao123.com/ )两个站点为例进行测试,看看他们是如何实现单点登录的。实验步骤如下:

1、清除两个站点的cookie和登录信息,使他们都处于非登录状态。

2、打开百度首页(https://www.baidu.com/ )并登录,查看用户信息。

3、打开hao123(https://www.hao123.com/ )站点,查看用户信息。

4、比对用户信息,发现用户信息一致。即二者实现了单点登录,只要登录百度,即可登录hao123网站。

我们查看二者cookie,发现两者cookie中有多个值相同(例如都存在BDUSS这个值,且值相同),可以猜测他们可能共用了一套用户信息系统,至少用户登录信息是有共享的。

在登录是,我用fiddler(抓包工具,可自行百度)进行抓包,看看他们到底进行了什么操作?百度的登录过程,抓包如下



通过分析,我在图中用画了几道红线,开始第一个是百度账号的登录验证,然后返回了一个加密的用户身份唯一标识(bdu),然后向hao123站点发起请求,该请求携带用户身份标识(bdu),hao123验证该标识后,返回会话id,并存在的hao123的域cookie中。如下图所示:



如图所示,单点登录过程可分为以下几个步骤:

1、用户李三打开aaa.com网站,输入用户名密码,发送登录请求至aaa.com服务器。

2、aaa.com服务器向客户信息管理中心发起请求,验证李三用户名密码。

3、验证完后返回会话id和用户唯一标识(一串加密的字符串,用于跨域验证,暂时称它为sid)。

4、aaa.com收到登录成功信息和sid后,携带sid向bbb.com服务器发起认证请求。

5、bbb.com服务器同样向客户信息管理中心发起唯一标识认证。

6、客户信息管理中心认证成功返回会话id给bbb.com服务器。

7、bbb.com服务器返回会话id,会话id会存储在域bbb.com的cookie中。

8、后续访问bbb.com网站,请求都会携带会话id,后台拿到id去认证。

这就是单点登录的整个过程。

具体实现

下面用node模拟两个站点进行单点登录模拟。
服务器1,有一个页面,两个接口。

登录页代码如下:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>登录页面</title>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <style>
        div {
            height: 30px;
            width: 300px;
            margin: 20px auto; 
            text-align: center
        }

        div > label {
            display: inline-block;
            width: 80px;
            text-align: right;
            padding-right: 15px;
            box-sizing: border-box
        }
        div > input {
            width: 200px
        }
    </style>
</head>
<body onload="addEvent()">
    <div>
        <label>userName:</label>
        <input type="text" id="user_name">
    </div>
    <div>
        <label>password</label>
        <input type="password" id="password">
    </div>
    <div>
        <input type="button" id="submit" value="logon" style="width: 100px">
    </div>
    <script>
        //添加按钮事件
        function addEvent() {
            let submit = document.getElementById('submit');
            if (submit) {
                submit.addEventListener('click', () => {
                    login();
                });
            }
          
        }

        //登录请求方法
        function login() {
            let userName = document.getElementById('user_name');
            let password = document.getElementById('password');
            $.ajax({
                url: '/login',
                method: 'GET',
                data: {
                    userName: userName.value,
                    password: password.value
                },
                success: function(result) {
                    //使用用户唯一标识进行跨域认证
                    crossdomain(result.id);
                    location.href = location.protocol + '//' + location.host + '/success';
                },
                error: function(error) {
                    console.error('登录错误');
                }
            })
        }

        //跨域用户认证请求
        function crossdomain(id) {
            let iframe = document.createElement('iframe');
            iframe.src = 'http://localhost:10000/crossdomain?id=' + id;
            iframe.style.display = 'none';
            document.body.appendChild(iframe);
        }
    </script>
    <script src="./jquery.js"></script>
</body>
</html>

服务端代码如下:

const express = require('express');
const app = express();
const cookieParser = require('cookie-parser');

app.use(cookieParser());

//未登录访问带接口返回“无权限”,登录后返回“站点3000登录成功”
app.get('/success', (req, res) => {
    let sessionId = req.cookies ? req.cookies.sessionId : '';
    if (sessionId && sessionId === '123456') {
        res.send('站点3000登录成功');
    } else {
        res.send('无权限');
    }
});

//登录接口,验证成功返回会话id和用户登录唯一标识
app.get('/login', (req, res) => {
    let name = req.query.userName;
    let password = req.query.password;
    if (name === 'user' && password === '123456') {
        res.cookie('sessionId', '123456', { expires: new Date(Date.now() + 10 * 1000)});
        res.send({
            id: '123456789'
        });
    } else {
        res.sendStatus(400);
    }
});

//静态资源访问路径,路由中包含/static
app.use('/static', express.static('./resources'));

app.listen(3000, () => console.log('Example app listening on port 3000!'))

服务器2只有两个接口,代码如下:

const express = require('express');
const app = express();
const cookieParser = require('cookie-parser');
app.use(cookieParser());

//对于已认证用户,返回‘站点10000登录成功’,反之,返回‘无权限’
app.get('/getcontent', (req, res) => {
    let sessionId = req.cookies ? req.cookies.sessionId : '';
    if (sessionId && sessionId === '123456') {
        res.send('站点10000登录成功');
    } else {
        res.send('无权限');
    }
});

//认证用户接口,根据用户唯一标识认证用户是否登录
app.use('/crossdomain', (req, res) => {
    let id = req.query.id;
    if (id === '123456789') {
        res.cookie('sessionId', '123456', { expires: new Date(Date.now() + 1000 * 1000)});
        res.send('有权限');
    } else {
        res.send('无权限');
    }
});

//静态资源访问路径,路由中包含/static
app.use('/static', express.static('./resources'));

app.listen(10000, () => console.log('Example app listening on port 10000!'))

猜你喜欢

转载自blog.csdn.net/u011350550/article/details/83017993