有点复杂有点长,一定要耐心看,看完之后你会对单点登录有个神奇的看法。
你能学到的知识: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.不会的一定要多查多问,不要怕丢人,因为谁一开始都是不会的。