Skip to content

Latest commit

 

History

History
238 lines (192 loc) · 8.23 KB

250-用户登录.md

File metadata and controls

238 lines (192 loc) · 8.23 KB

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

由于后端的认证方式为 JWT 认证,即后端返回给前端一个 token,前端在请求的 Header 中附带此 token 令牌来证明身份。这就有个不可避免的问题:token 保存在前端的什么地方?

本教程将采用 token 保存在 localStorage 中,实现登录功能。

此问题有广泛的讨论,因为 token 无论是保存在 localStorage、sessionStorage 或者 cookie 中均存在某些情况下被盗取的可能。网络安全不是本教程重点关注的问题,因此为了入门平滑将 token 保存于 localStorage 中,更深入的对安全的讨论请见 HASURAMDN以及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() 语句。