GCIP 项目中的登录逻辑
one 简化登录逻辑
① 点击登录按钮
② 前端验证输入格式
③ 后端验证用户名和密码是否匹配,返回token
④ state 和 cookie 存储 token
⑤ axios 获取 user 信息,存储 user 信息在 state 和 cookie 中
⑥ 路由跳转到 /english/ ,router.js 重定向成 /english/home
⑦ router.beforeEach((to, from, next) 有判断 token ,next(/${to.path.split(‘/‘)[1]}/news
) 跳转到新闻页面
two 与登录相关的文件
login.vue
<template>
<div class="login-container">
<el-row class="login-form">
<pncInput class="username" :placeholder="$t('common.username') | capitalize" v-model="form.username" :validator="validator.username"></pncInput>
<pncInput class="password" type="password" :placeholder="$t('common.password') | capitalize" v-model="form.password" :validator="validator.password" @enter="onLogin"></pncInput>
<el-row class="login-row">
<pncCheck v-model="check"></pncCheck>
<router-link class="href" :to="`/${$route.meta.lang}/reset`">?</router-link>
</el-row>
</el-row>
<el-row class="button-sec">
<pncButton @click="onLogin" :loading="loading"></pncButton>
</el-row>
</div>
</template>
<script>
export default {
data() {
return {
i18n: {
en: {
reset: 'Forget Password',
label: 'Remember me',
question: {
question: 'No account',
jumper: 'Register'
}
},
cn: {
reset: '忘记密码',
label: '记住密码',
question: {
question: '没有账号',
jumper: '注册'
}
}
},
loading: false,
form: {
username: '',
password: ''
},
check: true,
source: this.$axios.CancelToken.source()
};
},
computed: {
validator() {
return {
username: {
rule: this.form.username.length > 0,
tip: this.$i18n.messages[this.$i18n.locale].common.form.tip.username
},
password: {
rule: this.$utils.passwordValid(this.form.password),
tip: this.$i18n.messages[this.$i18n.locale].common.form.tip.password1
}
};
}
},
methods: {
//登录完了保存token
onLogin() {
if (this.$utils.isValid(this.validator)) {
this.loading = true;
this.$axios.post(this.$url.login, this.form, {
cancelToken: this.source.token
}).then(res => {
this.goSetToken(res.data.token);
}).catch(err => {
this.loading = false;
this.$error(err, {
400: {
en: 'The username or password is wrong, please retry or find your password',
cn: '用户名或密码有误,请重新输入或找回密码'
}
});
});
} else {
this.$message.error(this.$i18n.messages[this.$i18n.locale].common.form.tip.invalid);
}
},
//保存完token去获取user信息
goSetToken(token) {
this.$store.dispatch('SetToken', token).then(() => {
this.getUserData(this.form.password);
}).catch(err => {
this.loading = false;
this.$message.error('login failed,please retry');
});
},
//获取并保存user信息
getUserData(password) {
this.$utils.getUserData(password).then(() => {
this.$router.push(`/${this.$route.meta.lang}/`);
}).catch(err => {
this.loading = false;
this.$error(err);
});
}
},
beforeDestroy() {
this.source.cancel();
this.loading = false;
}
};
</script>
pncInput.vue
<template>
<div class="pncInput">
<input :class="{disabled:disabled,padding:padding}" :type="type" :value="value" :name="name" :style="style" :placeholder="placeholder" :disabled="disabled | isDisabled" @keyup.enter="$emit('enter')" @blur="onTest" @input="onInput">
<div :class="['tip',{'is-wrong':isWrong}]" v-if="validator"></div>
</div>
</template>
<script>
export default {
props: {
type: {
type: String,
default: 'text'
},
placeholder: String,
value: [String, Number],
name: String,
disabled: {
type: Boolean,
default: false
},
validator: Object,
padding: {
type: Boolean,
default: true
},
direction: {
type: String,
default: 'left'
}
},
data() {
return {
currentValue: this.value,
isWrong: false,
style: {
'text-align': this.direction
}
};
},
watch: {
'value' (val, oldValue) {
this.currentValue = val;
this.onTest();
},
'currentValue' (val) {
this.$emit('input', val);
this.$emit('change', val);
}
},
methods: {
onInput(event) {
this.currentValue = event.target.value;
},
onTest() {
this.validator ? this.isWrong = !this.validator.rule : null;
}
}
};
</script>
子组件在传值的时候,选用input,如this.$emit(‘input’,val),在父组件直接用v-model绑定,就可以获取到了
store/user.js
import {
loginByEmail,
logout,
getInfo
} from 'api/login';
import Cookies from 'js-cookie';
import axios from 'axios';
import url from '@/url.js';
import Vue from 'vue';
const user = {
state: {
token: Cookies.get('Token'),
groups: Cookies.get('Groups') ? JSON.parse(Cookies.get('Groups')) : [],
username: Cookies.get('UserInfo') ? JSON.parse(Cookies.get('UserInfo')).username : '',
password: Cookies.get('UserInfo') ? JSON.parse(Cookies.get('UserInfo')).password : '',
},
mutations: {
SET_TOKEN: (state, token) => {
state.token = token;
token ? Cookies.set('Token', token) : Cookies.remove('Token');
},
SET_GROUPS: (state, groups) => {
state.groups = groups;
groups.length ? Cookies.set('Groups', groups) : Cookies.remove('Groups');
},
SET_USERNAME: (state, username) => {
state.username = username;
},
},
actions: {
// 设置token
SetToken({
commit
}, token) {
return new Promise((resolve, reject) => {
commit('SET_TOKEN', token);
resolve();
}).catch(err => {
console.log(err);
});
},
//设置groups
SetGroups({
commit
}, groups) {
return new Promise((resolve, reject) => {
if (groups.length) {
let temp = [];
for (let group of groups) {
temp.push(group.name);
}
commit('SET_GROUPS', temp);
resolve(temp);
} else {
commit('SET_GROUPS', ['none']);
resolve(['none']);
}
}).catch(err => {
console.log(err);
});
},
//保存个人信息
SetUserInfo({
commit
}, userInfo) {
return new Promise((resolve, reject) => {
let user = {
id: userInfo.pk,
username: userInfo.username,
password: userInfo.password,
firstname: userInfo.first_name,
lastname: userInfo.last_name,
email: userInfo.email,
city: userInfo.city,
company: userInfo.company_name,
position: userInfo.position,
photo: userInfo.photo,
birthdate: userInfo.birthdate,
gender: userInfo.gender,
addres: userInfo.addres
};
Cookies.set('UserInfo', user);
commit('SET_ID', user.id);
commit('SET_USERNAME', user.username);
commit('SET_PASSWORD', user.password);
commit('SET_FIRSTNAME', user.firstname);
commit('SET_LASTNAME', user.lastname);
commit('SET_EMAIL', user.email);
commit('SET_CITY', user.city);
commit('SET_COMPANY', user.company);
commit('SET_POSITION', user.position);
commit('SET_PHOTO', user.photo);
commit('SET_BIRTHDATE', user.birthdate);
commit('SET_GENDER', user.gender);
commit('SET_ADDRES', user.addres);
resolve();
}).catch(err => {
console.log(err);
});
},
}
};
export default user;
utils.js
//登录前流程,获取groups和user
export function getUserData(password) {
return new Promise((resolve, reject) => {
//获取groups
axios.get(url.groups).then(groupsRes => {
//保存groups
store.dispatch('SetGroups', groupsRes.data.groups).then(() => {
//获取user信息
axios.get(url.user).then(userRes => {
//保存user信息
password ? userRes.data.password = password : null
store.dispatch('SetUserInfo', userRes.data).then(() => {
resolve();
}).catch(err => {
reject(err);
});
}).catch(err => {
reject(err);
});
}).catch(err => {
reject(err);
});
}).catch(err => {
reject(err);
});
}).catch(err => {
console.log(err);
});
}
router.js
/*多语言constantRouter生成函数,所有权限都可以访问*/
const constantRouterGenerator = function (lang) {
let constantRouter = [{
path: '/',
redirect: '/english/'
}, {
path: `/${lang}/`,
redirect: `/${lang}/home`,
component: Layout,
children: [{}]
}
}
main.js
//刷新token
setInterval(function () {
let token = Cookies.get('Token') || '';
token ? refreshToken(token) : null;
}, 420000);
//刷新失败则再次刷新
function refreshToken(token) {
axios.post(url.refreshToken, {
token
}).then(res => {
store.dispatch('SetToken', res.data.token).then(res2 => {}).catch(err2 => {});
}).catch(err => {
login().then(() => {
axios(err.config);
});
});
}
//login的多种种方式
function loginMethod() {
return [url.login, {
username: store.getters.username,
password: store.getters.password
}];
}
//重新登录
function login() {
return new Promise((resolve, reject) => {
axios.post(loginMethod()[0], loginMethod()[1], {
headers: {
Authorization: ''
}
}).then(res => {
store.dispatch('SetToken', res.data.token).then(() => {
resolve();
}).catch(err => {
reject(err);
});
}).catch(err => {
reject(err);
});
}).catch(err => {
console.log(err);
});
}
//axios的请求拦截器,用来在每次发送请求前,如果存在token则加到headers里
axios.interceptors.request.use(function (config) {
let AUTH_TOKEN = store.getters.token;
let csrfToken = Cookies.get('csrftoken');
if (csrfToken) {
config.headers['X-CSRFToken'] = csrfToken;
}
if (AUTH_TOKEN && config.headers['Authorization'] !== '') {
config.headers['Authorization'] = `JWT ${AUTH_TOKEN}`;
}
return config;
}, function (err) {
return Promise.reject(err);
});
//axios的响应拦截器,可以用来处理错误信息
axios.interceptors.response.use(function (res) {
return res;
}, function (err) {
let config = err.config;
if (axios.isCancel(err)) { //被取消的axios
return Promise.reject('canceled');
}
console.log('error:', err.response ? err.response : err.message);
if (err.response) {
switch (err.response.status) {
case 401: //如果错误是401则直接重登陆,登录后再重试
console.log('去重登陆');
return login().then(() => {
console.log('重登成功');
return axios(config);
});
break;
case 404:
case 400: //404和400错误直接reject,不需要重试
return Promise.reject(err);
break;
default:
break;
}
}
if (!config || !config.retry) return Promise.reject(err);
config.__retryCount = config.__retryCount || 0;
if (config.__retryCount >= config.retry) {
return Promise.reject(err);
}
config.__retryCount += 1;
var backoff = new Promise(function (resolve) {
setTimeout(function () {
resolve();
}, config.retryDelay || 1000);
});
return backoff.then(function () {
return axios(config);
});
});
// 权限判断
function hasPermission(groups, permissionGroups) {
if (groups.indexOf('dev') >= 0 || groups.indexOf('all') >= 0 || groups.indexOf('test-user') >= 0) return true; // dev all 直接通过
if (!permissionGroups) return true; //目标没有分组限定
return groups.some(group => permissionGroups.indexOf(group) >= 0);
}
//多语言路由生成器
function langRouterGenerator(routers) {
let temp = [];
for (let lang of langs) {
for (let router of routers) {
temp.push(`/${lang}${router}`)
}
}
return temp;
}
const noReturn = ['/login', '/home']; //登陆后不能回到的页面
//router前
router.beforeEach((to, from, next) => {
NProgress.start(); //开启Progress
from ? store.dispatch('SetLastRoute', from).then().catch(err => {
console.log(err);
}) : null;
if (store.getters.token) { //判断是否有token
if (langRouterGenerator(noReturn).indexOf(to.path) !== -1) { //有token则不能再回到login页面了
next(`/${to.path.split('/')[1]}/news`);
NProgress.done();
} else if (hasPermission(store.getters.groups, to.meta.groups)) { //判断是否有权限进入
if (to.meta.source) { //来源受限路由
if (to.meta.source.indexOf(from.meta.id) >= 0 || to.meta.id === from.meta.id || from.name === null) { //控制某个路由只能由某个路由进入
next();
} else { //如果不是在source来源内的路由,则回退
next(false);
NProgress.done();
}
} else { //正常路由
next();
}
} else { //无权进入
next(`/${to.meta.lang}/401`);
NProgress.done();
}
} else if (langRouterGenerator(noLoginList).indexOf(to.path) >= 0 || to.path.indexOf('english/phone')) { //在免登录白名单,直接进入
next();
} else { //否则全部重定向到登录页
next(`/${to.path.split('/')[1]}/login`);
NProgress.done();
}
});
//router后
router.afterEach(() => {
window.scrollTo(0, 0); //跳转后回到页面顶部
Vue.prototype.$lang = utils.langSwitch(router.currentRoute.meta.lang); //简写的lang
i18n.locale = router.currentRoute.meta.lang; //全称的lang
axios.defaults.baseURL = url.base; //设置axios的根请求路径
document.title = `${router.currentRoute.name}-China Investor Market-Boutique Firm-Global Cloud Investment Platform-GCIP`;
NProgress.done(); // 结束Progress
});
three 详细登录逻辑
// login.vue
<pncButton @click="onLogin" :loading="loading"></pncButton>
// login.vue
//登录完了保存token
onLogin() {
if (this.$utils.isValid(this.validator)) {
this.loading = true;
this.$axios.post(this.$url.login, this.form, {
cancelToken: this.source.token
}).then(res => {
this.goSetToken(res.data.token);
}).catch(err => {
this.loading = false;
this.$error(err, {
400: {
en: 'The username or password is wrong, please retry or find your password',
cn: '用户名或密码有误,请重新输入或找回密码'
}
});
});
} else {
this.$message.error(this.$i18n.messages[this.$i18n.locale].common.form.tip.invalid);
}
},
① 点击登录按钮,执行 onLogin() 方法,
② 前端首先验证用户名和密码的格式 this.$utils.isValid(this.validator)
,若格式不正确弹出错误提示框“输入内容有误或未填写”,若格式正确,
③ 则 this.$axios.post(this.$url.login, this.form)
将用户名和密码发送给服务器,如果没有通过提示“用户名或密码有误,请重新输入或找回密码”,用户名和密码正确,后端会返回用户的token,然后执行 goSetToken(res.data.token) 方法,
// login.vue
// 保存完token去获取user信息
goSetToken(token) {
this.$store.dispatch('SetToken', token).then(() => {
this.getUserData(this.form.password);
}).catch(err => {
this.loading = false;
this.$message.error('login failed,please retry');
});
},
// store/user.js
state: {
token: Cookies.get('Token'),
},
mutations: {
SET_TOKEN: (state, token) => {
state.token = token;
token ? Cookies.set('Token', token) : Cookies.remove('Token');
},
},
actions: {
// 设置token
SetToken({
commit
}, token) {
return new Promise((resolve, reject) => {
commit('SET_TOKEN', token);
resolve();
}).catch(err => {
console.log(err);
});
},
}
④ goSetToken 方法 this.$store.dispatch('SetToken', token)
, mutations 中 SET_TOKEN 方法,将token保存在 vuex 的 state 中,并且 Cookies.set(‘Token’, token) 浏览器设置 cookie
// login.vue
//获取并保存user信息
getUserData(password) {
this.$utils.getUserData(password).then(() => {
this.$router.push(`/${this.$route.meta.lang}/`);
}).catch(err => {
this.loading = false;
this.$error(err);
});
}
⑤ goSetToken 方法保存完 token 后去获取 user 信息,执行 getUserData(this.form.password) 方法 this.$utils.getUserData(password),
// utils.js
//登录前流程,获取groups和user
export function getUserData(password) {
return new Promise((resolve, reject) => {
//获取groups
axios.get(url.groups).then(groupsRes => {
//保存groups
store.dispatch('SetGroups', groupsRes.data.groups).then(() => {
//获取user信息
axios.get(url.user).then(userRes => {
//保存user信息
password ? userRes.data.password = password : null
store.dispatch('SetUserInfo', userRes.data).then(() => {
resolve();
}).catch(err => {
reject(err);
});
}).catch(err => {
reject(err);
});
}).catch(err => {
reject(err);
});
}).catch(err => {
reject(err);
});
}).catch(err => {
console.log(err);
});
}
// store/user.js
state: {
groups: Cookies.get('Groups') ? JSON.parse(Cookies.get('Groups')) : [],
},
mutations: {
SET_GROUPS: (state, groups) => {
state.groups = groups;
groups.length ? Cookies.set('Groups', groups) : Cookies.remove('Groups');
},
},
actions: {
//设置groups
SetGroups({
commit
}, groups) {
return new Promise((resolve, reject) => {
if (groups.length) {
let temp = [];
for (let group of groups) {
temp.push(group.name);
}
commit('SET_GROUPS', temp);
resolve(temp);
} else {
commit('SET_GROUPS', ['none']);
resolve(['none']);
}
}).catch(err => {
console.log(err);
});
},
}
// store/user.js
const user = {
state: {
username: Cookies.get('UserInfo') ? JSON.parse(Cookies.get('UserInfo')).username : '',
password: Cookies.get('UserInfo') ? JSON.parse(Cookies.get('UserInfo')).password : '',
......
},
mutations: {
SET_USERNAME: (state, username) => {
state.username = username;
},
......
},
actions: {
//保存个人信息
SetUserInfo({
commit
}, userInfo) {
return new Promise((resolve, reject) => {
let user = {
id: userInfo.pk,
username: userInfo.username,
password: userInfo.password,
firstname: userInfo.first_name,
lastname: userInfo.last_name,
email: userInfo.email,
city: userInfo.city,
company: userInfo.company_name,
position: userInfo.position,
photo: userInfo.photo,
birthdate: userInfo.birthdate,
gender: userInfo.gender,
addres: userInfo.addres
};
Cookies.set('UserInfo', user);
commit('SET_ID', user.id);
commit('SET_USERNAME', user.username);
commit('SET_PASSWORD', user.password);
commit('SET_FIRSTNAME', user.firstname);
commit('SET_LASTNAME', user.lastname);
commit('SET_EMAIL', user.email);
commit('SET_CITY', user.city);
commit('SET_COMPANY', user.company);
commit('SET_POSITION', user.position);
commit('SET_PHOTO', user.photo);
commit('SET_BIRTHDATE', user.birthdate);
commit('SET_GENDER', user.gender);
commit('SET_ADDRES', user.addres);
resolve();
}).catch(err => {
console.log(err);
});
},
}
};
⑥ getUserData(password) 方法为登录前流程,获取 groups 和 user,axios.get(url.groups) 获取权限集合 ,store.dispatch('SetGroups', groupsRes.data.groups)
将用户权限存在 state 和 cookie 中,然后
⑦ axios.get(url.user) 获取user信息, store.dispatch('SetUserInfo', userRes.data)
将user 的各种信息存到state中,并且 Cookies.set(‘UserInfo’, user);
//获取并保存user信息
getUserData(password) {
this.$utils.getUserData(password).then(() => {
this.$router.push(`/${this.$route.meta.lang}/`);
}).catch(err => {
this.loading = false;
this.$error(err);
});
}
⑧ getUserData 方法成功之后 this.$router.push(/english/)
;
// main.js
//axios的请求拦截器,用来在每次发送请求前,如果存在token则加到headers里
axios.interceptors.request.use(function (config) {
let AUTH_TOKEN = store.getters.token;
let csrfToken = Cookies.get('csrftoken');
if (csrfToken) {
config.headers['X-CSRFToken'] = csrfToken;
}
if (AUTH_TOKEN && config.headers['Authorization'] !== '') {
config.headers['Authorization'] = `JWT ${AUTH_TOKEN}`;
}
return config;
}, function (err) {
return Promise.reject(err);
});
⑨ 然后全局 axios.interceptors.request.use() , axios的请求拦截器,用来在每次发送请求前,如果存在token则加到headers里, config.headers[‘Authorization’] = JWT ${store.getters.token}
;
// main.js
//router前
router.beforeEach((to, from, next) => {
NProgress.start(); //开启Progress
from ? store.dispatch('SetLastRoute', from).then().catch(err => {
console.log(err);
}) : null;
if (store.getters.token) { //判断是否有token
if (langRouterGenerator(noReturn).indexOf(to.path) !== -1) { //有token则不能再回到login页面了
next(`/${to.path.split('/')[1]}/news`);
NProgress.done();
} else if (hasPermission(store.getters.groups, to.meta.groups)) { //判断是否有权限进入
if (to.meta.source) { //来源受限路由
if (to.meta.source.indexOf(from.meta.id) >= 0 || to.meta.id === from.meta.id || from.name === null) { //控制某个路由只能由某个路由进入
next();
} else { //如果不是在source来源内的路由,则回退
next(false);
NProgress.done();
}
} else { //正常路由
next();
}
} else { //无权进入
next(`/${to.meta.lang}/401`);
NProgress.done();
}
} else if (langRouterGenerator(noLoginList).indexOf(to.path) >= 0 || to.path.indexOf('english/phone')) { //在免登录白名单,直接进入
next();
} else { //否则全部重定向到登录页
next(`/${to.path.split('/')[1]}/login`);
NProgress.done();
}
});
⑩ 全局router前,router.beforeEach((to, from, next)
⑪ 登录之后 store.getters.token 有 token,
// router.js
/*多语言constantRouter生成函数,所有权限都可以访问*/
const constantRouterGenerator = function (lang) {
let constantRouter = [{
path: '/',
redirect: '/english/'
}, {
path: `/${lang}/`,
redirect: `/${lang}/home`,
component: Layout,
children: [{}]
}
}
⑫ router.js 中 { path: /${lang}/
, redirect: /${lang}/home
} , /english/ 会 重定向成 /english/home ,
⑬ “/english/home” 在 [“/english/login”, “/english/home”, “/chinese/login”, “/chinese/home”]
⑭ 有token则不能再回到login页面了 ,执行 next(/${to.path.split(‘/‘)[1]}/news
);
⑮ 这样正确登录之后就来到了news 页面
前端通用登陆流程 common
❶ 在登录页点击登录的时候,前端会带着用户名和密码去调用后端的登录接口。
❷ 后端收到请求,验证用户名和密码,验证失败,会返回错误信息,前端提示相应错误信息,如果验证成功,就会给前端返回一个 token。
❸ 前端拿到 token,将 token 储存到 Vuex 和 localStorage 中,并跳转页面,即登录成功。
❹ 前端每次跳转至需要具备登录状态的页面时,都需要判断当前 token 是否存在,不存在就跳转到登录页,存在则正常跳转(通常封装在路由守卫中)。
❺ 另外,在向后端发送其他请求时,需要在请求头中带上 token (项目中通常封装在请求拦截器中),后端判断请求头中有无 token,有则验证该 token,验证成功就正常返回数据,验证失败(如已过期)则返回相应错误码。前端拿到错误信息,清除 token 并回退至登录页。
more
- 登录逻辑伪代码请看 github, 项目 v-login