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
}