Django restframework加Vue打造前后端分离的网站(七)登录并保持状态
restful api是一种无状态的请求,每个请求与其他请求隔离,也不会记录交互的场景,不在意反馈请求的服务端是哪一个。restful架构要求服务器端不保有任何与特定HTTP请求相关的资源,所以应用状态必须由请求方在请求过程中提供。
既然后端采用了restful api的实现方式,那么用了token验证方式也是一种无状态的,两者观念相符。
token相比session来说有如下区别:
1)减轻服务器压力,不用在服务器端维持用户session;
2)session是浏览器特有的,如果后端服务也需要支持手机app,那么token则是好一点的选择。
3)支持跨域,token可以少考虑CSRF跨站请求伪造的问题。
4)token是无状态的(stateless),session则是维持状态的(presist)
5)当有多服务器时,token需要同步。不过session也是只维持在一个服务器上的。
6)app或者单页面应用(SPA)或者第三方登录推荐用token,多页面应用(MPA)推荐用session。
--------------------------------------------------
前面说了这么多些,是因为打算用token验证,但是前端获取token后,刷新页面就会丢失登录状态。该篇文章会记录下如何集成登录以及保持状态。
我们可以用vuex来管理全局状态,可随时读取和修改state,从后端获取token并记录,跳转新页面做新的后台请求时,就会带上该token,否则返回登录页面,如果token过期也会更新该状态。就能记录用户的登录状态并能判断是否需要再次登录。
先安装vuex
npm install --save vuex
在frontend/src下创建store/index.js,分别有state / mutations / actions / getters, 在actions这里面写了login和logout两个方法,每次调用时会更新到state里的状态的值里面去,并存储在local storage中,因为虽然状态会保存在vuex中,但如果用户关闭浏览器状态就会丢失了,再次打开时若token仍然有效,应仍然让用户处于登录状态。
// store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
import axios from 'axios'
Vue.use(Vuex)
export default new Vuex.Store({
state: {
status: '',
token: localStorage.getItem('token') || '',
user : localStorage.getItem('user') || ''
},
mutations: {
auth_request(state){
state.status = 'loading'
},
auth_success_token(state, token){
state.status = 'success'
state.token = token
// state.user = user // 这里传递第二个参数却会是undefined,未找到原因,所以将token和user分成两个
},
auth_success_user(state, user){
state.status = 'success'
state.user = user
},
auth_error(state){
state.status = 'error'
},
logout(state){
state.status = ''
state.token = ''
state.user = ''
},
},
actions: {
login({commit}, user){
return new Promise((resolve, reject) => {
commit('auth_request')
axios({url: 'http://127.0.0.1:8000/automation/api/api-token-auth/', data: user, method: 'POST' })
.then(resp => {
const token = resp.data.token
const user = resp.data.username
localStorage.setItem('token', token)
localStorage.setItem('user', user)
axios.defaults.headers.common['Authorization'] = 'Token ' + token // 这里是在登录后将token加到默认的header中,便于后续调用需要登录权限的api
commit('auth_success_token', token)
commit('auth_success_user', user)
resolve(resp)
})
.catch(err => {
commit('auth_error')
localStorage.removeItem('token')
reject(err)
})
})
},
logout({commit}){
return new Promise((resolve, reject) => {
commit('logout')
localStorage.removeItem('token')
localStorage.removeItem('user')
delete axios.defaults.headers.common['Authorization']
resolve()
})
}
},
getters : {
isLoggedIn: state => !!state.token,
authStatus: state => state.status,
}
})
在main.js中引用该文件,并设置如果token存在就会带上默认的header,便于调用需要登录权限的api。
import Vue from 'vue'
import App from './App'
import router from './router'
import store from './store'
import axios from 'axios'
Vue.prototype.$http = axios;
const token = localStorage.getItem('token')
console.log(token)
if (token) {
Vue.prototype.$http.defaults.headers.common['Authorization'] = 'Token ' + token
}
Vue.config.productionTip = false
new Vue({
el: '#app',
router,
store,
components: { App },
template: '<App/>'
})
修改home.vue文件,添加login入口和一些状态展示,当然还是测试用。当logout时,就会销毁记录的状态和token。
<template>
<div>
<h3>{{ msg }}</h3>
<button type="button" @click='getProjects' :style="{ margin: '10px', padding: '5px' }">get projects</button>
<a v-if="this.$store.state.token" @click="logout" :style="{ margin: '10px', padding: '5px' }">Logout</a>
<router-link v-else to="/login" :style="{ margin: '10px', padding: '5px' }">Login</router-link>
<p>{{ auth_text }}</p>
<p>{{ this.$store.state.token }}</p>
<p>{{ this.$store.state.status }}</p>
<p>{{ this.$store.state.user }}</p>
<p>{{ this.$store.getters.isLoggedIn }}</p>
<p>{{ this.$store.getters.authStatus }}</p>
<table v-if="auth.length > 0" :style="{ border: '2px solid gray', borderRadius: '5px', padding: '10px' }" align='center'>
<tr v-for='a in auth'>
<td>{{ a.id }}</td>
<td>{{ a.username }}</td>
<td>{{ a.email }}</td>
<td>{{ a.token }}</td>
</tr>
</table>
<table :style="{ border: '2px solid gray', borderRadius: '5px', padding: '10px' }" align='center'>
<tr v-for='p in projects'>
<td>{{ p.id }}</td>
<td>{{ p.name }}</td>
<td>{{ p.create_time }}</td>
<td>{{ p.update_time }}</td>
</tr>
</table>
</div>
</template>
<script>
export default {
name: 'Home',
data () {
return {
msg: 'Projects in Automation Center',
projects: [],
auth: [],
auth_text: ''
}
},
created: function() {
if (this.$store.state.token) {
this.auth_text = "user already logged in ";
}
else {
this.auth_text = "no user logged in";
}
},
methods: {
getProjects() {
this.$http.get('http://127.0.0.1:8000/automation/api/projects/').then(response => {this.projects = response.data["results"], this.msg = 'get projects data from django api'});
},
logout: function () {
this.$store.dispatch('logout')
.then(() => {
this.$router.push('/login')
})
}
}
}
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
h1, h2 {
font-weight: normal;
}
ul {
list-style-type: none;
padding: 0;
}
li {
display: inline-block;
margin: 0 10px;
}
a {
color: #42b983;
}
</style>
并添加一个login.vue文件作为登录用,login.vue会调用刚才store中的login方法,并修改全局的状态,以获取是否有登录。
// login.vue
<template>
<div>
<form class="login" @submit.prevent="login">
<h1>Sign in</h1>
<label>username</label>
<input required v-model="username" type="text" placeholder="Name"/>
<label>Password</label>
<input required v-model="password" type="password" placeholder="Password"/>
<button type="submit">Login</button>
</form>
</div>
</template>
<script>
export default {
data () {
return {
username : "",
password : ""
};
},
methods: {
login: function () {
const { username, password } = this
this.$store.dispatch('login', { username, password })
.then(() => this.$router.push('/'))
.catch(err => console.log(err))
}
}
};
</script>
此时需要再router/index.js中添加login的路由。
// router/index.js
import Vue from 'vue'
import Router from 'vue-router'
import Home from '@/components/home'
import Login from '@/components/login'
Vue.use(Router)
export default new Router({
mode: 'history',
routes: [
{
path: '/',
name: 'home',
component: Home
},
{
path: '/login',
name: 'login',
component: Login
},
]
});
此时运行前端(npm run dev)和后端(python manage.py runserver)时,就能看到默认是未登录,也无法调用api。
但是登录后即使刷新页面,也会有token和登录状态的保留,不会丢失。
参考文章:
https://scotch.io/tutorials/handling-authentication-in-vue-using-vuex
https://blog.sqreen.com/authentication-best-practices-vue/
https://stackoverflow.com/questions/45384172/best-practice-for-storing-auth-tokens-in-vuejs