面试官:讲讲JWT登录鉴权

174 阅读4分钟

JWT登录鉴权与无感刷新:构建现代Web应用的安全基石

信息传递的"身份证"

在Web开发中,身份认证就像发放"身份证"——用户登录后,服务器需要通过某种方式记住用户的身份。传统的Cookie+Session方案虽然可靠,但在前后端分离架构中显得笨重。而 JWT(JSON Web Token) 就像一张轻便的电子身份证,它通过加密签名的方式,让客户端和服务器之间能够安全地传递身份信息。

Token通常被理解为令牌,也可以说通关文牒


一、JWT的三重奏:Header.Payload.Signature

JWT由三部分组成,用点号分隔:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.
eyJzdWIiOiIxMjM0NTY7
.
HmacSHA256加密后的签名
  • Header:声明算法(如HS256)和令牌类型
  • Payload:核心数据(用户ID、角色、过期时间等)
  • Signature:使用密钥对Header和Payload的签名

想象这是一张盖了钢印的身份证:Header是纸张材质说明,Payload是个人信息,Signature是防伪钢印。任何修改都会让钢印失效!


二、登录鉴权的完整流程

1. 用户登录:获取双Token-- access tokenrefresh token

// 后端(Node.js示例)

  // 定义JWT生成Token的规则函数
function sign (options, duration) {
  return jwt.sign(
    options, 
    process.env.ACCESS_SECRET,  // 加密口令,可以使Token变得难以破解
    {expiresIn: duration, // 过期时间}
  )
}
  
app.post('/login', async (ctx) => {
  // 1. 获取请求体中的数据
  // POST请求携带的参数都在ctx.request该请求体中
  const { username, password } = ctx.request.body;
  const user = await validateUser(username, password);
  
  if (!user) {
    return res.status(401).json({ error: 'Invalid credentials' });
  }

  const accessToken = sign(
    { userId: user.id, username: user.username },
    { expiresIn: '15m' }
  );

  const refreshToken = sign(
    { userId: user.id },
    { expiresIn: '7d' }
  );

  ctx.body({ 
    accessToken,
    refreshToken,
    user: { id: user.id, username: user.username }
  });
});

2. 前端存储Token -- 储存起来以便随时给后端发送鉴定权限

// 登录成功后存储
localStorage.setItem('accessToken', response.data.accessToken);
localStorage.setItem('refreshToken', response.data.refreshToken);

🔒 安全建议refreshToken建议使用HttpOnly Cookie存储,防止XSS攻击。

3. 请求拦截器:自动携带Token

// Axios请求拦截器--用于处理请求数据
axios.interceptors.request.use(request => {
  // 从浏览器的本地储存中获取token
  const access_token = localStorage.getItem('access_token')
  // 如果token存在,则在请求头中添加Authorization字段
  if (access_token) {
    request.headers.Authorization = access_token
  }
  return request // 请求放行
})

三、无感刷新:让用户体验丝滑如流水

1. 双Token设计原理

  • Access Token:短时效(15分钟),用于日常接口调用
  • Refresh Token:长时效(7天),用于换取新Access Token

Access Token 过期时校验 Refresh Token

  1. Refresh Token 没过期,则前端向后端发送一个刷新Token的请求,做到权限长期有效即只要用户不玩长期失踪,这个有效期就一直延长
  2. 若两个Token双双过期,则需要重新获取新的Token

想象这是健身房的会员卡:Access Token是每日签到卡(每天重新激活),Refresh Token是年卡(长期有效但需要定期续费)。

2. 刷新Token的流程

// 刷新Token接口
app.post('/refresh-token', async (ctx) => {
  const { refreshToken } = ctx.request.body;
  // 解析校验refresh_token是否有效
  const decoded = refreshVerify(refresh_token)
  
  if (decoded.id) {
    // 创建新的 长短 token
    // console.log(decoded);
    
    const access_token = sign(data, '1h')
    const refresh_token = sign(data, '7d')
    ctx.body = {
      code: '1',
      msg: 'token刷新成功',
      access_token: access_token,
      refresh_token: refresh_token,
    }

  } else {  // 长 token 也过期了
    ctx.status = 416
    ctx.body = {
      code: '0',
      msg: '登录状态已过期,请重新登录',
    }
  }
})

3. 前端自动刷新实现

// Axios响应拦截器
axios.interceptors.response.use(
  response => response,
  async error => {
    const originalRequest = error.config;
    
    // 如果是401错误且未重试过
    if (error.response?.status === 401 && !originalRequest._retry) {
      // 记录未成功的请求
        const originalRequest = error.config

        // 重新请求新的 access_token 和 refresh_token
        const refresh_token = localStorage.getItem('refresh_token')
        if (refresh_token) {
          axios.post('/user/refresh', {
            refresh_token: refresh_token,
          }).then(res => {
            if (res.code === '1') {
              // 成功获取新的 token
              localStorage.setItem('access_token', res.access_token)
              localStorage.setItem('refresh_token', res.refresh_token)

              // 更改原请求的 Authorization 头
              originalRequest.headers.Authorization = res.access_token

              // 重新发送原请求
              return axios(originalRequest)
            }
          })
        }
      }

      if (status === 416) {
        toast.error(error.response.data.msg)
        // 可以在这里处理跳转到登录页
        setTimeout(() => {
          window.location.href = '/login'
        }, 800)
      }
    } else {
      // 网络错误或其他错误
      toast.error('网络连接失败')
    }

    return Promise.reject(error)
  },
)

