2

如何设计统一登录业务

 3 years ago
source link: https://www.joynop.com/p/404.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

如何设计统一登录业务

几乎所有的项目都需要登录,无论是权限限制、个性化定制、信息安全等需求,都要通过登录系统来获取用户信息,以便提供后续服务。
而一个公司可能会有多个不同的项目,每个项目后端都是共用同一套用户系统的话,就势必会有通用登录的需求出现。
通用登录的方式有很多种,下面我们仅探讨前端的实现方案。

项目子域名不同,共用一个父域

通过设置 cookiedomian 属性,可以使得 cookie 携带的内容在父子域名下共享。
根据这个特性,登录之后将 token 保存在 cookie 里面,所有子项目可以共享 token。
将登陆系统单独提出来做成一个单独的项目,其他所有的项目在未登录的情况下重定向到独立的登录系统,登录之后再根据来源跳转到对应的页面,简单的实现如下:

// 子项目在判断未登录的时候,跳转对应的登录项目并将当前的url作为参数带给登录系统
location.replace('https://login.abc.com?redirectUrl' + window.location.href)

// 登录系统在登录之后,根据redirectUrl跳回对应的项目
location.replace(redirectUrl)
JavaScript

这种方式是最为简单的,并且由于登录是独立的项目,也可以将个性化的定制放到项目中,只需要在其他项目跳转的时候除了 redirectUrl 外,多附带项目类型参数(参数名随便取)就可以针对不同的子系统定制个性化的登录界面。

同域,但根据网关来区分项目

实现效果同上,但是由于是同域,所以可操作性的地方就更多,token 不仅仅限制于 cookie,任何本地存储的方式都可以使用,例如 sessionStoragelocalStorage 等本地缓存都行。

一般使用此方式的都是 pc 端,定制化高,但是同时登录项目的资源会比较多,加载速度有影响。

将登录的组件、接口、逻辑全部打包成 npm 包,使用到的项目可以按需引入之后,调用统一的登录方式。

就跟写组件业务一样,把登录当成一个独立的业务组件来写,缺点是当登录业务升级的时候,所有有关的项目都需要重新构建、发布。

CDN SDK

sdk 的统一登录方案,这里就拿出来详细说下,顺便附带部分代码讲解。

其实总的来说,没啥难度,就是将整个登录业务封装一下,做的更为通用罢了。

首先,分析一下,登录业务需要拆分成如下 4 个部分:

1.登录 DOM 渲染
2.请求模块
3.登录使用到的事件模块
4.登录事件之后的回调(成功、失败等)

登录 DOM 渲染模块

预先将登录的静态 html 写好。然后将写好的模板以模板字符串保存,样式以内联样式写入。

