Nuxt.js的学习(四)单点登录

1、登录认证流程

1. 门户客户端要求登录时,输入用户名密码,认证客户端提交数据给认证服务器

2. 认证服务器校验用户名密码是否合法,合法响应用户基本令牌 userInfo 、访问令牌 access_token 、刷新令 牌 refresh_token。不合法响应错误信

息。

      单点登录全称Single Sign On(以下简称SSO),是指在多系统应用群中登录一个系统,便可在其他所有系统中得到授权而无需再次登录,包括单点登录与单点注销两部分 。

      官方一点的解释是这样:相比于单系统登录,sso需要一个独立的认证中心,只有认证中心能接受用户的用户名密码等安全信息,其他系统不提供登录入口,只接受认证中心的间接授权。间接授权通过令牌实现,sso认证中心验证用户的用户名密码没问题,创建授权令牌,在接下来的跳转过程中,授权令牌作为参数发送给各个子系统,子系统拿到令牌,即得到了授权,可以借此创建局部会话,局部会话登录方式与单系统的登录方式相同。这个过程,也就是单点登录的原理

      我的理解逻辑是,系统将登录模块部分分离出来,同域名下,顶级域和子域名能共享cookie值,登录的时候,A系统跳转到B统一登录页面,后面带?redirectURL=“A系统地址”,B登录页面登录成功后,通过redirectURL重定向到A系统地址

 2、登录页面登录接口

import request from '@/utils/request.js'



// 数据格式
const headers = { 'Content-Type': 'application/x-www-form-urlencoded' }
// 请求头添加 Authorization: Basic client_id:client_secret
const auth = {
    username: 'mxg-blog-admin',
    password: '123456'
}
// axios 会带在请求头上,并用base64编码
// http://localhost:7000/dev-api/auth/login?username=11111&password=111111
// 因为后面是带?方式传参,所以用params
export function login(data) {
    return request({
        headers: headers,
        auth: auth,
        url: `/auth/login`,
        method: 'POST',
        params: data
    })
}

3、通过Vuex进行状态管理,router进行路由管理

当登录成功后,后台响应的 userInfo、access_token、refresh_token 信息使用 Vuex 进行管理,并且将这些信息 保存到浏览器 Cookie 中。(目录结构如下)
(一)、安装 js-cookie 和 vuex 模块

npm install --save js-cookie vuex

 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

在main.js中引入 vuex

(二)、引入操作cookie的工具类

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);
    console.log("打印的存储的值为",value)
    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})
  }

  getAll() {
    Cookies.get();
  }

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

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

 (三)、在store文件夹下面创建modules/auth.js文件

import { login, logout, refreshToken } from "@/api/auth"

import { PcCookie, Key } from "@/utils/cookie"

const state = {
    //用户基本信息
    userInfo: PcCookie.get(Key.userInfoKey) ? JSON.parse(PcCookie.get(Key.userInfoKey)) : null,
    // userInfo: PcCookie.get(Key.userInfoKey) ? PcCookie.get(Key.userInfoKey) : null, // 用户信息对象
    //访问令牌
    accessToken: PcCookie.get(Key.accessTokenKey),
    //刷新令牌,当访问令牌过期了,就通过刷新令牌来拿去访问令牌
    refreshToken: PcCookie.get(Key.refreshTokenKey)

}
// 改变状态值
const mutations = {
    // const mutation = {
    // 赋值用户状态
    SET_USER_STATE(state, data) {
        // SET_USER_STATE(state, data) {
        // 结构传递过来的参数,并将参数赋值给 vuex进行管理
        const { access_token, userInfo, refresh_token } = data
        // userInfo
        state.userInfo = userInfo;
        state.accessToken = access_token;
        state.refreshToken = refresh_token;
        // 保存到cookie中
        // console.log(userInfo)
        // 在存储对象的之前,先将对象转换成json字符串,然后再存储
        // 参考博客:https://juejin.cn/post/6844903954011127816
        // 使用sessionStorage存储域的时候,
        // 因为user类型是一个对象,如果你不将对象转换成字符串的时候,
        // 浏览器则自动将对象强转为字符串。但是浏览器强转字符串的时候,并不具备将对象的键和值转换成字符串,所以只是单纯地将对象转换成【object object】
        PcCookie.set(Key.userInfoKey, JSON.stringify(userInfo))
        PcCookie.set(Key.accessTokenKey, access_token)
        PcCookie.set(Key.refreshTokenKey, refresh_token)

    },
    // 重置用户状态
    RESET_USER_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 }, userData) {
        const { username, password } = userData;

        // return new Promise((resolve, reject) => {
        return new Promise((resolve, reject) => {
            login({ username: username.trim(), password: password }).then(response => {
                console.log(response)
                const { code, data } = response;
                // 20000表示响应成功
                if (code == 20000) {
                    //状态赋值 
                    commit("SET_USER_STATE", data)
                    // commit('SET_USER_STATE', data)

                }
                resolve(response) // 不要少了
            }).catch(error => {
                //触发mutation中的方法,将数据置空
                commit("RESET_USER_STATE")
                reject(error)
            })
        })
}

