React + Express 实现自动登录

947 阅读3分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 2 天,点击查看活动详情

你好,我是南一。这是我在准备面试八股文的笔记,如果有发现错误或者可完善的地方,还请指正,万分感谢🌹

现在有一个需求,我在网站登录过一次之后,下一次打开直接是登录状态,无需再次输入账号密码。

接下来我会用两种方法来实现:TokenSession。要实现自动登录,首先想到的是自动发送账号密码登录,这样的话前端需要存储账号密码,无非就是cookielocalstoragesessionstorage等等这些存储方法。但是这些都是可见的,账号密码非常容易泄露。因此需要一个不会暴露账号密码的方法,而且可以记录用户的状态。

这篇文章会涉及cookie、token、session这些知识点,不熟悉的同学可以看Cookie、Session、token小白科普版这篇文章了解一下每一个的工作原理。

以下例子适用于前后端分离的项目,本文前端采用React,服务端是express

服务端搭建

const express = require('express')
const app = express()
app.use(express.json())
app.use(express.urlencoded({ extended: true }))

app.listen(80, () => {
  console.log("express running at port 80...");
})

Session实现自动登录

工作原理:用户登录,服务端通过cookie将sessionID发送给浏览器,由于cookie设置了httpOnly,所以document.cookie取不到cookie。当登录成功在localstorage存一个登录标识。遇到//login这两个路由时,判断标识是否存在,存在就直接跳过登录页。如果session无效或者过期,服务端返回错误状态码10101。前端做请求拦截,遇到10101报错,跳转登录页。

服务端代码

const sessions = require('express-session')
// 模拟数据库中的用户名和密码
const username_DB = 'Nanyi'
const password_DB = '123'
// 存储产生的session
let session_DB

app.use(sessions({
  secret: '密钥',
  name: 'sessionName',
  cookie: { maxAge: 150000, expires: new Date('2024-1-10') },
  resave: false,
  saveUninitialized: false
}))

// 请求拦截,如果session里没有name字段,证明session失效,需要重新登录
app.use(function (req, res, next) {
  if (req.originalUrl === '/login' || req.session.name !== undefined) {
    next()
  } else {
    res.send({
      code: 10101,
      status: 'error',
      message: '登录状态已过期,请重新登录'
    })
    res.end()
  }
})

// 登录时,验证用户信息是否在数据库中,将session的信息保存到数据库,往session加入name字段
app.post('/login', function (req, res) {
  if (req.body.name === username_DB && req.body.password === password_DB) {
    session_DB = req.session
    session_DB.name = req.body.name
    res.send({
      code: 200,
      status: 'success',
      message: '登录成功'
    })
  } else {
    res.send({
      code: 400,
      status: 'error',
      message: '用户名或密码错误'
    })
  }
})

app.get('/safe', function (req, res) {
  res.send({
    code: 200,
    status: 'success',
    message: `欢迎登录${session_DB.name}`
  })
})

前端代码

在web应用挂载的时候,开启全局响应拦截,并将navigate函数作为参数传递过去,用于路由跳转。如果报错10101,则跳转登录页并删除登录标识。

export const interceptors = function (navigate) {
  axios.interceptors.response.use(function (res) {
    if (res.data.code === 10101) {
      localStorage.removeItem('islogin')
      navigate('/login')
    }
    return res
  })
}
// App
export default function App(){
  const navigate = useNavigate()
  useLayoutEffect(() => {
    interceptors(navigate)
  }, [])
  return (...)
}

登录页,如果有登录标识就跳转到内部主页;点击登录时,将登录标识存在loaclstorage,并跳转内部主页

export default function Login() {
  const navigate = useNavigate()
  
  useEffect(() => {
    let islogin = localStorage.getItem('islogin')
    islogin && navigate('/CSRF')
  }, [])
  
  const login = () => {
    axios.post('/login', { name: 'Nanyi', password: '123' }).then((res) => {
      alert(res.data?.message)
      localStorage.setItem('islogin', 'true')
      navigate('/CSRF')
    })
  }
  return (...)
}

Token实现自动登录

工作原理:用户登录,服务端返回token,前端将token存在localstorage。遇到//login这两个路由时,判断标识是否存在,存在就直接跳过登录页。如果token无效或者过期,服务端返回错误,状态码10101(用于标识是token错误),需要重新登录。前端做请求拦截,遇到10101报错,跳转登录页。

服务端代码

const jwt = require('jsonwebtoken')
let JWT_SECRET = '密钥'
let user_DB // 存token里面解析出来的用户信息

app.use(function (req, res, next) {
  const { authorization } = req.headers
  const token = authorization.replace('Bearer ', '')
  try {
    user_DB = jwt.verify(token, JWT_SECRET)
  } catch (error) {
    let message = ''
    switch (error.name) {
      case 'TokenExpiredError':
        message = 'token已过期'
        break;
      case 'JsonWebTokenError':
        message = '无效token'
        break;
    }
    res.send({
      code: 10101,
      status: 'error',
      message: message
    })
    res.end()
    return;
  }

  next()
})

app.post('/login', function (req, res) {
  let name = req.body.name
  res.send({
    code: 200,
    status: 'success',
    message: {
      token: jwt.sign({ name }, JWT_SECRET, { expiresIn: '1d' })
    }
  })
})

app.get('/safe', function (req, res) {
  res.send({
    code: 200,
    status: 'success',
    message: `欢迎登录${user_DB.name}`
  })
})

前端代码

在web应用挂载的时候,开启全局响应拦截,并将navigate函数作为参数传递过去,用于路由跳转。如果报错10101,则跳转登录页并删除token。

export const interceptors = function (navigate) {
  axios.interceptors.response.use(function (res) {
    if (res.data.code === 10101) {
      localStorage.removeItem('islogin')
      localStorage.removeItem('token')
      navigate('/login')
    }
    return res
  })
}
// 登录时,把token存在localstorage
  const login = () => {
    axios.post('/login', { name: 'Nanyi', password: '123' }).then((res) => {
      alert(res.data?.message)
      localStorage.setItem('islogin', 'true')
      localStorage.setItem('token', res.data?.token)
      navigate('/CSRF')
    })
  }

前端代码与session的实现基本一致,只需要在以上两处加上一句代码,在localstorage存删token即可。由于token是放在请求头传递,所以需要做一步请求拦截。

axios.interceptors.request.use(function (config) {
  let token = localStorage.getItem('token')
  if (token) {
    config.headers.Authorization = 'Bearer ' + token
  }
  return config;
});

总结

至此,我们实现了自动登录,以及用户身份过期跳回登录页。自动登录前端部分的实现大同小异,根据服务端身份验证的实现进行调整。