单点登录认证系统前端实现方案

基于SSO单点登录开发的认证平台。实现了多个独立的业务系统之间的访问控制,用户只需要登录一次,就可以访问所有已经集成单点登录功能的业务系统,不需要对每一个业务系统都逐一登录。

单点登录概述

概念

单点登录(Single Sign On),简称SSO。是指在多个应用系统中,用户只需要登录一次,就可以访问所有相互信任的应用系统。

方案介绍

1. 技术选型:

采用OAuto2.0授权协议实现单点登录功能。本系统使用 Authorization Code(授权码模式)来获取 访问令牌 (access_token)

流程:
在这里插入图片描述

2. 涉及系统(只包含前端):

系统 说明
统一身份认证平台 用于业务系统登录,身份认证
管理后台 对集成单点登录的业务系统进行管理,包含业务系统注册、修改、注销等功能
业务系统 自身需要集成单点登录,可涉及多个业务系统

统一身份认证平台

平台包含功能:登录、注册、账号管理

需要开发一个独立的身份认证系统,其余业务系统不提供登录入口 (如果业务系统需要保留自身登录入口,可以在自身登录页集成单点登录模式,让用户可以根据需求选择使用哪种登录方式)。

业务系统在进行单点登录时,会统一跳转到身份认证系统的登录页。登录成功后通过重定向跳转回业务系统 的认证页面

注意:此项目前后端必须部署在同一个ip下。

  • 后端拦截器会根据cookie中是否存在用户信息,来进行跳转
  • cookie则无法跨域携带,如果登录系统与后端不在同一个ip下,会产生跨域问题。
  • 也可以尝试解决跨域问题,试过网上的配置没找到合适方法,最简单的方法就是把前后端部署在同一个ip下。

管理后台

使用开源框架 maxkey,对其进行了二次开发。后台管理使用了maxkey-web-frontend/maxkey-web-mgt-app 路径下的代码。

github地址:https://github.com/dromara/MaxKey.git

框架参考地址:http://ng.ant.design/docs/introduce/zh

可以通过 git upstream 将 maxkey源代码与自己仓库关联,maxkey项目更新了方便同步代码。因为前端只用到了 maxkey-web-mgt-app 路径下代码,通过稀疏检出屏蔽多余代码

maxKey前端maxkey-web-mgt-app项目拆分:

  • 第一种:git clone只能整个项目,不能指定目录
  • 第二种采用稀疏检出:
  1. 添加上游远程:git remote add upstream https://gitee.com/dromara/MaxKey
  2. 开启稀疏检出:git config core.sparsecheckout true
  3. 配置稀疏检出文件 :echo "/maxkey-web-frontend/maxkey-web-mgt-app/* " >> .git/info/sparse-checkout
  4. 拉取远端代码: git pull upstream main
  5. 注意:稀疏检出后可以只拉取maxkey-web-mgt-app目录下的文件,但是保留了Maxkey项目的目录结构(maxkey-web-frontend/maxkey-web-mgt-app)

maxkey-web-mgt-app使用angular语法开发,简单记录一下项目运行打包流程

maxkey-web-mgt-app项目调试运行:

  • 1.全局安装npm install -g @angular/cli
  • 2.初始化项目 cnpm install
  • 3.ng serve

api请求地址baseUrl修改:environment.prod.ts (生产、测试环境) | environment.ts(开发环境)

项目打包 :ng build(打包前务必确认environment.prod.ts 中baseUrl环境是否对应)

打包后目录前缀修改: angular.json中baseHref修改dist文件部署在服务器上的目录前缀

业务系统

业务系统需要对自身的登录、退出功能进行改造。

代码实现

统一身份认证平台

1. 登录页面:/user/login

用户登录成功后,将返回的congress 和 online_ticket 存入cookie

// 登录
Login({
     
      commit }, userInfo) {
    
    
  userInfo.authType = 'bx-mobile'
  userInfo.state = storage.get('loginState')
  userInfo.remeberMe = false
  return new Promise((resolve, reject) => {
    
    
    loginByMobile(userInfo).then(response => {
    
    
      if (response.code == 0) {
    
    
        storage.set('token', response.data.token)
        storage.set('userId', response.data.userId)
        storage.set('userInfo', response.data)
        commit('SET_TOKEN', response.data.token)
        commit('SET_USERID', response.data.userId)
        commit('SET_USERINFO', response.data)
        VueCookies.set('congress', response.data.token, {
    
     path: '/' })
        VueCookies.set('online_ticket', response.data.ticket, {
    
     domain: getSubHostName(), path: '/' })
      }
      resolve(response)
    }).catch(error => {
    
    
      reject(error)
    })
  })
}

