本文分别站在了客户端(reactjs)与服务端(nodejs)的角度,总结了整个用户校验过程各自的操作。
一 概念明晰
1.1 localStorage 和 Cookie
都是存储数据的方式
- localStorage:储存在客户端(浏览器)本地
- Cookie:存储在服务端,安全性更高。(是一个 HTTP 请求标头,由服务器通过
Set-Cookie
设置,存储到客户端的 HTTP cookie)
1.2 Token/JWT 和 SessionId
都是用户信息标识
- Token:一个通用术语,是代表用户身份的字符串。它通常由服务器在用户成功登录后生成,并在用户进行后续请求时发送给服务器以验证其身份。
- JWT(JSON Web Token):一种特殊的 Token。由三部分组成的字符串:Header(令牌类型和签名算法)、Payload(用户信息)、Signature组成
- SessionId:用来识别和追踪用户会话的一串唯一的字符
本文主要讲JWT
二 JWT的生成与使用
- 安装JWT库
npm i jsonwebtoken
- 登录时生成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
- 调用其他请求时验证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:"用户验证失败"}) } };
三 应用场景
- JWT & localStorage
- JWT & Cookie
3.1 存储在localStorage
-
服务端:将token返回给客户端
const login = async (req, res) => { // ...登录成功后 // ...生成完token const token = "your_token" // 将token返回给客户端 res.status(StatusCodes.OK).json({ msg: '登录成功', token, }); };
-
客户端:将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'; // 跳转到登录页 }, });
-
服务端:拿到客户端发过来的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
-
(可选)服务端:安装Cookie解析库
npm i cookie-parser
// app.js const cookieParser = require('cookie-parser'); app.use(cookieParser()); // 或加密 // app.use(cookieParser(process.env.COOKIE_SECRET, { signedCookies: true }));
-
服务端:将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: '登录成功' }); };
-
客户端:不需要存储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' } })
-
服务端:对于其他请求,拿到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);
-
服务端:对于退出登录,还需要清除Cookie的token
const logout = async (req, res) => { res.clearCookie('token') res.status(StatusCodes.OK).json({ msg:'成功退出' }) }