this.domTpl = `<div style="position: fixed; top: 0; left: 0; background: #fff; width: 100%; height: 100%; z-index: 9999;font-family: 'PingFangSC-Regular'">
    ${this.close ? `<div id="closeIcon" style="position: absolute; right: 10px; top: 10px"><p style="height: 20px; width: 20px;" >X</p></div>` : ''}
    ${this.imgUrl.loginImgStart ? `<div class="logo" style="text-align: center; padding-top: 60px;">
      <img src=${this.imgUrl.loginImgUrl} style="width: 36.6vw; height: 36.6vw" />
    </div>` : ''}
    <div style="width: 78.6vw; margin: 0 auto; margin-top: 16px;">
      <input id="phone" type="text" name="phone" placeholder="请输入手机号码"
             style="width: 100%;font-size: 16px; padding-top: 22px; -webkit-tap-highlight-color: rgba(255, 255, 255, 0);
             outline: none;border: none;border-bottom: 1px solid rgba(232,232,232,1);padding-bottom: 10px;" />
    </div>
    <div style="width: 78.6vw; margin: 0 auto; display: flex;">
      <input id="code" type="text" placeholder="请输入验证码"
             style="width: calc(100% - 94px); font-size: 16px; padding-top: 22px; -webkit-tap-highlight-color: rgba(255, 255, 255, 0);
             outline: none;border: none;border-bottom: 1px solid rgba(232,232,232,1);padding-bottom: 10px;" />
      <p class="Obtain" style="width: 84px;border:1px solid rgba(42,112,254,1); font-size: 12px;padding: 5px 12px; text-align: center;margin: 20px 0 0px 0;
                           color: #2A70FE;border-radius:8px;">获取验证码</p>
    </div>
    <div style="width: 78.6vw; margin: 0 auto;margin-top: 45px;position: relative;">
      <div class="tipModel" style="display: none; position: absolute; top: -24px; left: 0; right: 0; color: #FF495F; font-size: 12px; text-align: center; margin-bottom: 12px;">123</div>
      <p class="loginButton" style="font-size: 17px;background:rgba(203,205,209,1);box-shadow:0px 1px 4px 0px rgba(82,88,102,0.2);border-radius:4px; text-align: center;
                font-family: 'PingFangSC-Regular';font-weight:400;color:rgba(255,255,255,1);line-height:40px;margin-block-start: 0;margin-block-end: 0;">登录</p>
    </div>
    ${this.agreement.start ? `<div style="width: 78.6vw; margin: 0 auto;margin-top: 12px;">
      <div id="notes" style="display: flex;align-content: center;">
        <i id="regulations" style="display: block;background: url(${this.regulations}); background-size: cover; width: 16px; height: 16px;margin-right: 5px;"></i>
        <p style="color: #7A8599;font-size: 12px;margin-block-start: 0;margin-block-end: 0;">已阅读并同意<a href=${this.agreement.serverUrl} style="color: #2A70FE;text-decoration:none;">《用户服务协议》</a>和<a href=${this.agreement.privacyUrl} style="color: #2A70FE;text-decoration:none;">《隐私政策》</a></p>
      </div>
    </div>` : ''}
</div>`;
JavaScript

统一的登录界面,可以预先添加一些模块定制化,比如登录 logo,背景图片等,会更加通用一些。
另外为了保证 sdk 的体积与加载速度,尽可能的少用大图素材,小的素材直接 base64 引入,背景大图这种比较大的资源,采用 cdn 引入。

为了保证较高的兼容性,以及 sdk 的大小,所以直接采用原生的 xhr 请求,不使用额外的 ajax 请求库与 fetch。

// 发送ajax请求
createXMLHttpRequest(url, errFun) {
    let xmlHttp = new XMLHttpRequest();
    xmlHttp.open("POST", url, false);
    xmlHttp.setRequestHeader('content-type', 'application/json');
    xmlHttp.send(this.paramsEven());
    return xmlHttp.onreadystatechange = () => {
      if (xmlHttp.readyState === 4 && xmlHttp.status === 200) {
        let data = JSON.parse(xmlHttp.responseText);
        if (data.code !== 0) {
          return errFun(data.errMsg);
        }
        if (url === this.dataStorage.url) {
          this[`${this.dataStorage.storage}Even`](data.data.token); // 根据配置缓存方法,将缓存存到制定的位置
          if (this.success) this.success(data.data.token); // 直接成功回调,把 token 传给调用者
        }
        return data;
      }
    };
}
JavaScript

登录使用到的事件模块

需要内置的事件如下:

1.验证码发送
2.手机、账号、验证码校验
3.登录请求
4.页面关闭
5.提示交互