根据浏览器queryString中是否携带参数redirect_uri进行判断:

  • 携带: window.open(redirect_uri, ‘_self’),通过重定向跳转到统一身份认证平台授权页面(拦截器中会对cookie进行校验,失败则不会跳转)
  • 不携带:this.$router.push(‘/home’),跳转平台首页
// 登录
submitForm() {
    
    
  this.$refs[this.current].validate(async valid => {
    
    
    if (!valid) return
    let params = {
    
     ...this[this.current] }
    // 账号登录 密码加密
    if (this.current == 'accountForm') {
    
    
      params.password = smcrypto(params.password)
    }

    this.Login(params).then(({
     
      code, data, message }) => {
    
    
      if (code != 0) {
    
    
        this.$message.error(message, 2)
        this.getCaptcha()
        return
      }
      // 缓存用户名和手机号
      if (this.current === 'accountForm' && this.rememberUsername) {
    
    
        storage.set('rememberUsername', this.accountForm.username)
      }
      if (this.current === 'codeForm' && this.rememberMobile) {
    
    
        storage.set('rememberMobile', this.codeForm.mobile)
      }

      if (this.$route.query.redirect_uri) {
    
    
        let redirect_uri = CryptoJS.enc.Base64url.parse(this.$route.query.redirect_uri).toString(CryptoJS.enc.Utf8)
        window.open(redirect_uri, '_self')
      } else {
    
    
        this.$router.push('/home')
      }
    })
  })
}

2. 平台首页:/home

对已授权的第三方业务系统进行管理。用户可以通过在身份认证系统登录,从首页跳转到选定的业务系统

// 跳转其他业务系统    
gotoOtherSystem(item) {
    
    
  if(item.protocol === 'Basic' || item.inducer === 'SP') {
    
    
    window.open(item.loginUrl)
  } else {
    
    
    console.log(`${
      
      this.$store.state.baseUrl}/sign/authz/${
      
      item.id}`)
    window.open(`${
      
      this.$store.state.baseUrl}/sign/authz/${
      
      item.id}`)
  }
}

3. 授权页面:/authorize

身份认证平台用户向用户请求授权, 用户是否允许第三方业务系统获得用户个人信息

分为 手动授权 和 自动授权两种模式。手动授权需要用户手动操作,自动授权对用户来说是无感知的

// 证实批准
approvalConfirm() {
    
    
  approvalConfirm(this.$route.query.oauth_approval).then(({
     
      code, data, message }) => {
    
    
    if (code != 0) {
    
    
      this.$message.error(message, 2)
      return
    }
    console.log('get-form表单数据', data)
    this.form.clientId = data.clientId
    this.form.appName = data.appName
    this.form.iconBase64 = data.iconBase64
    this.form.oauth_version = data.oauth_version
    this.form.user_oauth_approval = 'true'
    this.form.approval_prompt = data.approval_prompt
    
    // 授权流程为自动时, 自动触发
    if (this.form.approval_prompt == 'auto') {
    
    
      this.approvalAuthorize()
    }
  })
},

// 批准授权
approvalAuthorize() {
    
    
  approvalAuthorize(this.form).then(({
     
      code, data, message }) => {
    
    
    if (code != 0) {
    
    
      this.$message.error('提交失败', 2)
      return
    }
    if(this.form.approval_prompt == 'force') {
    
    
      this.$message.success('提交成功', 2)
    }
    // 跳转
    window.location.href = data
  })
},

// 拒绝授权
onDeny() {
    
    
  window.close()
}

4. 退出页面:/user/logout

退出时需要清除cookie中缓存的congress、online_ticket信息

Logout({
     
      commit, state }) {
    
    
  return new Promise((resolve) => {
    
    
    commit('SET_TOKEN', undefined)
    commit('SET_USERID', undefined)
    commit('SET_USERINFO', {
    
    })
    storage.remove('token')
    storage.remove('userId')
    storage.remove('userInfo')

    let rememberUsername = storage.get('rememberUsername')
    let rememberMobile = storage.get('rememberMobile')
    // 清除所有的键
    storage.clearAll()
    storage.set('rememberUsername', rememberUsername)
    storage.set('rememberMobile', rememberMobile)
    // 清除cookie
    VueCookies.remove('congress')
    VueCookies.remove('online_ticket')
    resolve(true)
  })
}

根据浏览器queryString中是否携带参数redirect_uri进行判断:

  • 携带:window.open(redirect_uri, ‘_self’)打开,会将页面重定向到单统一身份认证平台的登录页,并携带参数redirect_uri
  • 不携带:this.$router.push(‘/user/login’),跳转统一身份认证平台登录页