export default {
    state,
    mutations,
    actions
}

主要设置三个

state(用户状态)相当于全局的data,在action中通过{state}获取,在mutations 中直接拿到

mutations :改变状态值,用于vuex的state值的添加删除,以及cookie值的添加删除操作

actions :定义行为,直接写方法,外面使用this.$store.dispatch("action中的方法名")

return new Promise((resolve, reject) => {})下面代码,如果正确返回,可以使用resolve(***),错误则使用reject(***)返回值,并且外部调用action中的方法,有返回。

4、用户跳转登录

登录页面如下

 (一)、主要逻辑内容如下

       login.vue登录页面,跳转到登录页面,登录页面create()方法通过this.$route.query.redirectURL判断请求地址,如果后面带有redirectURL,则将其保存下来,登录成功后通过window.location.href="保存的值",进行跳转到原网址

 async created() {
    //判断请求的参数有没有redirectURL,并将他截取出来
    if (this.$route.query.redirectURL) {
      this.redirectURL = this.$route.query.redirectURL;
    }
    const xieyiContent = await getXieyi();
    // console.log(xieyiContent)
    this.xieyiContent = xieyiContent;
    //获取协议内容
  },

(二) 登录方法如下

1、this.subState进行表单效验,如果为true,则证明还在请求中,防止重复提交

2、效验用户名密码

3、this.$store.dispatch("UserLogin", this.loginData),调用vuex状态管理action中的UserLogin方法,因为使用了new Promise,所以有返回值,通过返回值效验,如果成功则跳转到原来网页,通过 window.location.href = this.redirectURL;来跳转


    // 提交登录
    loginSubmit() {
      // 表单效验
      if (this.subState) {
        return false;
      }
      //效验用户名
      if (!isvalidUsername(this.loginData.username)) {
        this.loginMessage = "输入正确的用户名 ";
        return false;
      }
      if (this.loginData.password.length < 6) {
        this.loginMessage = "请输入正确的用户名密码 ";
        return false;
      }
      this.subState = true; //登陆中

      this.$store
        .dispatch("UserLogin", this.loginData)
        .then((resonse) => {
          //正常响应,在response里面接收
          const { code, message } = resonse;
          if (code === 20000) {
            // 跳转回来源的地址,引发到登录页面的地址
            // 跳转到来源地址
            window.location.href = this.redirectURL;
            // http://localhost:7000/?redirectURL=https%3A%2F%2Fwww.baidu.com%2F
          } else {
            // 如果是用户名或者密码错误,则将返回后台的message参数
            this.loginMessage = message;
          }
        })
        .catch((error) => {
          //比如后台出现500错误
          this.subState = false;
          this.loginMessage = "系统繁忙,请稍后重试";
        });
    },

5、退出登录拦截,转发

1、配置路由

import Vue from 'vue'

import Router from 'vue-router'

