一、前言:为什么你的应用需要长短Token?
想象一下,你正在游乐园玩耍。工作人员给你两种门票:
- 当日票(Access Token):有效期2小时,丢了可以补办
- 季卡(Refresh Token):有效期3个月,丢了就得重新买票
这就是长短Token机制的精髓——用短效Token保证安全,用长效Token维持体验。今天,我们就来手把手实现一套比游乐园门票更智能的认证系统!
二、技术选型:我们的武器库
1. 服务端
- Node.js:世界上最好的语言???(PHP:以前老子顶的这个名头)
- Express:让API开发比喝奶茶还简单
- jsonwebtoken:JWT生成与解析神器
- bcrypt:把密码和Token变成外星文
2. 前端
- axios:网络请求界的瑞士军刀
- localStorage:临时存点小秘密(但别放贵重物品)
三、核心原理:长短Token如何起舞?
1. 登录流程
用户:我要登录!
服务端:验明正身 → 发Access Token(15分钟) + 藏Refresh Token到HTTPOnly Cookie
客户端:揣好Access Token,Cookie自动保管Refresh Token
2. Token刷新流程
客户端:Access Token过期了!
服务端:检查Cookie里的Refresh Token → 有效?发新Access Token : 滚去登录
四、服务端实现:打造Token印钞机
1. 安装依赖
npm install express jsonwebtoken bcrypt cookie-parser
2. 初始化Express服务
const express = require('express');
const cookieParser = require('cookie-parser');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
const app = express();
// 中间件配置
app.use(express.json());
app.use(cookieParser());
app.use((req, res, next) => {
console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
next();
});
const ACCESS_TOKEN_SECRET = '你猜我是不是秘钥'; // 生产环境请用环境变量
const REFRESH_TOKEN_SECRET = '打死也不告诉你';
const PORT = 3000;
app.listen(PORT, () => {
console.log(`认证服务已在端口 ${PORT} 就绪`);
});
3. 用户登录接口
// 模拟数据库
const users = [
{
id: 1,
username: '王大锤',
password: '$2b$10$W7u9yvJZ5hLzH6Z8XzQqE.9M8bTkYdN9oLJfQgKjxD7yvJ5Q5Q5W' // bcrypt哈希后的"123456"
}
];
app.post('/api/login', async (req, res) => {
const { username, password } = req.body;
// 1. 找用户
const user = users.find(u => u.username === username);
if (!user) return res.status(401).json({ message: '用户不存在' });
// 2. 验密码
const valid = await bcrypt.compare(password, user.password);
if (!valid) return res.status(401).json({ message: '密码错误' });
// 3. 造Access Token
const accessToken = jwt.sign(
{ userId: user.id },
ACCESS_TOKEN_SECRET,
{ expiresIn: '15m' }
);
// 4. 造Refresh Token(存数据库)
const refreshToken = jwt.sign(
{ userId: user.id },
REFRESH_TOKEN_SECRET,
{ expiresIn: '7d' }
);
// 5. 把Refresh Token藏到Cookie
res.cookie('refreshToken', refreshToken, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'Strict',
maxAge: 7 * 24 * 60 * 60 * 1000 // 7天
});
res.json({ accessToken });
});
五、前端实现:做个聪明的搬运工
1. 登录函数
async function login(username, password) {
try {
const response = await axios.post('/api/login', {
username,
password
});
// 存Access Token到内存(别放localStorage!)
let accessToken = response.data.accessToken;
// 启动Token刷新监听
startTokenRefreshTimer(accessToken);
return true;
} catch (error) {
console.error('登录失败:', error.response?.data);
return false;
}
}
2. 自动刷新Token
let refreshTimeout = null;
function startTokenRefreshTimer(accessToken) {
// 解析Token过期时间
const { exp } = JSON.parse(atob(accessToken.split('.')[1]));
const expiresIn = exp * 1000 - Date.now();
// 提前30秒刷新
const refreshTime = expiresIn - 30000;
refreshTimeout = setTimeout(() => {
refreshAccessToken();
}, refreshTime);
}
async function refreshAccessToken() {
try {
const response = await axios.post('/api/refresh', {}, {
withCredentials: true // 携带Cookie
});
const newAccessToken = response.data.accessToken;
startTokenRefreshTimer(newAccessToken);
} catch (error) {
console.error('刷新Token失败:', error);
// 跳转登录页
window.location.href = '/login';
}
}
六、高级技巧:给你的Token上把锁
1. 设备指纹绑定
// 服务端生成设备指纹
function generateDeviceFingerprint(req) {
const ip = req.ip;
const userAgent = req.get('User-Agent');
return crypto.createHash('sha256')
.update(ip + userAgent)
.digest('hex');
}
// 存储Refresh Token时关联指纹
async function storeRefreshToken(userId, token, fingerprint) {
const hashedToken = await bcrypt.hash(token, 10);
// 存数据库...
}
2. Token黑名单
// 退出登录时拉黑Token
app.post('/api/logout', (req, res) => {
const refreshToken = req.cookies.refreshToken;
addToBlacklist(refreshToken); // 存Redis或数据库
res.clearCookie('refreshToken');
res.sendStatus(204);
});
七、常见问题:你可能会遇到的坑
1. 跨域Cookie携带问题
症状:前端明明设置了withCredentials,Cookie就是不跟着飞
药方:
// 服务端CORS配置
app.use(cors({
origin: 'https://your-frontend.com',
credentials: true
}));
2. 安卓微信内置浏览器Cookie异常
症状:Refresh Token经常离家出走
药方:
// 在URL参数中追加fallback token
if (isWechatBrowser()) {
const fallbackToken = getFallbackToken();
axios.get(`/api/data?fallbackToken=${fallbackToken}`);
}
八、总结:让安全与体验齐飞
实现一个健壮的长短Token系统就像给应用穿上防弹衣的同时穿上跑鞋:
- Access Token是冲锋陷阵的战士——轻装上阵但定期轮换
- Refresh Token是后方指挥中心——重兵把守且深居简出
- 设备指纹绑定是身份识别系统——防止敌人冒名顶替
记住:没有绝对安全的系统,但我们可以让攻击者的成本高到怀疑人生。现在就去给你的应用穿上这套黄金圣衣吧!
最后的友情提示:
- 生产环境一定要上HTTPS!
- 密钥管理要用专业的Vault工具!
- 定期演练Token撤销流程!
祝各位代码永无Bug,Token永不泄露!
PS:千万记住一条,Refresh Token是需要增加一键拉黑机制的,防止真·黑客哪天破解了你的Refresh Token还能一键拉黑自救小命