用户登录
上一章做好了用户注册,本章来完成用户登录功能。
由于后端的认证方式为 JWT 认证,即后端返回给前端一个 token,前端在请求的 Header 中附带此 token 令牌来证明身份。这就有个不可避免的问题: token 保存在前端的什么地方?
本教程将采用 token 保存在 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 中供后续使用,并将跳转到首页。
显示登录状态
为了让用户在任意页面都知道自己是否处于登录状态,登录显示一般位于页眉中。
修改 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 过期后登录状态是否会正常退出。还可以尝试修改代码,将简单的页面优化得更漂亮。发挥你的创造力吧。
课后作业
登录时若密码输入错误无任何页面提示(控制台有报错),请优化它。
Last modified: 06 January 2025