Vue.use(Router)
const router = new Router({
    // 开启历史路由,不然路由前面有个哈希值#
    mode: 'history',
    routes: [
        {
            // path: '/',
            //    comments:function(){
            //        return "@/components/layout/index.vue"
            //    }
            // 可以简写成为下面这种,因为只有一行返回值
            path: '/',
            component: () => import('@/components/layout'),
            children: [
                {
                    path: '',
                    component: () => import('@/views/auth/login'),
                }
            ]

        },{
            // path: '/',
            //    comments:function(){
            //        return "@/components/layout/index.vue"
            //    }
            // 可以简写成为下面这种,因为只有一行返回值
            path: '/refresh',
            component: () => import('@/components/layout'),
            children: [
                {
                    // src\views\auth\refresh.vue
                    path: '',
                    component: () => import('@/views/auth/refresh'),
                }
            ]

        }
    ]
})
import store from "@/store"
//路由拦截处理,来实现退出操作
// 路由拦截器,每次路由跳转都会通过这个拦截器
router.beforeEach((to, from, next) => {

    if (to.path ==="/logout") {
        // http://localhost:7000/logout?redirectURL=https://www.baidu.com/
        // 拿到请求地址中跳转:::to.query.redirectURL
        store.dispatch("UserLogout", to.query.redirectURL)
    } else {
        next()
    }
   
}
)


export default router

路由router需要在main.js中引入

使用router.beforeEach对路由进行拦截,并使用to.query.redirectURL拿到传递过来的redirectURL中的地址,传递给vueX中action中 的UserLogout,并把vuex中state中的值和cookie中的值清空,并在vuex中重定向到原来的地址

