关于前端长短Token实现【让你的应用安全得像金库,丝滑得像德芙】

205 阅读2分钟

一、前言:为什么你的应用需要长短Token?

想象一下,你正在游乐园玩耍。工作人员给你两种门票:

  1. 当日票(Access Token):有效期2小时,丢了可以补办
  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系统就像给应用穿上防弹衣的同时穿上跑鞋

  1. Access Token是冲锋陷阵的战士——轻装上阵但定期轮换
  2. Refresh Token是后方指挥中心——重兵把守且深居简出
  3. 设备指纹绑定是身份识别系统——防止敌人冒名顶替

记住:没有绝对安全的系统,但我们可以让攻击者的成本高到怀疑人生。现在就去给你的应用穿上这套黄金圣衣吧!


最后的友情提示

  • 生产环境一定要上HTTPS!
  • 密钥管理要用专业的Vault工具!
  • 定期演练Token撤销流程!

祝各位代码永无Bug,Token永不泄露!

PS:千万记住一条,Refresh Token是需要增加一键拉黑机制的,防止真·黑客哪天破解了你的Refresh Token还能一键拉黑自救小命