Cookie、Vuex状态管理器实现单点登录SSO
什么是单点登录?
单点登录即SSO(Single Sign-On),是一种统一认证和授权机制,指访问同一服务器不同应用中的受保护资源的同一用户,只需要登录一次,即通过一个应用中的安全验证后,再访问其他应用中的受保护资源时,不需要重新登录验证。
解决用户只需要登录一次就可以访问所有相互信任的应用系统,而不用重复登录。
单点登录的三种实现方式
-
方法1:登录成功之后将Cookie回写到多个域名下
这种办法可能十分简单,可以通过后端也可以用前端js实现,但维护工作十分复杂,同时对于增加站点也会特别痛苦。对于Cookie的销毁也是十分复杂的,因为还是要对所有域名下的Cookie进行删除。对于小型站点这种办法是十分可取的。如果Cookie的加密算法泄露,攻击者通过伪造Cookie则可以伪造特定用户身份。 -
方法2:jsonp
用户在父应用中登录后,跟Session匹配的Cookie会存到客户端中,当用户需要登录子应用的时候,授权应用访问父应用提供的JSONP接口,并在请求中带上父应用域名下的Cookie,父应用接收到请求,验证用户的登录状态,返回加密的信息,子应用通过解析返回来的加密信息来验证用户,如果通过验证则登录用户。同时这种办法需要很大的维护成本,每一次请求都要去固定的域下取相应的Cookie之后再做请求。维护十分头疼。 -
方法3 :引入一个中间态的Server(页面重定向)
通过父应用和子应用来回重定向中进行通信,实现信息的安全传递。
父应用提供一个GET方式的登录接口,用户通过子应用重定向连接的方式访问这个接口,如果用户还没有登录,则返回一个的登录页面,用户输入账号密码进行登录。如果用户已经登录了,则生成加密的Token,并且重定向到子应用提供的验证Token的接口,通过解密和校验之后,子应用登录当前用户。
单点登录SSO实现
登录认证流程
相比于单系统登录,sso需要一个独立的认证中心,只有认证中心能接受用户的用户名密码等安全信息,其他系统不提供登录入口,只接受认证中心的间接授权。
1.门户客户端要求登录时,输入用户名密码,认证客户端提交数据给认证服务器。
2.认证服务器校验用户名密码是否合法,合法响应用户基本令牌 userInfo 、访问令牌 access_token 、刷新令牌 refresh_token。
Vuex 登录信息状态管理
当登录成功后,后台响应的 userInfo、access_token、refresh_token 信息使用 Vuex 进行管理,并且将这些信息保存到浏览器 Cookie 中。
1.安装 js-cookie 和 vuex 模块
npm install --save js-cookie vuex
2.创建 Vuex.Store 实例
//store中index.js创建 session 状态模块
import Vue from 'vue' import Vuex from 'vuex'
import auth from './modules/session'
Vue.use(Vuex)
const store = new Vuex.Store({
modules: {
session
}
})
export default store
3.将 store 已添加到Vue 实例中
import Vue from 'vue'
import App from './App.vue'
import router from "./router"
import store from './store'
Vue.config.productionTip = false
new Vue({
router,
store,
render: h => h(App),
}).$mount('#app')
4.Vuex中modules创建认证状态模块文件
//modules目录下创建添加对 userInfo、access_token、refresh_token 状态管理
import { PcCookie, Key } from '@/utils/cookie'
// 定义状态
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.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.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) {
console.log('UserLogin', userData)
const { username, password } = userData
return new Promise((resolve, reject) => {
login({ username: username.trim(), password: password }).then(response => {
// 获取响应值
const { code, data } = response
if(code === 20000) {
// 状态赋值
commit('SET_USER_STATE', data)
}
resolve(response) // 正常响应钩子
}).catch(error => {
// 重置状态
commit('RESET_USER_STATE')
reject(error) // 异常
})
})
},
// 退出
UserLogout({ state, commit }, redirectURL) {
// 调用退出接口, 上面不要忘记导入 logout 方法
logout(state.accessToken).then(response => {
// 重置状态
commit('RESET_USER_STATE')
// 重写向回来源地址,如果没有传重定向地址则回到到登录页
window.location.href = redirectURL || '/'
}).catch(error => {
// 重置状态
commit('RESET_USER_STATE')
window.location.href = redirectURL || '/'
})
},
// 发送刷新令牌
SendRefreshToken({ state, commit }) {
return new Promise((resolve, reject) => {
// 判断是否有刷新令牌
if( !state.refreshToken ) {
commit('RESET_USER_STATE')
reject('没有刷新令牌')
return
}
// 发送请求
refreshToken(state.refreshToken).then(response => {
console.log('获取到的新认证令牌', response.data)
// 更新用户状态新数据
commit('SET_USER_STATE', response.data)
resolve() // 正常钩子
}).catch(error => {
// 重置状态
commit('RESET_USER_STATE')
reject(error)
})
})
},
}
export default {
state,
mutations,
actions
}
5.设置cookie保存的域名(在 .env.development 和 .env.production文件)
//cookie保存的域名,utils/cookie.js 要用
VUE_APP_COOKIE_DOMAIN = 'location'
6.设置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 = 30 // 30 天
}
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()
提交登录触发 action
1.在登录页 login.vue 的 created 生命钩子里获取redirectURL,是引发跳转到登录页的引发跳转 URL ,登录成功后需要重定向回 redirectURL
data () {
return {
redirectURL: 'http//www.baidu.com', // 登录成功后重写向地址
},
created() {
if(this.$route.query.redirectURL) {
this.redirectURL = this.$route.query.redirectURL
}
},
2.登录成功后,重定向回到redirectURL参数值对应的页面,如果不带 redirectURL 重写向到 http//www.baidu.com
单点退出系统
定义 Vuex 退出行为
单点登录自然也要单点注销,在一个子系统中注销,所有子系统的会话都将被销毁.
actions 对象中添加调用 logout 退出api方法。退出成功后回到登录页。
const actions = {
UserLogout({ state, commit }, redirectURL) {
// 调用退出接口, 上面不要忘记导入 logout 方法
logout(state.accessToken).then(() => {
// 重置状态
commit('RESET_USER_STATE')
// 退出后,重写向地址,如果没有传重写向到登录页 / window.location.href = redirectURL || '/'
}).catch(() => {
// 重置状态
commit('RESET_USER_STATE') window.location.href = redirectURL || '/'
})
}
}
路由拦截器退出操作
router.beforeEach((to, from , next) => {
if(to.path === '/logout') {
// 退 出
store.dispatch('UserLogout', to.query.redirectURL)
}else {
next()
}
})
Vuex 发送请求与重置状态
actions 中 添加发送刷新令牌请求 行为
const actions = {
// 2. 发 送 刷 新 令 牌 ++++++++++++ SendRefreshToken({ state, commit }) {
return new Promise((resolve, reject) => {
// 判断是否有刷新令牌
if(!state.refreshToken) { commit('RESET_USER_STATE')
reject('没有刷新令牌')
return
}
// 发送刷新请求
refreshToken(state.refreshToken).then(response => {
// console.log('刷新令牌新数据', response)
// 更新用户状态新数据
commit('SET_USER_STATE', response.data) resolve() // 正常响应钩子
}).catch(error => {
// 重置状态commit('RESET_USER_STATE') reject(error)
})
})
}