router.beforeEach((to, from, next) => {

    if (to.path ==="/logout") {
        // http://localhost:7000/logout?redirectURL=https://www.baidu.com/
        // 拿到请求地址中跳转:::to.query.redirectURL
        store.dispatch("UserLogout", to.query.redirectURL)
    } else {
        next()
    }
   
}

vuex中定义行为的action中的方法UserLogout ,通过路由拦截到/logout路由,触发action,然后重定向回去

    UserLogout({ state, commit }, redirectURL) {
        // 通过accessToken来退出
        logout(state.accessToken).then(response => {
            // 重置状态
            commit("RESET_USER_STATE")
            // 跳转回来源地址,假如没有重定向地址redirectURL,则跳转到登录页面
            window.location.href = redirectURL || "/"
        }).catch(error => {
            // 出现错误
            // 跳转回来源地址,假如没有重定向地址redirectURL,则跳转到登录页面
            window.location.href = redirectURL || "/"
        })
    },

6、通过refreshToken,刷新accessToken

     当A系统页面的accessToken,带accssToken访问后台,后台效验accessToken过期,系统后台返回404,A系统前端通过axios统一拦截后,捕获404,再跳转到B系统登录页面,进行accessToken授权,通过accessToken进行后台拿取用户信息,如果有效,返回登录时候的全部用户信息包括(userInfo,accessToken,refreshToken)信息,无效则跳转到统一登录页面,登录成功或者刷新成功后,返回到A页面

(一)、在router.js中配置路由,

,{
            // path: '/',
            //    comments:function(){
            //        return "@/components/layout/index.vue"
            //    }
            // 可以简写成为下面这种,因为只有一行返回值
            path: '/refresh',
            component: () => import('@/components/layout'),
            children: [
                {
                    // src\views\auth\refresh.vue
                    path: '',
                    component: () => import('@/views/auth/refresh'),
                }
            ]

        }

(二)、refresh.vue刷新页面的主要代码为

1、在created()初始化中拿到redirectURL中的路径值

created() {
    //   将重定向的URL解析并赋值
    this.redirectURL = this.$route.query.redirectURL || "/";
    this.refreshLogin();
  },

2、methods中通过刷新令牌refreshToken,来获取accessToken

//通过刷新令牌,来获取accessToken来
    refreshLogin() {
      this.$store.dispatch("SendRefreshToken").then((response) => {
        //刷新成功重定向到应用
        window.location.href=this.redirectURL
      }).catch(error=>{
        //刷新失败
        // this.message="您的身份已经过去,请点击 <a href='/?redirectURL=${this.redirectURL}'>重新登录</a>"
        // 点击跳转到登录页面,下面是地址的拼接<a href="/refresh?redirectURL=${this.redirectURL}">这样就跳转到本页面
        this.message =`您的身份已过期,请点击<a href="/?redirectURL=${this.redirectURL}">重新登录<a> `
      });
    },

3、通过this.store.dispatch(“SendRefreshToken”)来调用store中action中的SendRefreshToken方法,这个方法通过new Promise((resolve,reject)=>{})来返回不同状态的值,,如果state.refeshToken中有值,则存储,无则返回reject中的值,在refresh.vue中跳转到登录页面

   SendRefreshToken({ state, commit }) {
        //调用一个promise,因为成功和失败会有返回值
        return new Promise((resolve, reject) => {
            // 判断是否有刷新令牌
            if (!state.refreshToken) {
                //清空cookie中的信息
                console.log("没有拿到刷新令牌")
                commit("RESET_USER_STATE")
                reject("没有刷新令牌")
                return
            }
            //发送请求
            refreshToken(state.refreshToken).then(response => {
                //更新用户状态

                const { code, data } = response;
                // 20000表示响应成功
                if (code == 20000) {
                    //状态赋值 
                    commit("SET_USER_STATE", data)
                    // commit('SET_USER_STATE', data)

                }
                resolve(response) // 不要少了
            }).catch(error => {
                //出现异常,重置状态

                reject(error)
            })

        })
    }

(三)、A系统通过axios拦截,捕获404 异常(accessToken失效),跳转到B,通过到B系统统一获取accessToken。

1、自定义axios拦截器interceptor.js,并在nuxt.config.js中引入拦截器


 

 plugins: [
    // 引入饿了么UI的插件
    '~/plugins/element-ui.js',
    // 引入axios拦截器插件
    '~/plugins/interceptor',


  ],

2、interceptor拦截器如下所示,注释写的很详细,因为nuxt有服务端和客户端,window只能运行到客户端,所以获取跳转的redirectURL要分开判断,如下面redirectURL()方法所示

//创建拦截器
export default ({ store, route, redirect, $axios }) => {
    // 请求的拦截器,配置请求token
    $axios.onRequest(config => {
        console.log('请求拦截器')
        const accessToken = store.state.accessToken
        if (accessToken) {
            config.headers.Authorization = 'Bearer ' + accessToken
        }
        return config

    })
    // 响应的拦截器
    $axios.onResponse(
        response => {
            console.log('响应拦截器', response)
            // 模拟401信息
            // if(!store.state.accessToken){
            //     console.log("没有访问令牌")
            //     sendRefreshRequest(store, route, redirect)
            // }
            return response
        },
    )
    // 出现异常的拦截器
    $axios.onError(
        error => {
            // 拦截404请求,并转到登录页面效验
            if (error.response.status !== 401) {
                // 如果请求不含有401.则直接返回错误
                return Promise.reject(error)
            }

            sendRefreshRequest(store, route, redirect)
            return Promise.reject('令牌过期,重新登录')

        }
    )


}
// 锁, 防止并发重复请求, true 还未请求,false 正在请求刷新
let isLock = true

const sendRefreshRequest = (store, route, redirect) => {
    // 如果是第一次请求,且vueX状态管理中的refreshToken有值
    if (isLock && store.state.refreshToken) {
        // 有刷新令牌,防止并发重复请求刷新,
        isLock = false
        // 通过刷新令牌获取新令牌,
        redirect(`${process.env.authURL}/refresh?redirectURL=${redirectURL(route)}`)
    } else {
        // 如果进入到这里,说明没有刷新令牌,就跳转登录页面
        isLock = true
        // 没有刷新令牌,跳转到登录页
        // 注意不要使用 store.dispatch('LoginPage') ,因为 LoginPage 里面使用了window对象,nuxt服务
        //端是没有此对象的,
        store.commit('RESET_USER_STATE')
        // 服务端帮我们跳转到登录页
        redirect(`${process.env.authURL}?redirectURL=${redirectURL(route)}`)
    }


}
const redirectURL = (route) => {
    // 因为window.location.href这个只能运行在服务端,运行在客户端会报错,
    // 所以这里对本地路由地址,进行判断,并分条件返回值
    if (process.client) {
        // process.client 判断是不是属于服务器端
        return window.location.href

    }
    // 服务端 process.env._AXIOS_BASE_URL_ 
    // http://localhost:3000/api http://blog.mengxuegu.com/api
    return process.env._AXIOS_BASE_URL_.replace('api', '') + route.path


}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值