Django-Vue搭建个人博客:用户登录
10384 views, 2021/03/25 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()
语句。