泽恩小站-教程区 Help

用户登录

上一章做好了用户注册,本章来完成用户登录功能。

由于后端的认证方式为 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。

这种方式没有在每次请求中向后端确认用户是否登录,而是根据本地保存的信息进行判断**(当请求“无害”时)**,算是减轻后端压力的取巧办法。

看看效果

页面长相如图所示。

登录页面:

P250 1

正确登录后,跳转到首页并且右上角有了已登录提示:

P250 2

很好,就这样实现了基本功能。

读者可以在脚本中增加一些 console.log(...) ,打印看看逻辑是否正确;或者等个几分钟,看看 token 过期后登录状态是否会正常退出。还可以尝试修改代码,将简单的页面优化得更漂亮。发挥你的创造力吧。

课后作业

登录时若密码输入错误无任何页面提示(控制台有报错),请优化它。

Last modified: 06 January 2025