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
localStorageorsessionStorage—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 Unauthorizedresponse to force the user to re-authenticate.
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
Secureto 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.
- On the frontend, store the access token in memory (e.g., React state) instead of
localStorageto avoid XSS risks. When the access token expires, call the/refreshendpoint automatically to get a new one. - In production, always use HTTPS to prevent token interception.
内容的提问来源于stack exchange,提问作者Dax

