vue-element-admin 扩展sso单点登录注册退出(八)

11 篇文章 15 订阅

有点复杂有点长,一定要耐心看,看完之后你会对单点登录有个神奇的看法。

你能学到的知识:1. layout布局 2.加载路由视图 3.封装路由以及路由拦截器使用 4.封装store auth验证模块 5.vue-router、vuex、js-cookie应用 6.什么叫sso单点登录

一、先介绍一下什么是单点登录?了解的同学可以直接跳过了~~~

1.为什么要用sso单点登录?

很多企业或者公司一开始的时候项目没有那么多,或许只有一两个,所以各自有各自的登录平台没啥关系,但是一旦到达一定规模后,单点登录显得尤为重要。在这里 举个栗子:淘宝、天猫、阿里云。一开始淘宝也是在本系统内进行登录注册的,但是当有了天猫,有了阿里巴巴之后,你登陆一个平台之后,另外几个平台再打开的时候会自动获取客户端存储的信息,然后验证是否登录,也就实现了单点登录。至于淘宝信息存在哪,有兴趣的可以自己去看看,这里不再赘述。

对了 这里有个小插曲,单点登录并不是说 两个网页同一个网站,这边登录后那边被顶下去了。这一定要注意~~~

2.单点登录的应用场景?

多个平台共用同一个用户系统,实现一端登陆后其余平台均可以拿到用户信息。

二、安装需要用到的插件,开始实际的代码操作

安装三个插件:npm install --save vue-router vuex js-cookie 安装完之后在项目里的package.json文件中有这三个插件就说明安装成功了

vue-router 应用vue的路由器

vuex 封装store,暴露模块

js-cookie 存浏览器cookie,获取cookie然后登录信息

三、详细介绍

跟紧党的 jue 步,要开始一步一步的操作了

1. 首先是 layout布局:我习惯吧layout文件夹放在组件components文件夹内,然后封装layout组件,引用路由组件,完成项目整体架构

在这里我新建了个layout文件夹以及下面的AppHeader和AppFooter文件夹,每个文件夹内新建了个index.vue文件,其实不难理解,就是布局的头部,底部以及整体的布局引用文件。

(1)AppHeader/index.vue && AppFooter/index.vue 这两个组件很简单,就是头部和底部代码,都是静态的。这里有个小细节点:<style scoped> 这个style标签内有个scoped属性,这个属性的意思是:里面的css只作用于当前的模板文件,也就是就算其他的组件有重名的class也不会有冲突。

(2)layout/index.vue文件,引入上面封装的两个组件,然后在头和脚中间 引用 rooter-view 组件

(3)在项目唯一暴露文件App.vue中引用route-view路由组件:这时候布局并不能出现,并且还会报错,别着急 继续往下走

2.加载路由视图:其实上面的router-view已经是加载路由视图了,只需要在组件中用router-view标签就可以

3.封装路由,以便让视图可以展示。这个时候前端就能展示出来了,如果还不显示的话,要看下你的代码是否编辑正确了,然后再根据实际报错实际分析,分析不出来的话就留言问我,我帮你分析。router/index.js

import Vue from 'vue'
import Router from 'vue-router'

Vue.use(Router)

const router = new Router({
    mode: 'history', // 定义路由地址前不带 # 
    routes: [
        {
            path: '/',
            component: () => import('@/components/layout'), // 必须写import导入,不然就是个字符串,不识别
            children: [
                {
                    path: '',
                    component: () => import('@/views/auth/login') // 因为auth下面不是index.vue所以login不能省略
                }
            ]
        }
    ]
})

// 引入store 执行里面的userLogout方法
import store from '@/store'
//路由拦截器
router.beforeEach((to, from, next) => {
    if (to.path === '/logout') {
        store.dispatch('userLogout', to.query.redirectURL) // 执行store里面的方法用dispatch(func, params)
    } else {
        next()
    }
})

export default router

4.页面完事了就要开始写功能了。编写顺序:(1)编写api----(2)封装store----(3)模板引用方法做处理 ,因为这里涉及的代码较多,所以不再截图,直接暴露我写的源码,源码中都有注释,哪里不懂的再留言问我。(题外话:其实我特别不想暴露源码,这样你们就不自己编写了,我不想让你变成一个ctrl+c & ctrl+v的程序员,所以如果不会写,尽量抄我的,不要直接复制,因为复制你还是不懂什么意思。)

(1)编写api 对了api请求接口也是做了转发,在我个人博客vue-element-admin初识中有介绍,不再多说,感兴趣的去看那篇文章。sso单点登录的项目请求地址:https://mock.mengxuegu.com/mock/6077a5b1990ff82a18f95bab/ssologin 也是mockjs模拟的数据。

