前言
经过第一章的登录页面的简单编写,完成了登录页面的简单交互。
正文
在登录页面已经能够将数据传输给后端并得到响应,接下来完成数据的存储、注册页面的编写等
- 数据的存储(在本地LocalStorage的存储、设置路由守卫)
- 设置令牌token(为数据生成token令牌、将所有请求添加token)
- 注册页面的编写(前端及后端的编写并完成数据的增加与查询)
将用户的数据存储
在用户重新打开页面时会存在需要重新登录的情况,如果制作的应用想让用户在一定时间内将不需要重新登录的话就需要将用户信息存储在本地
此外,在许多应用中可以体验到用户账号的个性化,将用户注册时输入的内容存储下来再将信息反馈给用户是很常见的交互体验
浏览器有这四种Cookies、LocalStorage、SessionStorage、IndexedDB
本地存储LocalStorage
- 在上一章的编写登录页面
Login.vue
中 - 绑定的
onSubmit
中添加LocalStorage
存储用户信息
localStorage.setItem('userInfo',JSON.stringify(res.data))//存储用户信息
const onSubmit = async (values) => {
// console.log('submit', values);//向后端发请求,将账号密码传给后端
const res = await axios.post('./user/login',values)
console.log(res);
localStorage.setItem('userInfo',JSON.stringify(res.data))//存储用户信息
if(res.code === 800){
router.push('/noteClass')
}
};
添加路由守卫
会发现一个问题,当没有完成登陆时,在网页输入http://localhost:5173/noteClass
主页地址也能直接进入,这不乱套了吗,因此在router中设置路由守卫来拦截这种情况
在Web开发中,路由守卫是一种常用的技术,用于控制访问权限,确保用户在访问某些路由之前必须满足特定条件(如登录状态)。
之前文章简单介绍过路由
熟练掌握这八个路由要点,轻松玩转路由! - 掘金 (juejin.cn)
我们设置一个前置的全局路由
// 添加路由守卫
const whilePath = ['/login','/register']
router.beforeEach((to,from,next) => {
document.title = to.meta.title;
if(!whilePath.includes(to.path)){//需要登录,不在whilePath中
if(!localStorage.getItem('userInfo')){
router.push('/login')
}else{
next()
}
}
next()
return
})
- 设置页面标题:根据将要跳转到的路由的
meta
对象中的title
属性,设置当前页面的标题。 - 判断是否需要登录:检查将要跳转的路由路径是否在
whilePath
数组中,如果不在,则表示该访问的页面(路由)需要登录才能访问。 - 检查本地存储中的用户信息:在本地存储LocalStorage检查是否存在该用户的信息,如果本地存储中没有
userInfo
,表示用户未登录,则将路由跳转到登录页;如果本地存储中有userInfo
,表示用户已登录,则继续执行下一个钩子。 - 执行下一个钩子:无论是否需要登录,都会执行下一个钩子函数,以完成路由的跳转。
后端加密鉴权,设置登录令牌(token)
让登录更安全,防止出现路由守卫被欺骗
一般企业都是将token令牌写在cookie
中,因为会在所有请求中自动携带,而这里为了学习写在了loclStorage
中
这个token将写在后端,在后端匹配到前端传来的参数是合法的之后,将数据加密加上token在返回给前端
第一章在user.js
中写了返回前端的数据,再该数据中添加令牌token
const data = {
id: result[0].id,
nickname: result[0].nickname,
username: result[0].username,
}
//生成token
const token = jwt.sign(data)
// console.log(token);
ctx.body = {
code: 800,
data: data,
msg: '登录成功',
token
}
生成token
看看阮一峰老师介绍的JSON Web Token
JSON Web Token 入门教程 - 阮一峰的网络日志 (ruanyifeng.com)
服务器认证以后,生成一个 JSON 对象,发回给用户,以后,用户与服务端通信的时候,都要发回这个 JSON 对象。
服务器完全只靠这个对象认定用户身份。为了防止用户篡改数据,服务器在生成这个对象的时候,会加上签名(加密后再次加密)。
服务器就不保存任何 session 数据了,也就是说,服务器变成无状态了,从而比较容易实现扩展。
使用第三方的库jsonwebtoken来生成token
jsonwebtoken - npm (npmjs.com)
其中详细介绍了使用方法及原理
将需要加密的数据传进来就会自动帮你加密,并可以添加自定义的参数进去,再次防止被解密,另外也自带校验
token
的方法
在终端输入npm install jsonwebtoken
引入jwt.js
中使用
var jwt = require('jsonwebtoken');
function sign(option) {
return jwt.sign(option,'666',{
expiresIn: 86400 //token的有效时长
})
}
module.exports={
sign
}
定义了一个名为sign
的函数,创建并返回一个JSON Web Token (JWT)。
从jsonwebtoken库中导入的jwt.sign
方法
option
: 这是传递给jwt.sign
的第一个参数,为user.js中传入的data(包含id、nickname,、username)。'666'
: 这是传递给jwt.sign
的第二个参数,即用于签名JWT的密钥(secret)。这个密钥是一个安全的字符串,用于对JWT进行加密签名,确保其在传输过程中的完整性和安全性。{ expiresIn: 86400 }
: 这是传递给jwt.sign
的第三个参数,是一个配置对象,其中expiresIn
属性指定了JWT的有效期限。这里的86400
是一个秒数,等于一天(24小时 * 60分钟 * 60秒)。这意味着生成的JWT将在一天后过期。
LocalStorage本地存储token
接着将得到的token在本地存储一下LocalStorage
localStorage.setItem('token',res.token)
设置请求拦截使得token被请求带上
需要在每一次请求都带上这个token:在登录、注册、点击页面等等需要检查用户信息的情况上都带上这个token,于是可以将他设置在请求拦截当中,每次请求都带上它
还记得在第一章与 后端数据交互(使用到axios) 中设置了响应拦截来处理来自服务器的HTTP响应,接着来设置请求拦截
// 请求拦截
axios.interceptors.request.use(req => {
// 获取token
const jwtToken = localStorage.getItem('token');//每次请求都携带token
if (jwtToken) {
req.headers.Authorization = jwtToken;//为所有请求添加请求头
}
return req;
});
在每个HTTP请求发送之前,自动地向请求头部添加一个名为Authorization
的字段,该字段包含了从浏览器的localStorage
中获取的JWT(JSON Web Token)令牌。
- 请求拦截器注册: 使用
axios.interceptors.request.use
来注册一个请求拦截器。这意味着在Axios发送任何请求之前,这个拦截器函数会被调用。 - 获取JWT Token: 在拦截器函数中,首先通过
localStorage.getItem('token')
尝试从本地存储中读取名为token
的项。这通常是在用户登录成功后存储的JWT令牌。 - 添加Authorization Header: 如果
localStorage
中存在token
,则将其赋值给jwtToken
变量。接下来,如果jwtToken
有值,那么就将它添加到请求头中,键名为Authorization
。这样,当请求被发送时,服务器可以基于此头信息验证请求的合法性。 - 返回修改后的请求配置: 最后,拦截器函数返回修改后的请求配置对象
req
。这样,Axios将使用带有Authorization
头的配置来发送请求。
用户首次访问或未登录: 当用户首次打开应用或尚未登录时,
localStorage
中不会有token
。在这种情况下,localStorage.getItem('token')
将返回null
。
那么至此登录页面算是真正完成了,介绍一下token
在Web应用中,使用Token进行身份验证和授权是一种常见的做法。Token可以提供一种安全的方式来验证用户身份,并控制对资源的访问。以下是Token的一些关键特点和使用方式:
Token的作用
- 身份验证:Token可以包含用户的身份信息,后端可以通过验证Token来确认请求者的身份。
- 授权:Token还可以包含权限信息,用于控制用户对特定资源的访问权限。
- 安全性:Token通常是加密的,这使得它们难以被篡改或伪造。
- 无状态:Token是自包含的,不需要在服务器端存储会话信息,这有助于扩展应用的可伸缩性。
Token的类型
- Access Token:用于访问受保护的资源,通常包含用户的身份信息和权限信息。
- Refresh Token:用于获取新的Access Token,通常在Access Token过期时使用。
- JWT (JSON Web Tokens) :是一种常见的Token格式,包含头部、载荷和签名三部分。
Token的存储
- Cookies:Token存储在Cookies中,会在每个HTTP请求中自动发送给服务器。这使得Token的传输变得方便,但也容易受到跨站请求伪造(CSRF)攻击。
- LocalStorage:Token存储在LocalStorage中,需要在每次请求中手动发送。这种方式可以避免CSRF攻击,但需要额外的代码来处理Token的发送。
Token的生成和验证
- 生成Token:在用户登录成功后,后端生成一个Token,并将其发送给前端。
- 验证Token:在每次请求中,前端将Token发送到后端,后端验证Token的有效性,并根据Token中的信息提供相应的服务。
注册页面
前端
有了第一章的铺垫,接下来的操作都是照葫芦画瓢般
注册页面(Register.vue
)和登陆页面(Login.vue
)大差不差
<template>
<div class="login">
<h1>注册</h1>
<div class="login-wrapper">
<div class="avatar">
<img src="https://tse1-mm.cn.bing.net/th/id/OIP-C.XFX8QbYxlWx9oG2UlkbLbwHaJ0?w=153&h=204&c=7&r=0&o=5&dpr=1.5&pid=1.7" alt="">
</div>
<van-form @submit="onSubmit">
<van-cell-group inset>
<van-field
v-model="username"
name="username"
label="用户名"
placeholder="用户名"
:rules="[{ required: true, message: '请填写用户名' }]"
/>
<van-field
v-model="password"
type="password"
name="password"
label="密码"
placeholder="密码"
:rules="[{ required: true, message: '请填写密码' }]"
/>
<van-field
v-model="nickname"
name="nickname"
label="昵称"
placeholder="昵称"
:rules="[{ required: true, message: '请填写昵称' }]"
/>
</van-cell-group>
<div style="margin: 16px;">
<van-button round block type="primary" native-type="submit">
注册
</van-button>
</div>
</van-form>
</div>
<p class="register" @click="router.push('/login')">已有账号?点击登录</p>
</div>
</template>
-
添加了一行昵称(
nickname
)的注册 -
更改
onSubmit
的跳转情况
const onSubmit = async (values) => {
const res = await axios.post('./user/register',values)
console.log(res);
if(res.code === 800){
showToast('恭喜宁,注册成功啦!');
setTimeout(() => {
router.push('/login')
}, 1000);
}
}
在注册成功后弹出注册成功的弹出框,并在1s之后跳转至登录页面
后端
收到了前端发来的数据,接着设置后端来解析数据和请求,并返回请求
使用koa路由
使用到@koa/router
@koa/router - npm (npmjs.com)
@koa/router中间件的使用
rotes/user.js
文件中添加访问/register
路由请求的情况
router.post('/register', async(ctx) => {
const { username, password,nickname } = ctx.request.body
if(!username || !password || !nickname){
ctx.body = {
code: 803,
msg: '用户名、密码、昵称不能为空',
data: 'error'
}
}
// 判断账号是否存在
const findRes = await userFind(username)
if(findRes.length > 0){//账号存在
ctx.body = {
code: 806,
msg: '账号已存在',
data: 'error'
}
return
}
// 往数据库添加用户
const res = await userRegister(username, password,nickname)
console.log(res);
if(res.affectedRows > 0){//添加成功(affectedRows:影响的行数)
ctx.body = {
code: 800,
msg: '注册成功',
data: 'success'
}
}else{//添加失败
ctx.body = {
code: 802,
msg: '注册失败',
data: 'error'
}
}
-
验证请求数据:检查这解构的三个字段
username, password,nickname
是否都存在且非空。如果有任何一个字段缺失,响应一个错误信息给客户端。 -
判断账号是否存在:调用
userFind
函数查询数据库,看是否有相同用户名的记录。如果查询结果有记录,说明用户已存在,返回错误信息。 -
往数据库添加用户:调用
userRegister
函数在数据库中注册新用户。根据affectedRows
判断注册是否成功,并相应地返回成功或失败的信息。
生产环境中应当启用
try-catch
块,来捕获异步操作中可能出现的异常
连接数据库的方法
专门来管理后端对数据库的增删改查../controllers/index
模块,在其中添加userFind
用户名是否存在的查找、userRegister
新用户注册的数据注入的方法
和登录路由中的userLogin
类似创建一样来创建userFind
、userRegister
方法
const userFind = (username) => {
const _sql = `select * from users where username="${username}";`
return allService.query(_sql)
}
const userRegister = (username,password,nickname) => {
const _sql = `insert into users (username,password,nickname) values ("${username}","${password}","${nickname}");`
return allService.query(_sql)
sql语句不会的时候可以问问百度吧
结语
第二章完成丰富登录页面的功能,使得用户的信息得以被安全保护、也避免了用户的非正规操作即设置了路由守卫并给数据生成token令牌、创建了注册的功能让新用户的数据注入数据库,接下来的章节就开始搭建应用的主页面等等。