用户认证session与token中,前后端的交互方式

之前写过一篇关于用户认证的文章,不过是在理论方面。
最近去看了两个课程,里面涉及到了用户认证实战方面,自己写的时候遇到了许多问题,所以想写篇文章记录一下。

本文涉及技术栈有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>
posted @ 2022-05-06 00:57  sanhuamao  阅读(653)  评论(1编辑  收藏  举报