同一浏览器多账号登录的一种实现方法

4,157 阅读5分钟

1. 功能描述

客户需要使用不同的账号在同一个浏览器上登录某系统进行查看和操作。

2. 实现难点

要实现多账号登录这个功能,我们首先想到的就是让客户使用多个浏览器分别登录不同的账号进行操作,但是这样的话,客户需要安装多个浏览器(比如IE、Firefox、Chrome等),而且还要频繁切换各个浏览器,着实不方便,用户体验很差。

同一浏览器多账号登录的实现难点是: 一个浏览器在同一时间同一个域下只允许一个帐号登录,我们将用户身份验证存储在Cookie中,并且Cookie在标签(同一域)之间共享,因此,当用户1和用户2都在同一个浏览器登录时,当用户1请求保存时,由于cookie已更新为用户2,因此它被识别为用户2的。

3. 方案设计

生产环境上,当前正在使用的域名作为主系统域名,再新增N(假设N=3)个子系统域名,这样的话,一个用户在同一个浏览器可以使用4个不同的账号登录某系统进行操作。

4. UI设计

UI设计比较简单,仅供参考。

1)在主系统的登录页增加一个下拉框,名字叫:【快速切换系统】,如果本系统或者子系统登录成功了,下拉选项就显示已登录的用户名,比如:“系统链接1--已登录的用户名”。如果是“未登录”,则显示“系统链接1--未登录”

image.png

2)在主系统的首页或者某个角落也加上下拉框:【快速切换系统】,如果本系统或者子系统登录成功了,下拉选项就显示已登录的用户名,比如:“系统链接1--已登录的用户名”。这样,在操作住系统时,能方便地看到各个系统的登录情况。

上面两点的交互设计,很简陋,大家可以根据自身需求做更好地设计。

3) 注意:

  1. 只能从主系统点击某链接跳转到某子系统,不能从某子系统点击链接跳转到其他子系统,也就是子系统不展示上文的UI模块。原因是:主系统需要实时展示各个子系统的登录情况,所以每次都得从主系统跳转过去。

  2. 和第1点差不多,限制只能从主系统跳转过去,不能直接复制子系统的链接打开页面,原因和第1点的一样。

5. 核心代码

5.1 新建域名配置文件

首先,新建一个域名配置文件DomianConf.es6,代码如下:

/**
 * 域名配置文件
 */
const DomainVo = {
    /**
     * 线上域名配置
     */
     // 主系统域名(含协议和端口号)
    mainDomain: 'https://xx.yyyy.com', 
    
    // 子系统域名(含协议和端口号)
    subDomain: [ 
        {
            domain: 'https://xx1.yyyy.com',
            name: '系统链接1',
        },
        {
            domain: 'https://xx2.yyyy.com',
            name: '系统链接2',
        },
        {
            domain: 'https://xx3.yyyy.com',
            name: '系统链接3',
        },
    ],
};

export default DomainVo;

5.2 实现UI模块

现在,来实现UI模块的代码,这个模块的代码由多个部分组成。

5.2.1:初始化子系统域名的信息

getInitialState() {         
    const domains = DomainConf.subDomain.map((subDomain) => {             
        return {                 
            domain: subDomain.domain,                 
            shortName: subDomain.name,                 
            userName: '',                
            href: subDomain.domain,             
        };        
    });    
    
    return {             
        domains,             
        show: false,         
    };     
},

5.2.2:实现主系统的信息展示

当主系统登录成功后,将用户信息存储到localStorage中,

localStorage.setItem('MULTI_ACCOUNTS', JSON.stringify({
            user: userVo.name,
    }));

基于ant-design框架实现的代码片段如下所示:

  render() {
        // 域名不生效,不显示
        if (!this.state.show) return null;
        
        const { props } = this;
        const { user } = JSON.parse(localStorage.getItem('MULTI_ACCOUNTS')) || {};
        const { host, protocol } = window.location;

        // 子系统不展示「登录子系统」
        if (host !== DomainConf.mainDomain.split('//')[1] 
            || protocol !== DomainConf.mainDomain.split('//')[0]) {
            return null;
        }

        const menu = (
            <Menu>
                // 渲染3个子系统链接的下拉选项
                {this.renderMultiAccount()}
                <Menu.Item
                    disabled
                    key={DomainConf.mainDomain}
                >
                    本系统
                    <span style={{ paddingLeft: 26 }}>
                        {user ? user : '未登录'}
                    </span>
                </Menu.Item>
            </Menu>
        );

        return (
            <div
                className="m-multiAccount"
                style={props.style}
            >
                <Dropdown overlay={menu}>
                    <a>
                        快速切换系统
                        <Icon type="down" />
                    </a>
                </Dropdown>
            </div>
        );
    }

