业务侧-登录管理器-LoginLoader

103 阅读5分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第11天,点击查看活动详情

设计思路:

此管理器,主要是为登录页面服务和首页退出系统服务

image.png

支持子系统登录和多端登录模式,封装后

平台后期可定制化登录或者多登录切换,实现相应公有化接口函数即可

设计实践:

生产环境模式:

image.png

开发环境模式:

image.png

/*
 * @Description: 登录管理器
 * @Author: ffzheng
 */
import { BaseStore, Service, Config, IM } from "@basic-library";
import { cache } from "@basic-utils";
import { useIntervalFn } from '@vueuse/core'
import { useHistory } from '@hooks'

/**
 * 设置缓存数据
 * @param {*} inData 
 * @returns 
 */
const initData = (inData)=>{
    // 更新用户中心数据以及状态
    cache.setCache("token", inData.token, "local");
    cache.setCache("accountId", inData.accountId, "local");
    BaseStore.app.updateUserInfo(inData);
    BaseStore.app.updateLoginStatus(true);
}

/**
 * 获取子应用
 * @param {*}  NO
 */
 const getSubApps = () => {
    const { subSystemList } = BaseStore.app.userInfo
    return subSystemList || []
}

/**
 * 获取子应用--默认取第一个
 * @param {*}  NO
 */
 const findDefSubApp = () => {
    const subApp =  getSubApps()
    if(subApp && subApp.length){
        return subApp[0].path
    }
    return 
}

/**
 * 设置租户信息
 * @param {*} data 租户ID
 */
const setTenant = (data)=>{
    cache.setCache("tenantId", data, "local");
}

/**
 * 获取租户信息
 * @param {*} no
 */
const getTenantList = ()=>{
    return Service.useHttp("queryTenantList")
}

/**
 * 设置用户
 * @param {*} accountName
 * @param {*} password
 * @param {*} tenantId
 */
 const setRememberUser = (accountName, password, tenantId)=>{
    cache.setCache('remember',true,'cookie')
    cache.setCache('accountName',accountName,'cookie')
    cache.setCache('password',Base64.toBase64(password),'cookie')
    cache.setCache('tenantId',tenantId,'cookie')
}

/**
 * 获取用户
 * @param {*} no
 */
 const getRememberUser = ()=>{
    const remember = cache.getCache('remember','cookie')
    const accountName = cache.getCache('accountName','cookie') 
    const rePassValue = cache.getCache('password','cookie')
    const tenantId = cache.getCache('tenantId','cookie')
    const password = rePassValue ? Base64.fromBase64(rePassValue) :''
    return {
        remember,
        accountName,
        password,
        tenantId
    }
}

/**
 * 移除记住用户
 */
 const removeRememberUser = ()=>{
    cache.removeCookies('remember')
    cache.removeCookies('accountName')
    cache.removeCookies('password')
    cache.removeCookies('tenantId')
}


/**应用路径拼装--斜杠路径补全
 * @param {*} url 
 * @returns 
 */
 const getMicroUrl = (url) => {
    let path = url
    const protocol = window.location.protocol
    const host = window.location.host
    if(path.indexOf('/') !== 0)  path  = `/${path}`
    if(path.lastIndexOf('/') !== path.length -1)  path  = `${path}/`
    if(path.indexOf('/') == -1)  path = `/${path}/`
    return `${protocol}//${host}${path}`
}

/**
 * 当前应用跳转
 * @param {*} route 地址
 */
const redirectApp = (route = '/login') => {
    window.location.href = window._IS_RUN_MICRO_MODE ? getMicroUrl('main')  : route
}

/**
 * 登录应用-跳转子应用
 */
 const redirectSubApp = () => {
    const subApp = findDefSubApp()
    const devApp = '/platform'
    window.location.href = window._IS_RUN_MICRO_MODE ? getMicroUrl(subApp)  : devApp
}

/**
 * 获取验证码
 */
const queryCaptcha = async() => {
    return await Service.useHttp('queryCaptcha')
}

/**
 * @description: 登出
 * @return {*}
 */
 const logout = async() => {
    return await Service.useHttp('logout')
 }

/**
 * @description: 登出--跳转
 * @return {*}
 */
 const redirect = () => {
    localStorage.clear()
    sessionStorage.clear()
    IM.closeConnection()
    redirectApp()
}
 
/**
 * 创建心跳连接
 * @param {*} params  NO
 */
const createHeartEvent = () => {
    const { pause, resume } = useIntervalFn(async() => {
        // 心跳检测
        await run()
    }, Config.BSConfig?.HeartTime || 30000, { immediate: true })

    // 检测运行
    const run = async() => {
        const { success,returnObj } = await Service.useHttp("queryCurrentAccount")
        if(success){
            if(returnObj){
                pause()
                initData(returnObj)
                useHistory("/")
            }
            resume()
        }else{
            pause()
        }
    }
    // 运行心跳检测
    run()
}

export default {
    initData,
    setTenant,
    getTenantList,
    setRememberUser,
    getRememberUser,
    removeRememberUser,
    createHeartEvent,
    queryCaptcha,
    logout,
    redirect,
    redirectApp,
    findDefSubApp,
    getSubApps,
    redirectSubApp
};