这里的请求头要询问后台开发人员是否需要,不需要的话就无所谓了。下面是登录接口

import request from '@/utils/request'

const header = {'Content-type': 'application/x-www-form-urlencode'}

//请求头添加 Authorization: Basic client_id:client_secret
const auth = {
    username: 'xzec-sso',
    password: '123456'
}

// 登录接口
export function login(data) {
    return request({
        header,
        auth,
        url: '/auth/login',
        method: 'post',
        params: data
    })
}

// 查询用户名是否已经存在
export function getByUsername(username) {
    return request({
        url: `/system/api/user/username/${username}`,
        method: 'get'
    })
}

// 注册
export function register(data) {
    return request({
        url: '/system/api/user/register',
        method: 'post',
        data
    })
}

// 获取注册协议,本地html
export function getXieyi() {
    return request({
        url: `${window.location.href}/xieyi.html`,
        method: 'get'
    })
}

// 退出
export function logout(accessToken) {
    return request({
        url: '/auth/logout',
        method: 'get',
        params: {accessToken}
    })
}

(2)封装store

store/index.js很简单,引用vuex插件,在module中暴露封装的auth.js;两个文件的位置:store/index.js、store/modules/auth.js

index.js

import Vue from 'vue'
import Vuex from 'vuex'
import auth from './modules/auth'

Vue.use(Vuex)

const store = new Vuex.Store({
    modules: {
        auth
    }
})

export default store 

auth.js

import { login, logout } from '@/api/auth'
import { PcCookie, Key } from '@/utils/cookie'

// 定义状态,state 必须是 func
const state = {
    userInfo: PcCookie.get(Key.userInfoKey) ? JSON.parse(PcCookie.get(Key.userInfoKey)) : null,
    accessToken: PcCookie.get(Key.accessTokenKey),
    refreshToken: PcCookie.get(Key.refreshTokenKey)
}

// 登录改变状态
const mutations = {
    //赋值用户状态
    SET_USER_STATE(state, data) {
        const { userInfo, access_token, refresh_token } = data
        // 设置state状态赋值
        state.userInfo = userInfo
        state.accessToken = access_token
        state.refreshToken = refresh_token
        
        // 设置浏览器cookie
        PcCookie.set(Key.userInfoKey, userInfo)
        PcCookie.set(Key.accessTokenKey, access_token)
        PcCookie.set(Key.refreshTokenKey, refresh_token)
    },

    //重置用户状态 退出和登录失败时使用
    RESET_USER_STATE(state) {
        // state状态置空
        state.userInfo = null
        state.accessToken = null
        state.refreshToken = null

        // 清空浏览器cookie
        PcCookie.remove(Key.userInfoKey)
        PcCookie.remove(Key.accessTokenKey)
        PcCookie.remove(Key.refreshTokenKey)
    }
}

// 定义用户行为
const actions = {
    userLogin({ commit }, userInputData) {
        const { username, password } = userInputData
        return new Promise((resolve, reject) => {
            login({ username: username.trim(), password:password.trim() }).then(res => {
                const { code, data } = res
                if (code === 20000) {
                    commit('SET_USER_STATE', data)
                }
                resolve(res) //成功时返回resolve
            }).catch(error => {
                commit('RESET_USER_STATE')
                reject(error) // 失败时返回reject
            })
        })
        
    },

    userLogout({state, commit}, redirectURL) {
        logout(state.accessToken).then(res => {
            commit('RESET_USER_STATE')
            window.location.href = redirectURL || '/' 
        }).catch(error => {
            commit('RESET_USER_STATE')
            window.location.href = redirectURL || '/'
        })
    }
}

export default {
    state,
    mutations,
    actions
}

(3)模板引用 views/auth/login.vue

