Double Token Auth with Express & Vue: Complete Implementation Guide
This article explains the double token (Access and Refresh) authentication mechanism, detailing its core principles, security benefits, and step‑by‑step implementation with Express on the backend and Vue on the frontend, including token issuance, verification, rotation, middleware, and client‑side handling.
Core Principles of the Double Token Mechanism
The double token approach uses two tokens with different characteristics to balance security and user experience. An Access Token is short‑lived (e.g., 12 seconds in the demo) and is used to access protected resources. A Refresh Token is long‑lived (e.g., 7 days) and is only used to obtain new Access Tokens.
This design limits the damage of a stolen Access Token while keeping the Refresh Token in a more secure environment, allowing immediate revocation if needed.
Backend Implementation: Express Double Token System
The backend issues and validates tokens, handling configuration, utility functions, middleware, and API endpoints.
Basic Configuration and Dependencies
const express = require('express');</code><code>const cors = require('require('cors');</code><code>const cookieParser = require('cookie-parser');</code><code>const app = express();</code><code>app.use(cors({ origin: ['http://localhost:5173','http://localhost:5174','http://localhost:5175','http://localhost:5176'], credentials: true }));</code><code>app.use(express.json());</code><code>app.use(cookieParser());This code imports Express and necessary middleware, configures CORS to allow specific front‑end origins with credentials, and enables JSON and cookie parsing.
Token Storage and Core Utility Functions
// In‑memory storage (replace with Redis or a database in production)</code><code>const accessTokens = new Map();</code><code>const refreshTokens = new Map();</code><code>// Get current timestamp (seconds)</code><code>function now() { return Math.floor(Date.now() / 1000); }</code><code>// Generate a random token with a prefix</code><code>function getRandom(prefix) { return `${prefix}-${Math.random().toString(26).slice(2)}${Date.now()}`; }Tokens are stored in memory for demonstration; production should use a distributed store such as Redis.
Token Issuance Functions
function getAccessToken(userId, ttlSec = 12) { const at = getRandom('AccessToken'); accessTokens.set(at, { userId, expiresIn: now() + ttlSec }); return at; }</code><code>function getRefreshToken(userId, ttlSec = 3600 * 24 * 7) { const rt = getRandom('RefreshToken'); refreshTokens.set(rt, { userId, expiresIn: now() + ttlSec, revoked: false }); return rt; }These functions create tokens, record the associated user ID, expiration time, and a revocation flag for Refresh Tokens.
Token Verification and Revocation Functions
function verifyAccessToken(at) { const result = accessTokens.get(at); if (!result || result.expiresIn <= now()) return null; return result.userId; }</code><code>function verifyRefreshToken(rt) { const result = refreshTokens.get(rt); if (!result || result.revoked || result.expiresIn <= now()) return null; return result.userId; }</code><code>function revokeRefreshToken(rt) { const result = refreshTokens.get(rt); if (result) result.revoked = true; }Verification checks existence, expiration, and revocation status, returning the user ID when valid.
Authentication Middleware: First Line of Defense
app.use((req, res, next) => { if (['/auth/login','/auth/refresh','/auth/logout','/login'].includes(req.path)) { return next(); } const token = req.headers.token || ''; const userId = verifyAccessToken(token); if (userId) { req.userId = userId; return next(); } res.send({ status: 401, msg: '未登录或令牌过期' }); });The middleware bypasses authentication for login‑related routes, validates the Access Token for other requests, attaches the user ID to the request object, and returns a 401 error on failure.
Core API Endpoints
Login Endpoint: Initial Token Issuance
app.post('/auth/login', (req, res) => { const { username } = req.body || {}; const userId = username || 'demoUser'; const at = getAccessToken(userId, 12); const rt = getRefreshToken(userId); res.cookie('rt', rt, { httpOnly: true, sameSite: 'lax', secure: false, path: '/', maxAge: 7 * 24 * 3600 * 1000 }); res.send({ status: 200, data: at }); });The login route generates both tokens, stores the Refresh Token in an httpOnly cookie, and returns the Access Token to the front‑end.
Refresh Endpoint: Seamless Token Renewal
app.post('/auth/refresh', (req, res) => { const rt = req.cookies.rt; if (!rt) return res.send({ status: 401, msg: '无刷新令牌' }); const userId = verifyRefreshToken(rt); if (!userId) return res.send({ status: 401, msg: '刷新令牌失效' }); revokeRefreshToken(rt); const newRt = getRefreshToken(userId); const newAt = getAccessToken(userId); res.cookie('rt', newRt, { /* same options as login */ }); res.send({ status: 200, data: newAt }); });This endpoint validates the Refresh Token, revokes the old one, issues new tokens, and updates the cookie, enabling invisible token rotation.
Logout Endpoint: Secure Session Termination
app.post('/auth/logout', (req, res) => { const rt = req.cookies.rt; if (rt) revokeRefreshToken(rt); res.clearCookie('rt', { path: '/' }); res.send({ status: 200, msg: '已登出' }); });Logout revokes the Refresh Token and clears the cookie, ensuring the session cannot be reused.
Frontend Implementation: Token Management in Vue
The front‑end stores the Access Token, sends it with requests, and automatically refreshes it when expired.
Router Guard: Controlling Page Access
router.beforeEach((to, from, next) => { const token = to.query.token; if (token) { localStorage.setItem('token', token); next({ path: to.path, query: {} }); return; } if (to.meta.requiresAuth) { const currentToken = localStorage.getItem('token'); if (!isValidToken(currentToken)) { window.open(`http://localhost:5174/login?resource=${window.location.origin}${to.path}`); return; } } next(); });The guard extracts a token from the URL, stores it, and redirects unauthenticated users to the login page.
Axios Interceptors: Automatic Token Handling
// Request interceptor: add token to headers</code><code>request.interceptors.request.use(config => { const token = localStorage.getItem('token'); if (isValidToken(token)) { config.headers = config.headers || {}; config.headers.token = token; } else { localStorage.removeItem('token'); } return config; });</code><code>// Response interceptor: handle 401 and refresh token</code><code>request.interceptors.response.use(async res => { if (res.data && res.data.status === 401) { const original = res.config || {}; if (original._retried) { window.open(`http://localhost:5174/login?resource=${window.location.origin}`); return res; } original._retried = true; if (!isRefreshing) { isRefreshing = true; refreshPromise = request.post('/auth/refresh', {}); } try { const newToken = await refreshPromise; localStorage.setItem('token', newToken); original.headers.token = newToken; return request(original); } catch (e) { window.open(`http://localhost:5174/login?resource=${window.location.origin}`); return res; } finally { isRefreshing = false; } } return res; });The request interceptor attaches the Access Token, while the response interceptor attempts a token refresh on 401 errors, retrying the original request with the new token.
Login Page Component: Handling Authentication
<script setup> import request from '../server/request'; import { useRoute } from 'vue-router'; import { watch, ref } from 'vue'; const route = useRoute(); const resource = ref(''); const token = localStorage.getItem('token'); function windowPostMessage(token, resource) { if (window.opener) { window.opener.postMessage({ token }, resource); } } watch(() => route.query.resource, (val) => { resource.value = val ? decodeURIComponent(val) : ''; if (token) { windowPostMessage(token, resource.value); } }, { immediate: true }); function login() { request.get('/auth/login').then(res => { const apitoken = res.data.data; localStorage.setItem('token', apitoken); windowPostMessage(apitoken, resource.value); window.location.href = `${resource.value}?token=${apitoken}`; window.close(); }); } </script>The component obtains a redirect resource, initiates login, stores the Access Token, notifies the opener window, and redirects back with the token.
Double Token Mechanism Demonstration
The following GIF illustrates the complete flow from login, token usage, automatic refresh, to logout.
Security Considerations of the Double Token Mechanism
Access Token stored in localStorage (XSS risk); Refresh Token stored in httpOnly cookie (protected from XSS).
Use HTTPS in production; set secure flag on cookies.
Short‑lived Access Tokens reduce the window for abuse; Refresh Tokens can be revoked.
Token rotation ensures a stolen Refresh Token can be used only once.
SameSite cookie attribute mitigates CSRF attacks.
Strict verification logic prevents invalid tokens from being accepted.
Conclusion and Extensions
The double token strategy balances security and user experience by combining short‑lived Access Tokens with long‑lived Refresh Tokens. The provided code demonstrates full token lifecycle handling on both server and client sides.
Potential extensions include:
Replacing in‑memory storage with Redis or another distributed store for scalability.
Implementing a token blacklist to handle revoked tokens before expiration.
Adding token revocation notifications for events like password changes.
Using JWT for stateless verification to reduce server load.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Rare Earth Juejin Tech Community
Juejin, a tech community that helps developers grow.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
