前言
什么是登录鉴权?
举个例子:
想象一下,你去高档酒店入住:
- 你到前台,出示身份证(用户名/密码 或 手机验证码 或 刷脸)。
- 前台工作人员(系统)核对你的身份证信息是否真实有效,并且和预订记录匹配(鉴权)。
- 核对无误后,前台给你一张房卡(登录成功,给你一个访问凭证,比如 Session 或 Token)。
- 之后在酒店里,你去餐厅、健身房、坐电梯,只需要刷房卡(系统自动用你的访问凭证进行鉴权),工作人员就知道你是住客,有权限使用这些设施,不用每次都掏身份证了。
- 如果你退房了,房卡就失效了(访问凭证过期或失效),不能再使用酒店设施。
看完这个例子,也许你已经略懂一些,也许还是不懂,那么接下来,我将借用一个简单的登录逻辑讲述登录鉴权的具体逻辑以及如何实现
背景
通常,用户通过输入账号密码完成登录后,会跳转到首页,随后前端会向后端发送首页所需的接口请求。
但如果用户在未登录的情况下直接访问首页地址,此时首页发送的接口请求,后端显然不应返回数据——因为这些数据可能涉及用户权限或隐私。(当然,若首页是纯静态页面,无需向后端请求数据,则登录鉴权的必要性会降低,因为没有需要保护的动态数据交互。)
因此,登录鉴权的核心就是防止未登录用户访问需要权限的页面或获取受保护的数据,确保系统资源和用户信息的安全性。
如何实现登录鉴权?
大体思路:在用户登录时,向后发送接口请求,账号密码都正确的情况下,后端会额外生成一个token令牌,并将其加入到响应体数据内返回给前端,之后前端会将令牌进行浏览器本地保存,并在后续的所有请求中,需要将该令牌(token)放到请求头中发送给后端,后端会根据请求中的令牌(token)来判断用户是否登录,如果用户未登录,后端会返回一个未登录的状态码401,前端会根据状态码来判断是否需要跳转到登录页面。
1. 使用jsonwebtoken生成加密的token令牌
官网www.npmjs.com/package/jso…
JSON Web Token入门教程www.ruanyifeng.com/blog/2018/0…
- 在后端文件中安装jisonwebtoken
npm i jsonwebtoken
- 在后端文件夹中创建一个jwt文件,该文件内可编写token生成函数,token校验函数
- 引入jsonwebtoken,使用自带的sign方法创建token令牌
//options(使用该函数传进来的参数)包含要存储在 token 中的实际数据,自己找个独一无二的数据(如用户的ID),该数据不会进行加密
//'666'为自己设置的固定密钥,只有后端知道(什么都可以),用于签发和验证 token 的密码
//expiresIn为token令牌的有效时间
function sign(options){
return jwt.sign(options,'666',{
expiresIn:'24h'
})
}
- 将token加入到登录接口请求的响应体中
router.prefix('/user')//路由前缀,表示所有路由都以/user开头
router.post('/login',async (ctx)=>{
//1.获取请求体中的账号密码
//post请求携带的参数都在请求体中
const {username,password} = ctx.request.body//从请求体中解构账号密码
//2.验证账号密码是否正确
//去数据库中查询账号密码是否正确
try{
const res = await userLogin(username,password)//res为查找的结果集,为一个数组,就一个元素
if(res.length){//当数组不为空时(引用类型转布尔类型都为true)
let data={
id:res[0].id,
username:res[0].username,
nikeName:res[0].nikeName,
create_time:res[0].create_time,
}
const token=sign(data)//生成token令牌
ctx.body={
code:'1',//逻辑成功
msg:'登录成功',
data,
token:token//加入到响应体数据中
}
}else{
ctx.body={
code:'0',//逻辑性错误,账号或密码错误,而不是程序或代码错误
msg:'账号或密码错误',
}
}
}catch(error){//程序性错误(代码错误)userLogin报错
ctx.body={
code:'-1',//程序或代码错误
msg:'服务器异常',
data:error
}
}
})
2. 前端将接收的token进行浏览器本地保存(localStorage)
const onFinish = values => {
axios.post('/user/login',values).then(res=>{
toast.success('登录成功')
navigate('/noteClass')
localStorage.setItem('token',res.token)//登录成功后,将令牌(token)存储到浏览器本地
}).catch(err=>{
console.log(err)
})
}
登录成功后会自动跳转到首页,首页发送接口请求给后端进行token验证,考虑到一个项目于不止首页一个页面需要token验证,如果每个页面都各自在请求头中加入token,这会很麻烦,所以我们使用axios中的请求拦截器,当前端向后端使用axios发送请求时,都会先被该请求拦截器拦截下来,在请求头上加上token之后再放行。
3. 使用axios请求拦截器,将token加入到请求头中
在axios的配置文件内加入
axios.interceptors.request.use(request => {//request表示前端发送的请求数据
const token = localStorage.getItem('token')//从浏览器本地获取token
if (token) {//如果令牌(token)存在,将令牌(token)放到请求头中
request.headers.Authorization = token//将令牌(token)放到请求头中
}
return request//返回请求数据(放行)
})
4. 首页发送测试接口请求text
axios.get('/user/text').then(res=>{
console.log(res)
})
5. 后端进行token验证
- 首先在jwt文件中打造一个token验证函数
// 验证token是否正确
function verify(){
//ctx最终为index.js中的use方法中的ctx参数
//next为内置的,表示执行了app.use中的回调函数之后要运行一个next,这样后面的app.use()才会生效
return async (ctx,next)=>{
//1.获取请求头中的token
const token = ctx.request.headers.authorization
//2.验证token是否存在
if(token){
//3.验证token是否正确
try{
const decoded = jwt.verify(token,'666')//解密token
//判断decoded是否存在,因为能解密成功,说明token没有过期,且有id属性
if(decoded.id){
await next()//放行
}
}catch(error){//如果短token过期
ctx.status=401
ctx.body={
code:'0',
msg:'登录过期,请重新登录',
}
}
}else{//token不存在时
ctx.status=401//状态码设为401
ctx.body={
code:'0',
msg:'请先登录'
}
}
}
}
如果touken过期了则返回状态码401给前端,告诉前端登录过期,需重新登录
- 调用verify()进行token验证
//测试接口
//当verify()运行成功后证明token存在且未过期,则接着执行后面的回调函数
router.get('/text',verify(),async (ctx)=>{
ctx.body={
code:'1',
msg:'测试成功',
}
})
同理,如果每个页面都返回一个401给前端,那么前端就需各自进行处理,这样太麻烦,所以我们使用axios的响应拦截器,后端传过来的数据先被响应拦截器拦截,并进行统一处理后再给各个页面。
6.使用axios拦截器进行不同状态码的处理
//响应拦截器
axios.interceptors.response.use(
//http状态码为200时才进第一个回调函数
(response) => {//response表示后端返回的数据
if (response.status === 200) {
if (response.data.code !== '1') {//逻辑性错误
toast.error(response.data.msg)
return Promise.reject(response)//返回一个失败的Promise对象,这样前端就可以在catch中处理异常情况
}
return Promise.resolve(response.data)//返回成功的Promise对象,这样前端就可以在then中处理成功情况
}
},
//http状态码为不为2开头时才进第二个回调函数
(res) => {
//后端返回的短token过期的状态码为401
if (res.status === 401) {
toast.error(res.response.data.msg)
setTimeout(() => {
window.location.href = '/login'//使用原生js跳转方式,跳转到登录页
}, 2000)
}
}
)
小结
至此完成了登录鉴权的全部逻辑,但是,这并不完美,当token的有效时间过了之后,用户又要重新登录,这样会很影响用户的使用体验,那么有没有更完美的办法,使得用户不用频繁登录呢?
当然有,这就是我们下期要说的双token无感刷新