<template>
  <div class="login_page">
    <div class="login_box">
      <div class="center_box">
        <!-- 登录&注册-->
        <div :class="{login_form: true, rotate: tab == 2}">
          <div :class="{tabs: true, r180: reverse == 2}">
            <div class="fl tab" @click="changetab(1)">
              <span :class="{on: tab == 1}">登录</span>
            </div>
            <div class="fl tab" @click="changetab(2)">
              <span :class="{on: tab == 2}">注册</span>
            </div>
          </div>
          
          <!-- 登录 -->
          <div class="form_body" v-if="reverse == 1">
            <!-- submit.prevent 阻止默认表单事件提交,采用loginSubmit -->
            <form @submit.prevent="loginSubmit">
              <input type="text" v-model="loginData.username" placeholder="请输入用户名" autocomplete="off">
              <input type="password" v-model="loginData.password" placeholder="请输入密码" autocomplete="off">
              <div class="error_msg">{{loginMessage}}</div>
              <input type="submit" v-if="subState" disabled="disabled" value="登录中···" class="btn" />
              <input type="submit" v-else value="登录" @submit="loginSubmit" class="btn" />
            </form>
          </div>

          <!-- 注册 -->
          <div class="form_body r180" v-if="reverse == 2">
            <form @submit.prevent="regSubmit">
              <input type="text" v-model="registerData.username" placeholder="请输入用户名" autocomplete="off">
              <input type="password" v-model="registerData.password" placeholder="6-30位密码,可用数字/字母/符号组合" autocomplete="off">
              <input type="password" v-model="registerData.repassword" placeholder="确认密码" >
              <div class="error_msg">{{regMessage}}</div>
              <div class="agree">
                <input type="checkbox" id="tonyi" v-model="registerData.check">
                <label for="tonyi">我已经阅读并同意</label><a href="jvascript:;"  @click="xieyi = true">《用户协议》</a>
              </div>
              <input type="submit" v-if="subState" disabled="disabled" value="提交中···" class="btn">
              <input type="submit" v-else value="注册" class="btn">
            </form>
          </div>
        </div>
      </div>
    </div>

    <!-- 用户协议 -->
    <div class="xieyi" v-if="xieyi" @click.self="xieyi = false">
      <div class="xieyi_content">
        <div class="xieyi_title">请认真阅读用户协议</div>
        <div class="xieyi_body" v-if="xieyiContent" v-html="xieyiContent">
        </div>
        <input type="button" class="xieyi_btn" value="确定" @click="xieyi = false">
      </div>
    </div>
  </div>
</template>
<script >
import { isvalidUsername } from '@/utils/validate'
import { getByUsername, register, getXieyi } from '@/api/auth'

export default {

    data () {
      return {
        tab:  1, // 高亮当前标签名
        reverse:  1, // 旋转 1 登录,2 注册
        loginMessage: '', //登录错误提示信息
        regMessage: '', //注册错误提示信息
        subState: false, //提交状态
        xieyi: false, // 显示隐藏协议内容
        xieyiContent: null, // 协议内容
        redirectURL: '//www.xzec.com', // 登录成功后重写向地址
        loginData: { // 登录表单数据
            username: '',
            password: ''
        },
        registerData: { // 注册表单数据
            username: '',
            password: '',
            repassword: '',
            check: false
        },
      }
    },

    async created() {
      if(this.$route.query.redirectURL) {
        this.redirectURL = this.$route.query.redirectURL
      }

      this.xieyiContent = await getXieyi()
    },

    methods: {

      // 切换标签
      changetab (int) {
          this.tab = int;
          let _that = this;
          setTimeout(() => {
            this.reverse = int
          }, 200)
      },

      // 提交登录
      loginSubmit() {
        // 如果在登录中,不允许登录
        if(this.subState) {
          return false
        }

        // 校验用户名密码
        if(!isvalidUsername(this.loginData.username)) {
          this.loginMessage = '用户名不正确,不得少4位'
          return false
        }
        if (this.loginData.password.length < 6) {
          this.loginMessage = '密码不得少于6位'
          return false
        }
        // 提交中状态
        this.subState = true

        // 执行登录
        this.$store.dispatch('userLogin', this.loginData).then(res => {
          const { code, message } = res
          if (code === 20000) {
            window.location.href = this.redirectURL
          } else {
            this.loginMessage = message
          }
          this.subState = false
          this.loginData.username = null
          this.loginData.password = null
        }).catch(error => {
          this.loginMessage = '系统繁忙,请稍候再试'
          this.subState = flase
        })
      },

      // 提交注册
      async regSubmit() {
        // 判断是否提交中
        if (this.subState) {
          return false
        }

        // 验证用户名
        if ( !isvalidUsername(this.registerData.username) ) {
          this.regMessage = '用户名不正确,不得少4位'
          return false
        }

        // 验证用户名是否已经存在
        const { code, data, message} = await getByUsername(this.registerData.username)
        if(code !== 20000) {
          this.regMessage = message
          return false
        } else if (data) {
          this.regMessage = '用户名已经存在,请更换'
          return false
        }

        // 验证密码
        if (this.registerData.password.length < 6 || this.registerData.password.length > 30) {
          this.regMessage = '密码在6-30位之间不可有空格'
          return false
        }
        if (this.registerData.password !== this.registerData.repassword) {
          this.regMessage = '两次输入密码不一致'
          return false
        }

        //是否勾选协议
        if (!this.registerData.check) {
          this.regMessage = '请阅读并同意用户协议'
          return false
        }

        this.subState = true

        const ress = await register(this.registerData)
        if (ress.code !== 20000) {
          this.regMessage = ress.message
        } else {
          this.subState = false
          this.changetab(1)
          this.registerData.username = null
          this.registerData.password = null
          this.registerData.repassword = null
          this.registerData.check = false
        }
      }

    },
}
</script>
<style scoped>
@import '../../assets/style/login.css'; 
</style>