暴露出这么多,看起来有点烦躁,不过还是要耐下心来,聊聊

登录具备基本的功能不做过多解释

看看代码即可,比如,记住用户管理、初始化用户数据、验证码、设置租户(业务需要)

需要简单解释下的

登录/登出API

疑问就出来了,为什么要封装成redirect或者获取应用配置跳转呢?

主要原因:

  • 多个客户端逻辑公用
  • 有专门的登录应用,来负责子应用运行
  • 子应用有自己的登录,主要是独立部署或者开发环境调试

大体上明白了吧,这个肯定也需要后端进行配置

后端需要提供登录后

1、当前用户所拥有的子系统,拿到当前子系统信息,然后进行切换跳转,目前默认第一个,后期发展可能会进行多应用切换
2、当前用户登录子应用所在会话

logout
redirect
redirectApp
findDefSubApp
getSubApps
redirectSubApp

代码中,可以看到,window._IS_RUN_MICRO_MODE,这个全局变量,标记当前子应用的运行环境,是开发环境还是线上环境

子应用入口页面,这么搞就可以了

window._IS_RUN_MICRO_MODE = true

if(process.env.NODE_ENV === 'development'){
   window._IS_RUN_MICRO_MODE = false
}

心跳检查 createHeartEvent

主要是使用了,定时器,这个定时器的参数可配置,页面进入后,执行一次,然后间隔时间执行。

实现:

用户正常会话内,非法关闭页面的情况,再次访问根目录,直接跳转系统首页。

这个逻辑很重要,各大云厂商都有相应逻辑实现,不能直接跳转到登录让重新登录。为什么呢?

架构设计,不允许客户端进行多租户访问。

有的ToB类系统是运行的,这个保存中立吧。

因为这个问题,上次一个项目都被搞栏过,差点跑路

设计上没沟通清楚,前端实现根路径是登录,认为支持多会话模式,但服务端是单一会话模式,就导致多用户登录,使用的一个会话,这样业务数据写入后,数据就串了。

因为我们业务,用户数据是根据租户走的。

这也许是一个悲伤的故事....

Login.vue

页面初始化

/**
 * 加载验证码
 */
const loadVerifyCode = (params) => {
  SecurityRefs.value.refreshCode()
  userForm.captcha = ''
}

onMounted(async () => {
  systemName.value = Config.BSConfig?.systemName;

  const {remember, accountName, password, tenantId} = LoginLoader.getRememberUser();
  // 记住用户
  if(remember){
    userForm.accountName = accountName
    userForm.password = password
    rememberPass.value = true
    userForm.tenantId = tenantId
  }

  // 加载验证码
  loadVerifyCode()
});
        <el-form-item prop="captcha" label="">
          <div class="captcha-text">
            <el-input placeholder="请输入验证码" 
              v-model="userForm.captcha"  
              onkeyup="value=value.replace(/[^\w\.\/]/ig,'')" 
              @keyup.enter="submitForm()" maxlength=20>
            </el-input>
          </div>
          <div class="captcha-code">
            <Security ref="SecurityRefs"></Security>
          </div>
        </el-form-item>
  1. 获取记住用户信息,登录表单赋值
  2. 验证码加载-验证码实现,可移步Vue3 实现图形验证码功能

登录

/**
 * @desc 表单校验
 * @param {Object} NO
 */
const submitForm = async () => {
  elmRefs.value.validate((loginValid) => {
    if (!loginValid) {
      EduMessage({
        type: "error",
        message: "请输入账号/密码/学校",
      });
      return;
    }
    jumpLogin();
  });
};

/**
 * @desc 登录
 * 
 */
const jumpLogin = async () => {
  setConfig()

  const { accountName, password, captcha } = userForm
  const result = await Service.useHttp({
    name: "loginService",
    headers: { captcha }
  }, {
    accountName,
    password,
  });

  loading.value = false
  
  if (result?.success) {
    if (!loginAfterService(result)) { 
      // 刷新验证码
      loadVerifyCode()
      return
    } 
    LoginLoader.redirectSubApp()
  } else {
    // 刷新验证码
      loadVerifyCode()
  }
};

/**
 * 登录时,配置设置
 */
const setConfig=()=>{
  LoginLoader.setTenant(userForm.tenantId);
  loading.value = true
}

基本逻辑还是围绕,登录事前和时后去做什么

登录时-配置设置
登录后-loginAfterService,权限认证、用户数据加载、记住用户

应用中,使用到了登录管理器中的

1、LoginLoader.getRememberUser --获取记住用户信息
2、LoginLoader.setTenant --设置租户ID
3、LoginLoader.initData --加载用户相关数据
4、LoginLoader.setRememberUser --设置记住用户信息
5、LoginLoader.removeRememberUser --移除记住用户信息
6、LoginLoader.logout --登出系统请求
7、LoginLoader.redirect --登出请求跳转
8、LoginLoader.createHeartEvent --页面心跳监听

看起来有点复杂,但是都是常用的逻辑

针对Loader管理器的一系列,请看下方:

业务侧-Loader设计
业务侧-系统应用加载器-AppLoader
业务侧-登录管理器-LoginLoader