Django-Vue搭建个人博客:用户登录
10788 views, 2023/10/16 updated Go to Comments
上一章做好了用户注册,本章来完成用户登录功能。
由于后端的认证方式为 JWT 认证,即后端返回给前端一个 token,前端在请求的 Header 中附带此 token 令牌来证明身份。这就有个不可避免的问题:token 保存在前端的什么地方?
本教程将采用 token 保存在 localStorage
中,实现登录功能。
此问题有广泛的讨论,因为 token 无论是保存在 localStorage、sessionStorage 或者 cookie 中均存在某些情况下被盗取的可能。网络安全不是本教程重点关注的问题,因此为了入门平滑将 token 保存于 localStorage 中,更深入的对安全的讨论请见 HASURA、MDN以及Stackoverflow。有关 localStorage 的入门讲解看这里。
准备工作
为了便于测试,修改后端 setting.py
配置,将 token 的过期时间设置短一些:
# drf_vue_blog
...
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=1),
...
}
登录页面
上一章写 Login.vue
时已经给登录的表单留好了位置,修改对应位置的代码:
<!-- frontend/src/views/Login.vue -->
<template>
...
<div id="grid">
...
<div id="signin">
<h3>登录账号</h3>
<form>
<div class="form-elem">
<span>账号:</span>
<input v-model="signinName" type="text" placeholder="输入用户名">
</div>
<div class="form-elem">
<span>密码:</span>
<input v-model="signinPwd" type="password" placeholder="输入密码">
</div>
<div class="form-elem">
<button v-on:click.prevent="signin">登录</button>
</div>
</form>
</div>
</div>
...
</template>
<script>
...
export default {
name: ...,
components: {...},
data: function () {
return {
...
signinName: '',
signinPwd: '',
}
},
methods: {
signup() {...},
signin() {
const that = this;
axios
.post('/api/token/', {
username: that.signinName,
password: that.signinPwd,
})
.then(function (response) {
const storage = localStorage;
// Date.parse(...) 返回1970年1月1日UTC以来的毫秒数
// Token 被设置为1分钟,因此这里加上60000毫秒
const expiredTime = Date.parse(response.headers.date) + 60000;
// 设置 localStorage
storage.setItem('access.myblog', response.data.access);
storage.setItem('refresh.myblog', response.data.refresh);
storage.setItem('expiredTime.myblog', expiredTime);
storage.setItem('username.myblog', that.signinName);
// 路由跳转
// 登录成功后回到博客首页
that.$router.push({name: 'Home'});
})
// 读者自行补充错误处理
// .catch(...)
},
}
}
</script>
<style scoped>
#signin {
text-align: center;
}
...
</style>
回顾一下向后端请求 token 的返回值:
{
"refresh": "eyJ0eXA...nHbY",
"access": "eyJ0eXAi...G0Uk"
}
access
是真正用于用户身份认证的令牌。但此令牌有效时间通常比较短(安全考虑),过期后可用 refresh
令牌重新获得一个令牌。
再回过头来看这个登录用的 signin()
方法,它首先发送请求申请 token,成功后则把令牌、过期时间和用户名一并保存到 localStorage 中供后续使用,并将跳转到首页。
expiredTime 为1970年1月1日至过期时间的毫秒数,60000 即代表1分钟;此数值需要与令牌有效期保持一致。
显示登录状态
为了让用户在任意页面都知道自己是否处于登录状态,登录显示一般位于页眉中。
修改 BlogHeader.vue
如下:
<!-- frontend/src/compnents/Blogheader.vue -->
<template>
<div id="header">
...
<hr>
<div class="login">
<div v-if="hasLogin">
欢迎, {{username}}!
</div>
<div v-else>
<router-link to="/login" class="login-link">登录</router-link>
</div>
</div>
</div>
</template>
<script>
import axios from 'axios';
export default {
name: ...,
data: function () {
return {
searchText: '',
username: '',
hasLogin: false,
}
},
methods: {...},
mounted() {
const that = this;
const storage = localStorage;
// 过期时间
const expiredTime = Number(storage.getItem('expiredTime.myblog'));
// 当前时间
const current = (new Date()).getTime();
// 刷新令牌
const refreshToken = storage.getItem('refresh.myblog');
// 用户名
that.username = storage.getItem('username.myblog');
// 初始 token 未过期
if (expiredTime > current) {
that.hasLogin = true;
}
// 初始 token 过期
// 如果有刷新令牌则申请新的token
else if (refreshToken !== null) {
axios
.post('/api/token/refresh/', {
refresh: refreshToken,
})
.then(function (response) {
const nextExpiredTime = Date.parse(response.headers.date) + 60000;
storage.setItem('access.myblog', response.data.access);
storage.setItem('expiredTime.myblog', nextExpiredTime);
storage.removeItem('refresh.myblog');
that.hasLogin = true;
})
.catch(function () {
// .clear() 清空当前域名下所有的值
// 慎用
storage.clear();
that.hasLogin = false;
})
}
// 无任何有效 token
else {
storage.clear();
that.hasLogin = false;
}
}
}
</script>
...
主要的改动就是 .mounted()
方法,在它里面一共干了三件事:
- 检查 localStorage 中保存的令牌过期时间,如果未过期则确认用户已登录。
- 若令牌已过期,检查是否能刷新获取令牌,若成功则确认用户已登录并更新 localStorage 的状态。
- 其他任何情况下均认为用户未登录,并清空 localStorage。
这种方式没有在每次请求中向后端确认用户是否登录,而是根据本地保存的信息进行判断(当请求“无害”时),算是减轻后端压力的取巧办法。
看看效果
页面长相如图所示。
登录页面:
正确登录后,跳转到首页并且右上角有了已登录提示:
很好,就这样实现了基本功能。
读者可以在脚本中增加一些 console.log(...)
,打印看看逻辑是否正确;或者等个几分钟,看看 token 过期后登录状态是否会正常退出。还可以尝试修改代码,将简单的页面优化得更漂亮。发挥你的创造力吧。
课后作业
登录时若密码输入错误无任何页面提示(控制台有报错),请优化它。
提示:补充
.catch()
语句。