// 登陆相关事件
bindAction() {
// 手机号正则
let checkPhone = (phone) => {
  if (!(/^1(3|4|5|6|7|8|9)\d{9}$/.test(phone))) {
    return false;
  } else {
    return true;
  }
};

// 弹窗
let tipModel = {
  show: (tipFont) => {
    let tipModel = document.getElementsByClassName('tipModel')[0];
    tipModel.innerHTML = tipFont;
    tipModel.style.display = 'block';
  },
  hide: () => {
    document.getElementsByClassName('tipModel')[0].style.display = 'none';
  }
};

// 验证码相关
let ObtainFun = () => {
  let ObtainStart = document.getElementsByClassName('ObtainStart')[0];
  let time = 50;
  ObtainStart.innerHTML = `${time} S`;
  ObtainStart.style.borderColor = 'rgba(245,246,247,1)';
  ObtainStart.style.background = 'rgba(245,246,247,1)';
  time = time - 1;
  let interval = setInterval(() => {
    ObtainStart.innerHTML = `${time} S`;
    time = time - 1;
    if (time < 0) {
      ObtainStart.innerHTML = `获取验证码`;
      clearInterval(interval);
      document.getElementsByClassName('ObtainStart')[0].className = 'Obtain';
      let Obtain = document.getElementsByClassName('Obtain')[0];
      Obtain.style.borderColor = '#2A70FE';
      Obtain.style.background = '#fff';
    }
  }, 1000)
};

// 验证码事件
document.getElementsByClassName('Obtain')[0].onclick = () => {
  let phone = document.getElementById('phone').value;
  if (!checkPhone(phone)) {
    tipModel.show('请输入正确的手机号码');
    return false;
  }
  let dataInfo = {};
  if (document.getElementsByClassName('Obtain')[0]) {
    dataInfo = this.createXMLHttpRequest(this.dataStorage.verifyCodeUrl, tipModel.show)();
  }
  if (dataInfo.code === 0) {
    document.getElementsByClassName('Obtain')[0].className = 'ObtainStart';
    ObtainFun();
  }
};

// closeIcon事件
if (this.close) {
  document.getElementById('closeIcon').onclick = () => {
    this.hide();
  };
}

// 判断验证码是否存在
document.getElementById('code').oninput = () => {
  let codeVal = document.getElementById('code').value;
  if (codeVal) {
    let loginButton = document.getElementsByClassName('loginButton')[0];
    loginButton.style.background = '#3D424D';
    loginButton.style.color = '#fff';
  }
};

// 登陆事件
document.getElementsByClassName('loginButton')[0].onclick = () => {
  if (!document.getElementById('phone').value || !document.getElementById('code').value) {
    return tipModel.show('请输入正确的手机号码和验证码');
  }
  if (this.agreement.start && document.getElementById('regulations').style.backgroundImage !== `url("${this.regulationsStart}")`) {
    return tipModel.show('请阅读用户相关条例');
  }
  this.createXMLHttpRequest(this.dataStorage.url, tipModel.show)();
};

// 用户条例事件
if (this.agreement.start) {
  document.getElementById('notes').addEventListener('click', () => {
    let regulations = document.getElementById('regulations');
    let regulationsBackground = regulations.style.backgroundImage;
    if (regulationsBackground === `url("${this.regulations}")`) {
      regulations.style.backgroundImage = `url("${this.regulationsStart}")`;
    } else {
      regulations.style.backgroundImage = `url(${this.regulations})`;
    }
  }, false)
}
}

JavaScript

登录事件之后的回调(成功、失败等)

在初始化的时候,可以将需要的回调方法传入,再在对应的场景下,执行对应的回调事件。

如上,已经完成了一个简单、通用的登录 sdk,在项目中,直接引入即可:
一些可选的额外功能(例如:是否需要勾选协议验证等)

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8"/>
    <meta name="viewport"
        content="width=device-width, initial-scale=1, user-scalable=no, shrink-to-fit=no,viewport-fit=cover"/>
    <title>登录</title>
  </head>
  <body style="margin: 0;"></body>
  <script type="text/javascript" src="./js/login.js"></script>
  <script>
    Login.init({
      imgUrl: {
        loginImgStart: true,
        loginImgUrl: "https://mirror-gold-cdn.xitu.io/168e088524247c4bcc7?imageView2/1/w/180/h/180/q/85/format/webp/interlace/1",
        loginImgStyleWidth: "130px",
        loginImgStyleHeight: "130px"
      },
      agreement: {
        start: true,
        serverUrl: '',
        privacyUrl: ''
      },
      close: true,
      success() {
        console.log('success')
      },
      error() {
        console.log('error')
      },
      dataStorage: {
        path: 'https://login.com'
      }
    })
  </script>
</html>

如上,一个通用的登录 sdk 开发完毕,总体压缩之后的大小为 9kb 左右。如果感觉还不够的话,可以使用 es5 语法开发,体积可以再压缩一些。

可以设置初始化 sdk 之后,自动、手动判断登录态,根据本身需进行登录业务处理
根据自身的项目需求,对通用的 sdk 进一步定制化


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK