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

React&Nodejs中JWT认证的Refresh Token安全实现问询

Great question! Implementing refresh tokens properly is crucial for balancing security and a smooth user experience in JWT authentication. Let’s walk through the best practices, address your security concerns, and adjust your existing code to integrate refresh tokens securely.

最佳实现方式

Here’s a step-by-step breakdown of how to fit refresh tokens into your workflow:

1. Store Refresh Tokens Securely

  • Frontend: Never store refresh tokens in localStorage or sessionStorage—these are vulnerable to XSS attacks. Instead, send the refresh token to the frontend via an HttpOnly, Secure cookie. This prevents client-side JavaScript from accessing the token, drastically reducing theft risk.
  • Backend: Persist the refresh token (or a hashed version) in your database, linked to the user account. This lets you invalidate tokens later if needed (e.g., user logs out, changes password).

2. Create a Dedicated Refresh Token Route

Add a separate endpoint that lets clients exchange a valid refresh token for a new access token. This way, users don’t have to re-login every time their short-lived access token expires.

3. Handle Token Expiry & Invalidation

  • When an access token expires, the frontend should automatically call the refresh endpoint to get a new one.
  • If a refresh token is expired, invalid, or not found in the database, return a 401 Unauthorized response to force the user to re-authenticate.
防止攻击者滥用Refresh Token的安全措施

To mitigate risks from stolen refresh tokens, implement these safeguards:

  • Use short-lived Access Tokens: Keep your access token expiry (like your current 1 hour) short—this limits the window of opportunity if an attacker steals it.
  • HttpOnly + Secure Cookies: As mentioned earlier, this prevents XSS from stealing the refresh token. In production, enable Secure to ensure the cookie is only sent over HTTPS.
  • Refresh Token Invalidation: Maintain a record of valid refresh tokens in your database. When a user logs out, changes their password, or logs in from a new device, invalidate the old refresh token by removing it from the database.
  • Limit Refresh Token Scope: Ensure refresh tokens can only be used to get new access tokens—never to access protected resources directly.
  • Optional: Hash Refresh Tokens in the Database: Just like passwords, you can hash refresh tokens before storing them (use a fast hashing algorithm like SHA-256 since refresh tokens are random and don’t need slow bcrypt). This adds an extra layer if your database is compromised.
修改后的完整代码示例

Let’s update your code to implement these practices:

1. Update User Model (Add Refresh Token Field)

First, modify your User schema to store the refresh token:

// Example Mongoose schema
const userSchema = new mongoose.Schema({
  username: String,
  password: String,
  email: String,
  refreshToken: String // Add this field
});

2. Updated Login Route

router.post("/login", async (req, res) => {
  const { username, password, remember } = req.body;
  if (!username || !password) {
    return res.status(400).json({ message: "Please fill in all fields." });
  }
  try {
    const user = await User.findOne({ username });
    if (!user) return res.status(400).json({ message: "User does not exist." });
    
    const isValid = await bcrypt.compare(password, user.password);
    if (!isValid) return res.status(400).json({ message: "Invalid credentials" });

    // Generate short-lived access token
    const accessToken = jwt.sign(
      { id: user.id }, 
      config.get("jwtSecret"), 
      { expiresIn: "3600s" } // 1 hour
    );

    let refreshToken = null;
    if (remember) {
      // Generate long-lived refresh token
      refreshToken = jwt.sign(
        { id: user.id }, 
        config.get("jwtRefreshSecret"), // Use a SEPARATE secret for refresh tokens!
        { expiresIn: "30d" }
      );

      // Store refresh token in database (you can hash it here if desired)
      user.refreshToken = refreshToken;
      await user.save();

      // Set HttpOnly cookie for refresh token
      res.cookie("refreshToken", refreshToken, {
        httpOnly: true,
        secure: process.env.NODE_ENV === "production", // Only send over HTTPS in prod
        maxAge: 30 * 24 * 60 * 60 * 1000, // 30 days
        sameSite: "strict" // Prevent CSRF
      });
    }

    // Send access token to frontend (can be in response body or another cookie)
    return res.status(200).json({ 
      accessToken,
      user: {
        id: user.id,
        username: user.username,
        email: user.email
      }
    });
  } catch (error) {
    console.error(error);
    return res.status(500).json({ message: "Server error" });
  }
});

Important: Use a separate secret key for refresh tokens! This limits the impact if one secret is compromised.

3. Add Refresh Token Route

router.post("/refresh", async (req, res) => {
  // Get refresh token from HttpOnly cookie
  const refreshToken = req.cookies.refreshToken;
  if (!refreshToken) {
    return res.status(401).json({ message: "Refresh token missing, authorization denied" });
  }

  try {
    // Verify refresh token
    const decoded = jwt.verify(refreshToken, config.get("jwtRefreshSecret"));
    
    // Find user and check if refresh token matches the one in database
    const user = await User.findById(decoded.id);
    if (!user || user.refreshToken !== refreshToken) {
      return res.status(401).json({ message: "Invalid refresh token" });
    }

    // Generate new access token
    const newAccessToken = jwt.sign(
      { id: user.id },
      config.get("jwtSecret"),
      { expiresIn: "3600s" }
    );

    return res.status(200).json({ accessToken: newAccessToken });
  } catch (error) {
    console.error(error);
    return res.status(401).json({ message: "Refresh token expired or invalid" });
  }
});

4. Add Logout Route (To Invalidate Refresh Token)

router.post("/logout", async (req, res) => {
  const refreshToken = req.cookies.refreshToken;
  if (!refreshToken) {
    return res.status(200).json({ message: "Logged out successfully" });
  }

  try {
    const decoded = jwt.verify(refreshToken, config.get("jwtRefreshSecret"));
    const user = await User.findById(decoded.id);
    
    // Remove refresh token from database
    if (user) {
      user.refreshToken = null;
      await user.save();
    }

    // Clear the refresh token cookie
    res.clearCookie("refreshToken", {
      httpOnly: true,
      secure: process.env.NODE_ENV === "production",
      sameSite: "strict"
    });

    return res.status(200).json({ message: "Logged out successfully" });
  } catch (error) {
    console.error(error);
    // Even if token is invalid, clear the cookie
    res.clearCookie("refreshToken", {
      httpOnly: true,
      secure: process.env.NODE_ENV === "production",
      sameSite: "strict"
    });
    return res.status(200).json({ message: "Logged out successfully" });
  }
});

5. Your Existing Auth Middleware (Unchanged, but for Access Tokens)

Your current auth middleware is still valid for protecting routes that require access tokens—just make sure the frontend sends the access token in the x-auth-token header.

Final Notes
  • On the frontend, store the access token in memory (e.g., React state) instead of localStorage to avoid XSS risks. When the access token expires, call the /refresh endpoint automatically to get a new one.
  • In production, always use HTTPS to prevent token interception.

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.08 16:42:36