之前写过一篇关于用户认证的,不过是在理论方面https://www.cnblogs.com/sanhuamao/p/16027260.html。
最近去看了两个课程,里面涉及到了用户认证实战方面,自己写的时候遇到了许多问题,所以想写篇文章记录一下。本文涉及技术栈有nodejs、express、fetch、xhr、localstorage。前置知识可参考我的笔记https://github.com/sanhuamao1/nodejs或https://gitee.com/chenxiangzhi/nodejs,下面的实例保存在目录04用户认证中。
过程:客户端发送用户信息给服务端——服务端验证通过后,将数据通过session保存起来,然后把对应的cookie返回给客户端——客户端再次发送请求时携带cookie——服务端对比cookie,验证用户。
要点:
express.static()
实现express-session
app.js
const express=require('express') const app=express() //解析请求体 app.use(express.urlencoded({ extended: false })) app.use(express.json()) // 静态资源 app.use(express.static('public')) //使用中间件express-session const session=require('express-session') app.use( session({ secret: 'itheima', resave: false, saveUninitialized: true, }) ) // 涉及两个status——0成功,1失败 // 1 向session中存数据 app.post('/api/login',(req,res)=>{ const username=req.body.username const password=req.body.password // 这里会交给数据库检查,判断有没有这个用户 // 如果没有该用户,返回登陆失败;如果有,往下走 // 这边就省略该过程了,默认都通过 // 将用户信息保存在session中 req.session.user=req.body req.session.islogin = true res.send({status:0,msg:'登陆成功!'}) }) // 2 从session中获取数据 app.get('/api/getusername',(req,res)=>{ if(!req.session.islogin){ return res.send({status:1,msg:'请重新登陆!'}) } res.send({status:0,msg:'获取成功',username:req.session.user.username}) }) // 3 清空session app.post('/api/logout',(req,res)=>{ req.session.destroy() res.send({ status:0, msg:'已退出!' }) }) app.listen(8080,()=>{ console.log('你的服务器运行在:http://localhost:8080') })
讲个小插曲:
上次跟着官网的推荐无脑加了个cookie: { secure: true },发现无法获取session。
原因在于,它要求使用https,而我使用的是http,所以为了安全考量它不会给我保存cookie
于是发送完请求后我去查看了下cookie,确实没有保存!
所以如果发送了没有带cookie的请求,服务端是无法从session中获取数据的
login.html
<h1>首页</h1> <p id="ptxt"></p> <button id="btn">退出登录</button> <script> // 获取用户信息 let p=document.getElementById('ptxt') // 一进来就调用下,判断有没有登陆过 window.onload=function(){ //因为是同源,所以可以省略前面的域名 fetch('/api/getusername').then(res=>res.json()).then(res=>{ if (res.status !== 0) { alert(res.msg) location.href = './login.html' } else { p.innerHTML='欢迎您:' + res.username } }) } // 退出 let btn=document.getElementById('btn') btn.addEventListener('click',()=>{ fetch('/api/logout',{ method:'POST' }).then(res=>res.json()).then(res=>{ if (res.status === 0) { location.href = './login.html' } }) }) </script>
login.html
<div>账号:<input id="username" /></div> <div>密码:<input id="password" /></div> <button id="submit">登陆</button> <script> let submit=document.getElementById('submit') submit.addEventListener('click',(e)=>{ let username=document.getElementById('username').value let password=document.getElementById('password').value fetch('/api/login',{ method:'POST', //指明请求体的数据类型 headers: { 'Content-Type': 'application/json' }, body:JSON.stringify({ username, password }) }).then(res=>res.json()).then(res=>{ if (res.status === 0) { location.href = './index.html' } else { alert(res.msg) } }) })
过程:客户端发送用户信息——服务端验证通过后返回token——客户端将返回的token保存在localstorage
中——在此访问需要用户认证的网页时,通过头部的Authorization
字段携带token发送请求——服务端验证通过后返回目标页面
要点:
live serve
快速给页面创建服务器(其他方式也可以,反正就是要让页面和接口不同源)jsonwebtoken
(生成token)、express-jwt
(解析token)、cors
(跨域)app.js
const express=require('express') const app=express() const cors=require('cors') app.use(cors()) app.use(express.json()) //1.定义密钥 const secretKey="mimayo~" //2.生成jwt const enjwt=require('jsonwebtoken') //3.还原jwt:会自动把解析出来的数据挂载到req.auth的属性上 const {expressjwt: jwt}=require('express-jwt') app.use( jwt({ secret:secretKey, algorithms: ["HS256"], }).unless({path:[/^\/api\//]}) //// 设置以/api/开头的不需要访问权限 ) // 1.认证加密 app.post('/api/login',(req,res)=>{ const user=req.body let {username,password}=user // 01 当用户调用了登陆接口,需要对其用户信息进行验证,这里假设数据库只有下面这个账户 if(username!=='admin'||password!=='123456'){ return res.send({status:0,msg:'账号或密码错误'}) } // 02 验证成功后对用户信息进行加密 (不建议携带密码) const tokenStr=enjwt.sign( {username}, secretKey, {expiresIn:'10h'} //有效期,也可以把单位换成s(秒),以便进行token期限测试 ) // 03 返回加密后的token res.send({ status:200, message:'登陆成功', token:tokenStr }) }) //2 验证用户 app.get('/admin/getInfo',(req,res)=>{ // 通过req.auth可以获取到token解析后的信息 if(req.auth){ res.send({ status:200, msg:'获取数据成功', data:req.auth }) }else{ res.send({ status:200, msg:'没有权限!', data:req.auth }) } }) // 3 处理错误:解析token错误或过期(错误中间件写后面) app.use((err,req,res,next)=>{ if(err.name==='UnauthorizedError'){ res.send({ status:401, msg:'token已过期' }) } res.send({ status:500, msg:'请求错误' }) }) app.listen(8080,()=>{ console.log('你的服务器运行在:http://localhost:8080') })
login.html
<div>账号:<input id="username" /></div> <div>密码:<input id="password" /></div> <button id="submit">登陆</button> <script> document.getElementById('submit').addEventListener('click',(e)=>{ let username=document.getElementById('username').value let password=document.getElementById('password').value fetch('http://localhost:8080/api/login',{ method:'POST', headers: { 'Content-Type': 'application/json' }, body:JSON.stringify({ username, password }) }).then(res=>res.json()).then(res=>{ if (res.status === 200) { // 验证通过了,将token保存在localstorage中 localStorage.setItem('token',res.token) location.href = `./index.html` } else { alert(res.msg) } }) }) </script>
index.html
<h1>首页</h1> <p id="ptxt"></p> <button id="btn">退出登录</button> <script> window.onload=function(){ //1 拿到传过来的token let token=localStorage.getItem('token') if(!token){ //2 当没有token时 alert('您尚未登录,请登录后再执行此操作!') location.href = './login.html' }else{ //3 当有token时,发送请求。由于fetch跨域起来很麻烦,所以我采用的是xhr let xhr=new XMLHttpRequest() xhr.open('GET','http://localhost:8080/admin/getInfo') xhr.setRequestHeader("Authorization", "bearer "+ token) xhr.send() xhr.onload=function(){ let res=JSON.parse(this.response) if(res.status=='200'){ document.getElementById('ptxt').innerHTML="欢迎您,"+res.data.username }else{ alert(res.msg) } } } } let btn=document.getElementById('btn') btn.addEventListener('click',()=>{ localStorage.clear() //清空tokon location.href = './login.html' }) </script>