阅读 2602

前端接入单点登录(sso)开发流程大礼包,内含vue、react、hash&history两种路由模式、微前端等处理方式

祖国生日,举国同庆,在下家境贫寒,没什么拿得出手的东西,给大🔥来个大礼包,整个活。

先简单说明一下,单点登录是什么,什么是单点登录

下面以前端的角度,说一下流程,以sso平台(sso.heiwangbatiancaishaonian.com)登录任意后台管理系统(one.heiwangbatiancaishaonian.com)为例:

sso提供的token过期时间为24小时,ticket的失效时间是5分钟,都不是永久有效的。

如图可以看到,在登陆成功后的跳转中,参数是含有ticket的,举例中的one.heiwangbatiancaishaonian.com是history模式的,而在hash模式下url会表现为 one.heiwangbatiancaishaonian.com?ticket=ST-173200-sA96hzXOTMY2R4sxz8OKU9RLTQEsz-lg-manage-11-74#/ ,由于项目可能有url解析逻辑或者location判断逻辑,所以我们在处理登录时需要在完成登录后去除url中ticket字段,避免与项目本身冲突产生不必要的bug。

登录逻辑

整体逻辑如图所示,蹩脚英语,将就看看

如图,以某一后台管理系统(one.heiwangbatiancaishaonian.com)分析测试用例,我们以结果为导向梳理一下这个逻辑

用户进入项目,有可能是两种情况:

  1. sso登录后跳转的
    • 不带重定向参数
    • 带重定向参数(可选)
  2. 直接输入想看的页面url
    • 首次登录(无token)
    • 二次登录(有token但不知是否失效)

url有三种情况:

  1. one.heiwangbatiancaishaonian.com/login

  2. one.heiwangbatiancaishaonian.com/appManage

  3. one.heiwangbatiancaishaonian.com/login?redir…

*三个测试用例分别对应为为登录逻辑组件,普通组件,url中带重定向参数的登录组件

注:注明可选的可以忽略,仅为需要记住用户当前url时使用,部分项目不适用于这种方式,公司后台中部分项目本身有登录后重定向逻辑,因权限问题,不适用于直接跳转回当前url,应在判断完登录状态之后交由原项目处理,新项目可以添加这种逻辑。

例子

由于权限问题,前端须保证用户首次访问(既没有token)的时候,尽量不看到页面,因此我们须在渲染页面之前有一层判断。

另外,还要根据网络请求后返回的code判断token是否过期,例子中分别是react跟vue的写法。

react - 跳转时参数会带上当前url

