之前写过一篇关于用户认证的文章,不过是在理论方面。
最近去看了两个课程,里面涉及到了用户认证实战方面,自己写的时候遇到了许多问题,所以想写篇文章记录一下。本文涉及技术栈有nodejs、express、fetch、xhr、localstorage。前置知识可参考我的笔记,下面的实例保存在目录04用户认证中。
用户认证方式
- session方式的用户认证适合服务器端渲染的web开发模式,该模式有利于SEO,常见于主要功能是展示而没有复杂的交互的网站。
- token方式的用户认证适合前后端分离的web开发模式,该模式不利于SEO,常见于交互强的后台管理系统。是跨域资源共享的解决方案。
01 session认证
过程:客户端发送用户信息给服务端——服务端验证通过后,将数据通过session保存起来,然后把对应的cookie返回给客户端——客户端再次发送请求时携带cookie——服务端对比cookie,验证用户。
要点:
- 该实例将页面及接口设置为同源,通过
express.static()
实现 - 第三方中间件
express-session
- 共三个文件:app.js(服务器)、index.html(后台页)、login.html(登录页)
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)
}
})
})
02 token认证
过程:客户端发送用户信息——服务端验证通过后返回token——客户端将返回的token保存在localstorage
中——在此访问需要用户认证的网页时,通过头部的Authorization
字段携带token发送请求——服务端验证通过后返回目标页面
要点:
- 使用vscode的扩展程序
live serve
快速给页面创建服务器(其他方式也可以,反正就是要让页面和接口不同源) - 第三方中间件
jsonwebtoken
(生成token)、express-jwt
(解析token)、cors
(跨域) - 注册拦截错误的中间件
- 共三个文件:app.js(服务器)、index.html(后台页)、login.html(登录页)
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,并自动对接口进行用户认证(除了api接口),会把解析出来的数据挂载到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 验证用户
// 由于这不是/api接口,所以jwt中间件会自动对该接口进行用户验证,而不需要开发者自己写。
app.get('/admin/getInfo',(req,res)=>{
// 通过req.auth可以获取到token解析后的信息
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>