截至上面,你已经完成了sso单点登录、注册、退出的所有功能了。注意:登录和注册在组件中触发,而退出则直接在路由中触发,也就是你请求地址比如:www.xx.com/logout?redirectURL=http://www.xxxx.com 会自动匹配/logout 路由进行退出。

小结:utils里面的三个文件,我给你们暴露一下,有兴趣的可以研究一下,没兴趣的可以直接使用。包含cookie.js、request.js、validate.js

cookie.js

import Cookies from 'js-cookie'

// Cookie的key值
export const Key = {
  accessTokenKey: 'accessToken', // 访问令牌在cookie的key值 
  refreshTokenKey: 'refreshToken', // 刷新令牌在cookie的key值 
  userInfoKey: 'userInfo'
}

class CookieClass {
  constructor() {
    this.domain = process.env.VUE_APP_COOKIE_DOMAIN // 域名
    this.expireTime = 15 // 15 天
  }

  set(key, value, expires, path = '/') {
    CookieClass.checkKey(key);
    Cookies.set(key, value, {expires: expires || this.expireTime, path: path, domain: this.domain})
  }

  get(key) {
    CookieClass.checkKey(key)
    return Cookies.get(key)
  }

  remove(key, path = '/') {
    CookieClass.checkKey(key)
    Cookies.remove(key, {path: path, domain: this.domain})
  }

  geteAll() {
    Cookies.get();
  }

  static checkKey(key) {
    if (!key) {
      throw new Error('没有找到key。');
    }
    if (typeof key === 'object') {
      throw new Error('key不能是一个对象。');
    }
  }
}

// 导出
export const PcCookie =  new CookieClass()

request.js 封装了一个axios

import axios from 'axios'

const service = axios.create({
  // .env.development 和 .env.productiont
  baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url
  timeout: 10000 // request timeout
})

// 请求拦截器
service.interceptors.request.use(
  config => {
    return config
  },
  error => {
    return Promise.reject(error)
  }
)

// 响应拦截器
service.interceptors.response.use(
  response => { 
    // 正常响应
    const res = response.data
    return res
  },
  error => {
    // 响应异常
    return Promise.reject(error)
  }
)

export default service

validate.js

// 校验用户名是否合法 只允许4-30位中文、数字、字母和下划线
export function isvalidUsername(str) {
  const valid_map = /^[a-zA-Z0-9_\u4e00-\u9fa5]{4,30}$/
  return valid_map.test(str)
}

// 校验手机号是否合法
export function isvalidMobile(str) {
  const valid_map = 11 && /^1(3|4|5|6|7|8|9)\d{9}$/
  return valid_map.test(str)
}
  
// 校验邮箱是否合法
export function isvalidEmail(str) {
  const valid_map = /^[A-Za-z0-9_.-]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/
  return valid_map.test(str)
}

/* 合法uri*/
export function validateURL(textval) {
  const urlregex = /^(https?|ftp):\/\/([a-zA-Z0-9.-]+(:[a-zA-Z0-9.&%$-]+)*@)*((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]?)(\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])){3}|([a-zA-Z0-9-]+\.)*[a-zA-Z0-9-]+\.(com|edu|gov|int|mil|net|org|biz|arpa|info|name|pro|aero|coop|museum|[a-zA-Z]{2}))(:[0-9]+)*(\/($|[a-zA-Z0-9.,?'\\+&%$#=~_-]+))*$/
  return urlregex.test(textval)
}

四、注意事项

1.千万不要直接复制我的代码,就算不会也要手写一遍,里面不理解的要多查多问多看。

2.细节很重要,要学会分析问题,找到问题,解决问题

3.promise 成功用什么失败用什么一定要区分清楚

4.在用户行为里面为什么用commit执行定义的方法

5.路由拦截beforeEach里面的三个参数分别是什么?

6.为什么路由component组件用import引入

7.不会的一定要多查多问,不要怕丢人,因为谁一开始都是不会的。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值