⚠️ 注意事项:需要防重试标识_retry防止无限递归,同时处理刷新失败的边界情况。


四、实战中的优化技巧

1. Token有效期策略

// 推荐配置
const accessTokenTTL = '15m';   // 15分钟
const refreshTokenTTL = '7d';   // 7天
const refreshBefore = '5m';     // 提前5分钟刷新

2. 安全增强措施

  • 使用HTTPS传输Token
  • 设置SameSite=Strict Cookie属性
  • 对敏感操作增加二次验证(如短信验证码)

3. 刷新Token的优雅降级

// 当同时收到多个401请求时
let isRefreshing = false;
let refreshSubscribers = [];

function onAccessTokenRefreshed(token) {
  refreshSubscribers.forEach(callback => callback(token));
  refreshSubscribers = [];
}

axios.interceptors.response.use(null, error => {
  if (error.response?.status === 401) {
    if (!isRefreshing) {
      isRefreshing = true;
      
      axios.post('/refresh-token', { refreshToken })
        .then(({ data }) => {
          localStorage.setItem('accessToken', data.accessToken);
          axios.defaults.headers.common['Authorization'] = `Bearer ${data.accessToken}`;
          onAccessTokenRefreshed(data.accessToken);
        })
        .catch(() => {
          localStorage.removeItem('accessToken');
          localStorage.removeItem('refreshToken');
          window.location.href = '/login';
        })
        .finally(() => isRefreshing = false);
    }
    
    return new Promise(resolve => {
      refreshSubscribers.push((token) => {
        originalRequest.headers['Authorization'] = `Bearer ${token}`;
        resolve(axios(originalRequest));
      });
    });
  }
});

五、总结:打造无缝衔接的安全体验

通过JWT双Token机制,我们实现了:

  • 无状态认证:服务器无需存储会话信息
  • 跨域支持:天然适应微服务架构
  • 无感刷新:用户无需频繁登录
  • 安全性保障:通过加密签名防止篡改

🚀 进阶建议:可以结合OAuth 2.0协议,将认证授权与业务逻辑解耦,进一步提升系统的可扩展性。

在现代Web开发中,JWT已成为身份认证的标配。通过合理的设计和实现,我们既能保障系统安全,又能为用户提供流畅的操作体验。记住:安全性和用户体验从来不是对立面,而是可以通过巧妙设计达到平衡的两个维度。