Nodejs 在实战中的校验用户信息(JWT、localStorage、Cookie)

本文分别站在了客户端(reactjs)与服务端(nodejs)的角度,总结了整个用户校验过程各自的操作。

一 概念明晰

都是存储数据的方式

  • localStorage:储存在客户端(浏览器)本地
  • Cookie:存储在服务端,安全性更高。(是一个 HTTP 请求标头,由服务器通过 Set-Cookie 设置,存储到客户端的 HTTP cookie

1.2 Token/JWT 和 SessionId

都是用户信息标识

  • Token:一个通用术语,是代表用户身份的字符串。它通常由服务器在用户成功登录后生成,并在用户进行后续请求时发送给服务器以验证其身份。
  • JWT(JSON Web Token):一种特殊的 Token。由三部分组成的字符串:Header(令牌类型和签名算法)、Payload(用户信息)、Signature组成
  • SessionId:用来识别和追踪用户会话的一串唯一的字符

本文主要讲JWT

二 JWT的生成与使用

https://jwt.io/

  1. 安装JWT库
    npm i jsonwebtoken
    
  2. 登录时生成JWT
    const jwt = require('jsonwebtoken');
    
    const login = async (req, res) => {
       // ...登录成功后
       
       const token = jwt.sign(
          { userId: <userId>, username: <username> }, // 填入想存储的用户信息
          process.env.JWT_SECRET,                     // 秘钥,可以为随机一个字符串
          {
             expiresIn: "7d",                         // 其他选项,如过期时间
          }
       );
       
       // ...
    };
    

    接着就是选择存储方式:1.将token返回到客户端让客户端存储在localStorage;2.将token存储在服务端Cookie

  3. 调用其他请求时验证Token
    // 验证的中间件
    
    const authToken = async (req, res, next) => {
      
      // ... 根据存储方式拿到token
      const token = "your_token"
      
       try {
          const decoded = jwt.verify(token, process.env.JWT_SECRET); // 传入token和秘钥
          // 拿到解出来的 { userId, username }
          // ... 进一步从数据库中判断这个用户信息是否存在
          // 将信息挂载req.user中供后续接口使用
          req.user = { userId, username, ... };
          next();
       } catch (error) {
           res.status(401).json({msg:"用户验证失败"})
       }
    };
    

三 应用场景

  1. JWT & localStorage
  2. JWT & Cookie

3.1 存储在localStorage

  1. 服务端:将token返回给客户端

    const login = async (req, res) => {
       // ...登录成功后
       // ...生成完token
       const token = "your_token"
       
       // 将token返回给客户端
       res.status(StatusCodes.OK).json({
           msg: '登录成功',
           token,
        });
    };
    
  2. 客户端:将token存储到localStorage,并在后续请求中将token发送给服务端

    为了方便管理,这里简单封装了下aixos:

    import toast from 'react-hot-toast';
    
    // 创建axios实例,把本地的token放在header中:
    const axiosInstance = axios.create({
         baseURL: '/api/v1',
         timeout: 3000,
         headers: { 'Authorization': 'Bearer ' + localStorage.getItem('token') }, // 每个请求都自动携带token
     });
     
     // 是否显示成功的提示或者失败的提示
     const defaultConfig = {
         showError: true,
         showSuccess: false
     }
     const request = (url: string, config= {}) => {
         const _config = {
             ...defaultConfig,
             ...config
         }
         const { data, params } = _config
         const method = _config.method || 'get'
    
         return axiosInstance.request({
             url,
             method,
             data: data || {},
             params: params || {},
         }).then((res) => {
             const data = res.data;
             _config.success && _config.success(data);
             if (_config.showSuccess) toast.success(data.msg || '请求成功');
             return data as TResData<T>
         }).catch((err) => {
             if (err.response.status >= 500) {
                 toast.error('服务器发生错误,请稍后再试')
             }
             // 如果用户校验失败,重新返回登录页
             if (err.response.status === 401) {
                 toast.error('用户凭证出现问题,请重新登录')
                 location.href = '/login'  
             }
             // 其他错误
             let data = err.response.data
             _config.error && _config.error(data)
             if (_config.showError) toast.error(data.msg || '未知错误')
             return data
         })
     }
    

    现在基于这个封装好的request,写一下示例:

    (1) 登录时存储token

    request('/login', {
          method: 'POST',
          data: {
             username,
             password,
          },
          showSuccess: true,
          success: (data) => {
             localStorage.setItem('token', data.token); // 登录成功后将token存储
             location.href = '/home'; // 跳转到主页 
          },
       });
    

    (2)其他请求:自动在Header上携带token

    request('/stats');
    

    (3)退出登录:清除localstorage的token

    request('/logout', {
      success: (data) => {
         localStorage.removeItem('token'); // 清除tokn
         location.href = '/login'; // 跳转到登录页
      },
    });
    
  3. 服务端:拿到客户端发过来的token进行验证

    // 用户验证中间件
    const authToken = async (req, res, next) => {
        // 获取token
       const authHeader = req.headers.authorization;
    
       if (!authHeader || !authHeader.startsWith('Bearer ')) {
          res.status(401).json({msg:"No token provided"})
       }
    
       const token = authHeader.split(' ')[1];
       
       // 验证token
       try {
          const decoded = jwt.verify(token, process.env.JWT_SECRET);
          // 以mongose为例
          const user = await User.findById(decoded.userId).select('-password');
          req.user = { userId: user._id, username: user.username, email: user.email };
          next();
       } catch (error) {
           res.status(401).json({msg:"用户验证失败"})
       }
    };
    

    在其他请求中加上中间件:

    app.use('/api/v1/jobs', authToken, jobsRoute);
    

3.2 存储在Cookie

  1. (可选)服务端:安装Cookie解析库

    npm i cookie-parser
    
    // app.js
    
    const cookieParser = require('cookie-parser');
    app.use(cookieParser());
    // 或加密
    // app.use(cookieParser(process.env.COOKIE_SECRET, { signedCookies: true }));
    
  2. 服务端:将token存储在Cookie中

    const login = async (req, res) => {
       // ...登录成功后
       // ...生成完token
       const token = "your_token"
       
       // 安装cookie-parser后可以这样写
       const oneDay = 1000 * 60 * 60 * 24;
       res.cookie('token', token, {
          httpOnly: true,
          expires: new Date(Date.now() + oneDay),
          secure: process.env.NODE_ENV === 'production',
          signed: true,
       });
       
       // 它实际上进行操作是:
       /**
          let cookieString = `token=${token}; Expires=${oneDay}; HttpOnly`;
          if (process.env.NODE_ENV === 'production') {
             cookieString += '; Secure';
          }
          res.setHeader('Set-Cookie', cookieString);
       */
       
         res.status(StatusCodes.OK).json({
           msg: '登录成功'
        });
    };
    

  3. 客户端:不需要存储token,也不需要在请求头携带token了,只需要根据服务端返回的status code来判断是否跳转回登录页

    // 依旧是使用上面封装好的request
    
    const axiosInstance = axios.create({
        baseURL: '/api/v1',
        timeout: 1000,
        // headers: { 'Authorization': 'Bearer ' + localStorage.getItem('token') },
    });
    
    
    // ...
    .catch(()=>{
         if (err.response.status === 401) {
            toast.error('用户凭证出现问题,请重新登录')
            location.href = '/login'
        }
    })
    
  4. 服务端:对于其他请求,拿到Cookie的token进行验证

    其他请求的请求头部的Cookie将会多一个token信息:

    // 用户验证中间件
    const authToken = async (req, res, next) => {
       const token = req.cookie.token;  
       // 等同于:req.headers.cookie.split('=')[1]
       // 如果上面的signed为true, 则  const token = req.signedCookies.token;
    
       if (!token) {
          res.status(401).json({msg:"No token provided"})
       }
    
       try {
          const decoded = jwt.verify(token, process.env.JWT_SECRET); // 传入token和秘钥
          // 拿到解出来的 { userId, username }
          // 将信息挂载req.user中供后续接口使用
          req.user = { userId, username, ... };
          next();
       } catch (error) {
           res.status(401).json({msg:"用户验证失败"})
       }
    };
    

    使用中间件

    app.use('/api/v1/groups', authToken, groupsRoute);
    
  5. 服务端:对于退出登录,还需要清除Cookie的token

    const logout = async (req, res) => {
       res.clearCookie('token')
       res.status(StatusCodes.OK).json({
          msg:'成功退出'
       })
    }
    
posted @ 2024-05-27 11:28  sanhuamao  阅读(216)  评论(0编辑  收藏  举报