5.2.3:实现三个子系统链接的信息展示

实现三个子系统链接的信息展示,也就是上文代码中提到的 this.renderMultiAccount()。

renderMultiAccount() {
        const { domains = [] } = this.state;

        return domains.map((domain) => {
            return (
                <Menu.Item key={domain.domain}>
                    <a
                        href={domain.href}
                        target={domain.domain}
                        className={cnames({
                            unLogged: !domain.userName,
                            logged: !!domain.userName,
                        })}
                    >
                        {domain.shortName}
                        <span style={{ paddingLeft: 20 }}>
                            {domain.userName ? domain.userName : '未登录'}
                        </span>
                    </a>
                </Menu.Item>
            );
        });
    }

5.2.4:主系统获取各个子系统当前的用户信息

主系统刚登录成功或者进入页面时,需要向后端发请求获取各个子系统当前的用户信息,用于展示。 注意:如果当前登录的是子系统,不需要发请求获取其他子系统的用户信息(实现方案中已经写明,不需要子系统展示该UI模块)。

  getMultiAccountInfo() {
        const { domains = [] } = this.state;

        domains.forEach((domain, index) => {
            jsonp(
                `${domain.domain}/xxx/yyy/zzz/getLoginInfoByDomain`,
                {
                    param: 'jsonpCallback',
                    timeout: 6000,
                },
                (err, data) => {
                    if (data) {
                        const { userVo } = data;
                        const { userName } = userVo || {};

                        domains[index].userName = userName;

                        this.setState({
                            domains: [].concat(domains),
                            show: true,
                        });
                    }

                    if (err) {
                        console.log(`无法访问${domain.domain},用户登录信息获取失败`);
                    }
                }
            );
        });
    }

分析: 使用jsonp跨域请求后端接口,获取各个子域名对应的用户名信息。

接口文档

用户登录主系统时,前端轮询调用,获取不同的子域名下的登录用户信息。

URL:/xxx/yyy/zzz/getLoginInfoByDomain

Method:GET

请求参数:

字段名称字段类型描述
jsonpCallbackString前端回调函数名称

响应参数

Jsonp格式的包含userVo信息的javaScritp脚本

返回参考

jsonpCallback( { "userVo": {id: 1, name: 'coco' })

5.2.5:实现主系统和子系统之间的通信

实现主系统和子系统之间的通信。比如: 子系统登录成功,发消息通知主系统; 子系统退出登录,发消息通知主系统。

  1. 两个系统之间通信使用postmessage

postMessage() 方法用于安全地实现跨源通信。

  1. 子系统给主系统发送消息
window.opener.postMessage({     
    type: 'message',     
    data: {         
        userName,        
        href: window.location.href,     
    }, 
}, '*');
  1. 主系统监听消息,并更新对应的子系统的用户信息。
 componentDidMount() {
        window.addEventListener('message', this.onTabMessage);
    },
componentWillUnmount() {
    window.removeEventListener('message', this.onTabMessage);
},
    
onTabMessage(e) {
        const { type, data = {} } = e.data;
        const { userName, href } = data;
        const { domains = [] } = this.state;

        if (!type || !data) {
            return;
        }

        const newDomains = domains;
        newDomains.forEach((datax, index) => {
            if (datax.domain == e.origin) {
                    newDomains[index].userName = userName;
                    newDomains[index].href = href || datax.domain;
            }
     });

    this.setState({
        domains: [].concat(newDomains),
    });
}

6. 本地调试方法

打开本地hosts文件:c:\windows\system32\drivers\etc

本地开发时,给本机多加几个名字:

127.0.0.1  a.b.com 
127.0.0.1  b.b.com 
127.0.0.1  c.b.com

这样,在本地开发时,完全可以只打开一个浏览器,然后开几个tab,分别访问上面几个url(

a.b.com、b.b.com、c.b.com),然后登录不同的用户账号进行操作。

同一浏览器实现多账号登录的方法有很多种,这是我当时选用的一种方法,文章中描述简单,省略了很多需求上和开发上的细节。

刚开始写技术类的文章,不知道大家是否能看得明白。大家如果有什么想说的,欢迎留言讨论哦。