logout().then(({
     
      code, data, message }) => {
    
    
  if (code != 0) {
    
    
    this.$message.error(message || '退出登录失败', 2)
    return
  }
  this.Logout().then(response => {
    
    
    if (process.env.NODE_ENV === 'development' || !this.redirect_uri) {
    
    
      this.$router.push('/user/login')
    } else {
    
    
      window.open(this.redirect_uri, '_self')
    }
  })
})

管理后台

1. 修改项目请求地址baseUrl。项目打包部署 ng bulid

environment.prod.ts

export const environment = {
    
    
  production: true,
  useHash: true,
  api: {
    
    
    baseUrl: 'http://10.110.208.213:9526/maxkey-mgt-api/', // 生产环境
    // baseUrl: 'http://192.168.202.55:9526/maxkey-mgt-api/', // 测试环境
    refreshTokenEnabled: true,
    refreshTokenType: 're-request'
  }
} as Environment;

2. 应用管理中 对 业务系统注册 单点登录功能。同时也可以对一些配置项进行修改

在这里插入图片描述
在这里插入图片描述
业务系统需要以下资源:

资源名称 说明
登录地址 业务系统的登录页地址,业务系统可以创建一个用于单点登录的授权页地址作为登录地址,来区分单点登录和普通登录。
认证地址 业务系统用于授权单点登录的地址,可以和提供的登录地址相同。
应用名称 应用名称是指业务系统的全称,例如:测试业务系统。

在应用配置中填写完上述信息,修改完配置项后。可以得到以下资源

资源名称 说明
client_id 应用编码,即客户端id
client_secret 应用秘钥,即客户端秘钥

业务系统集成单点登录前端配置

1. 增加单点登录入口

业务系统登录页面,增加入口,点击跳转授权页面进行单点登录

this.$router.push('/idass')

2. 授权&单点登录页面:/idass

新增idass授权页面,并添加/idass路由,对该路由设置访问白名单权限

路由跳转前添加拦截,如果路由中没有携带code,window.open打开授权重定向接口,跳转身份认证平台登录页面

beforeRouteEnter(to, from, next) {
    
    
   if (!to.query.code) {
    
    
      window.open(`${
      
      vuex.state.baseUrl}/api/sso/directAuthorize`, '_self')
   } else {
    
    
      next()
   }
},

created方法中书写以下逻辑,如果路由中携带code,调用单点登录接口 /api/sso/ssoLogin 获取用户信息,并缓存;

接口调用成功后,执行原本登录成功后代码逻辑

created() {
    
    
  this.Login({
    
    
    userInfo: {
    
     code: this.$route.query.code },
    _this: this
  })
}

// 登录
Login({
     
      commit }, DATA) {
    
    
  let callback = response => {
    
    
    ... 原本登录后逻辑
  }

  let {
    
     userInfo, _this } = DATA
  let loginType = userInfo.code ? 'idass' : userInfo.account ? 'account' : 'code'
  switch (loginType) {
    
    
    case 'idass': // 单点登录
      ssoLoginUsingGET(userInfo).then(response => {
    
    
        if (response.code === 200) storage.set('idass', true)
        callback(response)
      })
      break
    case 'code': // 账号
      userInfo.code = undefined
      userInfo.encryptedData = undefined
      userInfo.iv = undefined
      userInfo.type = 'VERIFICATION'
      loginByMobileUsingPOST(userInfo).then(response => callback(response))
      break
    case 'account': // 验证码
      loginByPwdUsingPOST(userInfo).then(response => callback(response))
      break
  }
},

3. 单点退出功能集成

如果是单点登录,退出时需要调用window.open(${vuex.state.baseUrl}/api/sso/logout, ‘_self’)打开单点退出重定向方法

  • 该方法会重定向到统一身份认证平台退出页面,并携带有redirect_uri
  • 退出后,会重定向到统一身份认证平台登录页,并携带有redirect_uri
  • 用户在当前页面再次登录,因为querystring中有参数redirect_uri,所以会再次走授权流程
// 登出
Logout({
     
      commit, state }) {
    
    
  return new Promise((resolve) => {
    
    
    if (storage.get('idass')) {
    
    
      // 清除所有的键
      storage.clearAll()
      window.open(`${
      
      vuex.state.baseUrl}/api/sso/logout`, '_self')
    } else {
    
    
      // 清除所有的键
      storage.clearAll()
      router.push('/user/login')
      resolve('200')
    }
  })
}

猜你喜欢

转载自blog.csdn.net/weixin_45559449/article/details/129283351