You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何处理活跃JWT有效期内用户权限变更的问题?

解决方案:JWT权限同步问题(React + Express)

针对你遇到的管理员修改用户权限后,前端旧JWT仍保留旧权限的问题,除了刷新时查数据库,这里提供几个更优的方案,结合React和Express栈实现:

方案1:短生命周期JWT + 刷新令牌(Refresh Token)

核心思路是把JWT的有效期设得很短(比如15分钟),同时用一个长期有效的刷新令牌存在Redis中。当JWT快过期时,前端自动用刷新令牌换取带最新权限的新JWT;管理员修改权限时,直接失效用户的刷新令牌,强制用户重新获取新令牌或登录。

Express端实现

const jwt = require('jsonwebtoken');
const redis = require('redis');
const client = redis.createClient();
const User = require('./models/User');

// 刷新令牌接口
app.post('/api/refresh-token', async (req, res) => {
  const refreshToken = req.cookies.refreshToken;
  if (!refreshToken) return res.sendStatus(401);

  // 校验刷新令牌有效性
  const storedToken = await client.get(`refresh:${req.cookies.username}`);
  if (storedToken !== refreshToken) return res.sendStatus(403);

  // 从数据库拉取最新权限
  const user = await User.findOne({ username: req.cookies.username });
  if (!user) return res.sendStatus(404);

  // 生成新的短周期JWT
  const newAccessToken = jwt.sign(
    { username: user.username, roles: user.roles },
    process.env.JWT_SECRET,
    { expiresIn: '15m' }
  );

  // 更新Cookie中的AccessToken(httpOnly防止XSS)
  res.cookie('accessToken', newAccessToken, { 
    httpOnly: true, 
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'strict'
  });
  res.json({ roles: user.roles });
});

// 管理员修改权限时,失效刷新令牌
app.post('/api/users/:id/update-role', async (req, res) => {
  const user = await User.findByIdAndUpdate(req.params.id, { roles: req.body.roles }, { new: true });
  await client.del(`refresh:${user.username}`);
  res.json(user);
});

React端实现

用Axios拦截器自动处理JWT刷新:

import axios from 'axios';
import jwtDecode from 'jwt-decode';
import { useState, useEffect } from 'react';

export const useAuth = () => {
  const [roles, setRoles] = useState([]);

  useEffect(() => {
    // 初始化时获取当前权限
    const initRoles = async () => {
      const token = document.cookie.split('; ').find(row => row.startsWith('accessToken='))?.split('=')[1];
      if (token) {
        const decoded = jwtDecode(token);
        setRoles(decoded.roles);
      }
    };
    initRoles();
  }, []);

  // 请求拦截器:检查JWT是否即将过期
  axios.interceptors.request.use(async (config) => {
    const token = document.cookie.split('; ').find(row => row.startsWith('accessToken='))?.split('=')[1];
    if (!token) return config;

    const decoded = jwtDecode(token);
    // 剩余时间小于1分钟时刷新
    if (decoded.exp * 1000 - Date.now() < 60000) {
      try {
        const res = await axios.post('/api/refresh-token');
        setRoles(res.data.roles);
      } catch (err) {
        window.location.href = '/login';
      }
    }
    return config;
  });

  return { roles };
};

// 在组件中使用
const Dashboard = () => {
  const { roles } = useAuth();
  return (
    <div>
      {roles.includes('admin') && <button onClick={handleManageUsers}>管理用户</button>}
    </div>
  );
};

方案2:后端接口强制校验权限,前端做状态同步

前端的权限展示只是优化用户体验,所有敏感操作的权限判断最终由后端负责。即使前端显示了越权按钮,后端也会拒绝请求。同时通过WebSocket同步权限状态。

Express端实现

const User = require('./models/User');

// 权限校验中间件
const requireRole = (requiredRole) => async (req, res, next) => {
  const username = req.user.username; // 从JWT解析出的用户名
  const user = await User.findOne({ username });
  
  if (!user || !user.roles.includes(requiredRole)) {
    return res.sendStatus(403);
  }
  next();
};

// 敏感接口使用中间件
app.delete('/api/users/:id', requireRole('admin'), async (req, res) => {
  await User.findByIdAndDelete(req.params.id);
  res.sendStatus(200);
});

// 提供获取当前权限的接口
app.get('/api/user/roles', async (req, res) => {
  const user = await User.findOne({ username: req.user.username });
  res.json({ roles: user.roles });
});

React端实现

用自定义hook获取并同步权限:

import { useState, useEffect } from 'react';
import axios from 'axios';
import io from 'socket.io-client';

const socket = io();

export const useUserRoles = () => {
  const [roles, setRoles] = useState([]);

  useEffect(() => {
    // 初始化拉取权限
    const fetchRoles = async () => {
      try {
        const res = await axios.get('/api/user/roles');
        setRoles(res.data.roles);
      } catch (err) {
        setRoles([]);
      }
    };
    fetchRoles();

    // 监听WebSocket推送的权限更新
    const username = document.cookie.split('; ').find(row => row.startsWith('username='))?.split('=')[1];
    socket.emit('auth', username);
    socket.on('role-updated', (newRoles) => {
      setRoles(newRoles);
    });

    return () => socket.disconnect();
  }, []);

  return roles;
};

方案3:WebSocket实时推送权限更新

管理员修改权限后,后端主动通过WebSocket向目标用户推送权限更新事件,前端立即更新状态,无需等待刷新或JWT过期。

Express端(结合Socket.io)

const { Server } = require('socket.io');
const http = require('http');
const server = http.createServer(app);
const io = new Server(server);
const User = require('./models/User');

// 存储用户与socket的映射
const userSocketMap = new Map();

io.on('connection', (socket) => {
  // 用户登录后绑定socket
  socket.on('auth', (username) => {
    userSocketMap.set(username, socket.id);
  });

  socket.on('disconnect', () => {
    // 移除映射
    for (const [user, id] of userSocketMap.entries()) {
      if (id === socket.id) {
        userSocketMap.delete(user);
        break;
      }
    }
  });
});

// 管理员修改权限时推送更新
app.post('/api/users/:id/update-role', async (req, res) => {
  const user = await User.findByIdAndUpdate(req.params.id, { roles: req.body.roles }, { new: true });
  const socketId = userSocketMap.get(user.username);
  
  if (socketId) {
    io.to(socketId).emit('role-updated', user.roles);
  }

  res.json(user);
});

方案对比

  • 方案1:平衡安全性和性能,适合大多数场景,无需实时同步时优先选择。
  • 方案2:最安全,权限判断完全由后端掌控,前端仅做展示优化。
  • 方案3:实时性最好,适合对权限同步速度要求高的系统。

内容的提问来源于stack exchange,提问作者lielb

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.26 21:27:07