前言
最近在帮校友写一个项目,欣音悦(以歌单歌单分享为主的音乐 webapp)。这是一个类似于 QQ 音乐的 webapp 目前还在开发中,其中歌曲数据和歌手数据来自于 QQ 音乐的接口,而歌单数据则存在本地数据库中。校友的需求是用户能添加歌单并分享,能听歌,能评论。所以就涉及用户登录状态保持,而我也是首次尝试使用后 token 方案进行登录处理,所以就写这篇博文记录下。
整理思路
- 在用户首次登录的时候,服务器会判断用户登录信息是否匹配(账户密码等),若匹配则生成一个包含相关用户信息的 token,并返回给前端,前端可以将这个 token 解析得到相关用户数据。
- 前端拿到 token 后,会将它存在 localStorage 或 Vuex 中。
- 而每次前端路由跳转时,判断 localStorage 或 Vuex 中是否存在 token,若存在则获取用户信息,改变登录状态。若不存在则跳转到登录组件。
- 设置一个 HTTP 拦截器,在每次请求中,请求头携带 token。
- 服务器判断请求头中是否存在 token ,若不存在或过期,返回 401 。
- 前端得到 401 则重定向到登录组件。
后端接口
在写这个项目的时候,服务器我采用的是 Node.js ,而 WEB 框架,则采用 Koa2 ,数据库则使用 Mongodb 。
生成 token ,需要一个模块 jsonwebtoken ,使用 npm 安装即可。
在服务器中处理用户信息相关接口的路由文件 users.js 中的登录接口,处理逻辑如下:
- 将用户 E-mail 和 password 从执行上下文提取出来后,与数据库中的信息进行匹配。
- 若成功,则生成 token 返回。
具体登录接口代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
|
router.post('/login', async ctx => { const findResult = await User.find({ email: ctx.request.body.email }) if (!findResult.length) { ctx.state = 404 ctx.body = { email: '用户不存在!' } } else { const user = findResult[0] const result = await bcrypt.compareSync( ctx.request.body.password, user.password ) if (result) { const payload = { id: user.id, name: user.name, avater: user.avatar } const token = jwt.sign(payload, keys.secretOrKey, { expiresIn: 60 * 60 })
ctx.state = 200 ctx.body = { success: true, token: `Bearer ${token}` } } else { ctx.state = 400 ctx.body = { password: '密码错误!' } } } })
|
对登录接口进行测试,邮箱和密码正确的话可以看到,此时,服务器已经返回给了一个 token。
前端处理
在前端正确登录,拿到 token 后,就需要将她存储起来,也就是在登录组件中,对token进行存储:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| methods: { _login() { const userInfo = { email: this.email, password: this.password } login(userInfo).then(res => { if (res.success) { localStorage.setItem('token', res.token) location.replace('/') } }).catch(err => { throw err }) } }
|
上面代码中的 login 方法,已经封装到 useruser.js 中了,直接调用就行,其主要代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| export function register(userInfo) { const url = 'http://localhost:5000/api/users/register'
const data = Object.assign({}, userInfo)
return axios .post(url, data) .then(response => { return Promise.resolve(response.data) }) .catch(err => { throw err }) }
|
在 router 的 index.js 中,就可是使用 loaclStorage.token 来进行前端路由拦截,非登录页面若 localStorage.token 不存在,则跳转到登录页。
1 2 3 4 5 6 7
| router.beforeEach((to, from, next) => { if (to.path !== '/login' && !localStorage.token) { return next('/login') } next() })
|
效果如图所示