HOC验证是否有token

  {
    path: '/',
    component: '@/layouts/SecurityLayout',
    routes: [
      {
        path: '/',
        component: '@/layouts/BasicLayout',
        routes: [
          {
            path: '/',
            redirect: '/appManage',
          },
          {
            path: '/appManage',
            name: 'appManage',
            component: './appList',
          },
          ...
  }
复制代码

.config\routes.ts

  render() {
    const { isReady } = this.state;
    const { children, loading } = this.props;
    const jwt = getToken();
    const isLogin = jwt && jwt.expireAt && jwt.expireAt > moment().unix();

    if ((!isLogin && loading) || !isReady) {
      return <PageLoading />;
    }
    if (!isLogin && window.location.pathname !== '/user/login') {
    
	const DOMAIN = 'https://sso.heiwangbatiancaishaonian.com'
    const { redirect } = getPageQuery();
    const queryString = stringify({
      redirect: redirect || window.location.href,
    });
    const service = `${window.location.origin}/user/login?${queryString}`;
    window.location.href = `${DOMAIN}/cas/login?service=${service}`;
    	return null;
    }
    return children;
  }
复制代码

src\layouts\SecurityLayout.tsx

在路由中添加多一级高阶组件SecurityLayout,如果通过验证才渲染路由中页面,否则跳转。

注意HOC判断中必须先判断当前页是否为/login

http拦截器


request.interceptors.response.use(async (response: Response) => {
  const data = await response.clone().json();
    
  if (data.respCode >= 40100 && data.respCode < 40199) {
    localStorage.clear();
    const ssoURL = 'https://sso.heiwangbatiancaishaonian.com/cas/login'
    const { redirect } = getPageQuery();
    const queryString = stringify({
      redirect: redirect || window.location.href,
    });
    const service = `${window.location.origin}/user/login?${queryString}`;
    window.location.href = `${ssoURL}?service=${service}`;
    ...
复制代码

src\utils\request.ts

返回拦截判断code,不成功就跳转,跳转时定义redirect字段以当前路径为参数拼入重定向链接

login组件



const Login: React.FC<LoginProps> = props => {
  useEffect(() => {
    const { dispatch, location } = props;
    // node中querystring模块
    const queryString = stringify({
      redirect: location.query.redirect,
    });
    const service = `${window.location.origin}/user/login?${queryString}`;
    dispatch({
      type: 'login/login',
      payload: {
        ticket: location.query.ticket,
        service: service,
      },
    });
  }, []);

  return (
    <div className={styles.main}>
      <PageLoading />
    </div>
  );
};
复制代码

src\pages\user\login\index.tsx

以当前路径拼接redirect为service的字段请求token。

该例子中请求token的service参数有可能与sso的url参数中service值不同,这不是常规的操作,正确的请求参数必须与sso的url参数中service值一致,否则无法通过验证

vue - 不记录url

beforeEach验证是否有token


import VueRouter from 'vue-router'

// 路由相关
VueRouter.beforeEach((to, from, next) => {
  const ua = window.navigator.userAgent.toLowerCase()
  if (!ua.includes('wxwork') && to.name !== ('index') && to.name !== ('login') && !localStorage.getItem('token')) {
    const ssoURL = 'https://sso.heiwangbatiancaishaonian.com/cas/login'
    const service = location.search ? location.href.split(location.search)[0] : location.href
    window.location.href = `${ssoURL}?service=${service}`
  }
  next()
})
复制代码

src\main.js

如果通过验证才渲染路由中页面,否则跳转,本质上跟HOC差不多。

注意判断中必须先判断当前页是否为login或index(该项目中index会重定向至login)

login组件


    async login () {
      let code
      let res
      if (this.getUrlParams('code') || this.$route.query.code) {
        code = this.getUrlParams('code') || this.$route.query.code
        res = await this.$axios.post(this.$api.login, {
          code: code
        })
      } else if (this.getUrlParams('ticket') || this.$route.query.ticket) {
        const ticket = this.getUrlParams('ticket')
        res = await this.$axios.post(this.$api.loginCas, {
          ticket: ticket,
          service: `${location.href.split(location.search)[0]}`
        })
      } else {
        Message({
          type: 'error',
          message: '登录缺少必要参数,请重试'
        })
        return
      }
      ...
      //原有登录逻辑
   }
复制代码

根据不同浏览器请求不同接口,共用res不影响原有登录逻辑。

http请求拦截器

axios.interceptors.response.use(
  response => {
		...
        // 错误码为6说明未登录或token失效
        case 6: {
          localStorage.removeItem('token')
          const ua = window.navigator.userAgent.toLowerCase()
          if (!ua.includes('wxwork')) {
            const ssoURL = 'https://sso.heiwangbatiancaishaonian.com/cas/login'
            const service = location.search ? location.href.split(location.search)[0] : location.href
            window.location.href = `${ssoURL}?service=${service}`
          } else {
            router.replace({
              path: '/login',
              query: { id: router.currentRoute.query.id }// 登录成功后跳入浏览的当前页面
            })
          }
          return response.data
        }
        ...
        }
)        
复制代码

根据后端返回code判断token是否过期,所有页面token过期都返回同一个地址

注意:vue项目中大多采用hash模式的路由,地址中会有#,如果有ticket或其他search,地址会变成xxx/ticket=xxxx#/xxx,所以需要加一层判断const service = location.search ? location.href.split(location.search)[0] : location.href。如果在登录后重定向去除了url中的ticket,则不需要加这个判断

微前端

最近这个好像在征文,我说说缺点吧。

妈的这个是真的坑,当然也可能是我真的菜,如有不对请指正。

  • 开发环境下每个系统要单独写一个proxy,配置代理
  • css样式没法完全隔离,qiankun通过类名哈希来隔离样式,但子应用遇到position:fiexd怎么办,全乱成一锅粥了。
  • 子应用内的接口都是原来的,测试环境,预发布环境,正式环境域名都不一样,前端文件部署到服务器后,后端的接口也会跨域,那后端也要配无数跨域。
  • 我司的后台管理系统基本都有部署在企业微信应用里的,那ua得判断三种,企业微信,微前端环境,web环境,有些系统有本身的登录逻辑,各种各样的写法都有,微前端有自己单独的生命周期,无法完全兼容,子应用也要爆改,包括前文所说的token过期处理之类的。
  • 还有一些生命周期的我忘了,但反正最后基本放弃了。

总结一下,其实微前端的解决方案理论上应该是最契合单点登录的,但改起来有点太麻烦了,适合新项目,不适合旧项目,旧项目接入基本等于半个重构,想一键装载还是有点难。

文章分类
前端
文章标签