前端密码过期问题的实战解决方案与避坑指南

百里利芹 安全 阅读 2,099
赞 20 收藏
二维码
手机扫码查看
反馈

先看代码,再扯别的

上周上线前一小时,测试小姐姐甩给我一个 bug:“密码明明没过期,为啥一直弹修改密码的框?” 我当场就懵了。查了半天后端接口、用户状态字段,最后发现是前端缓存了登录时间,自己算了个“假过期”。这事儿折腾了我俩小时,血亏。

前端密码过期问题的实战解决方案与避坑指南

所以今天这篇,直接上干货,怎么在前端靠谱地处理密码过期逻辑。不是那种教科书式的“定义-流程-图解”,我是真在项目里用过的,亲测有效。

核心逻辑:别自己瞎算

一开始我傻乎乎地想:用户登录时间记下来,密码有效期 90 天,那我就拿当前时间减一下,超过 90 天就提示改密码。结果呢?后端策略一变(比如临时延长到 120 天),前端直接失控。

后来我改了思路:**前端只负责展示,不负责判断**。该不该改密码?让后端说了算。

后端返回的用户信息里加个字段:

{
  "user": {
    "id": 123,
    "username": "dev_zhang",
    "password_expired": true,
    "password_expires_in_days": 7
  }
}

前端拿到这个数据,直接根据 password_expired 字段决定是否弹窗。简单粗暴,但稳。

实战代码:登录后检查 + 路由守卫

我在 Vue 项目里这么写的,React 同理,换钩子就行。

// store/modules/auth.js
const state = {
  userInfo: null,
  showPasswordExpiredModal: false
}

const mutations = {
  SET_USER_INFO(state, info) {
    state.userInfo = info
    // 登录时立即检查
    if (info?.password_expired) {
      state.showPasswordExpiredModal = true
    } else {
      state.showPasswordExpiredModal = false
    }
  },
  CLEAR_PASSWORD_EXPIRED_MODAL(state) {
    state.showPasswordExpiredModal = false
  }
}

const actions = {
  async login({ commit }, credentials) {
    const res = await api.post('/auth/login', credentials)
    const { data } = res
    commit('SET_USER_INFO', data.user)
    return data
  }
}

然后在路由守卫里也加一层保险,防止页面跳转时漏掉:

// router/index.js
router.beforeEach((to, from, next) => {
  const isAuthenticated = store.getters.isAuthenticated
  const showExpireModal = store.state.auth.showPasswordExpiredModal

  if (isAuthenticated && to.name !== 'ChangePassword') {
    if (showExpireModal) {
      // 强制跳转到改密码页
      next({ name: 'ChangePassword', query: { redirect: to.fullPath } })
      return
    }
  }

  next()
})

这里注意下,我踩过坑:一开始没在路由守卫里加判断,用户手动输入 URL 就能绕过弹窗。加了之后才真正兜住。

这个场景最好用:静默检查 + 倒计时提醒

不是所有情况都要强制改密码。有时候还剩 7 天过期,可以先给个非阻断式提醒,让用户心里有数。

我在首页顶部加了个小横幅:

<div v-if="daysLeft >= 0 && daysLeft <= 7" class="bg-yellow-100 p-2 text-center text-sm">
  您的密码将在 {{ daysLeft }} 天后过期,请及时修改。
  <a href="#" @click.prevent="$router.push('/profile/change-password')" class="text-blue-600 underline">
    立即修改
  </a>
</div>
// computed property
computed: {
  daysLeft() {
    const user = this.$store.state.auth.userInfo
    return user?.password_expires_in_days ?? -1
  }
}

这个体验比一上来就弹窗友好多了。用户不会觉得被“绑架”,但我们该提醒的也提醒了。

踩坑提醒:这三点一定注意

  • 别信本地时间:我之前用 new Date() 和后端时间对比,结果用户电脑时间调快了两天,直接误判。现在统一以后端返回的倒计时天数为准,前端只做展示。
  • modal 出现后别让用户逃掉:如果 password_expired: true,就必须去改密码页。我加了路由拦截 + 按钮禁用(按 ESC、点遮罩都不行),逼用户面对现实。
  • 改完密码要刷新用户信息:很多人改完密码,password_expired 字段没更新。记得在提交成功后重新拉一次用户信息,否则会无限循环弹窗。

附上改密码后的刷新逻辑:

async submitChangePassword() {
  await api.post('/auth/change-password', this.form)
  // 重新获取用户信息
  const freshUser = await api.get('/auth/user')
  this.$store.commit('SET_USER_INFO', freshUser.data)
  // 关闭 modal 或跳回原页面
  const redirect = this.$route.query.redirect || '/'
  this.$router.replace(redirect)
}

API 设计建议:前端不要太被动

虽然我说“前端不判断”,但也不是完全躺平。我和后端商量加了个接口:

// GET https://jztheme.com/api/v1/auth/password-expired-check
// 返回
{
  "expired": true,
  "days_left": 0
}

这个接口可以在用户长时间停留页面时轮询调用(比如每 30 分钟一次),避免中途策略变更导致前端状态滞后。不过别太频繁,免得被防刷机制干掉。

高级技巧:把逻辑抽成指令 or 插件

如果你多个项目都用这套,可以封装成 Vue 指令或者全局 mixin。

这是我抽出来的简易插件:

// plugins/password-expiry-guard.js
export default {
  install(Vue, options) {
    Vue.prototype.$checkPasswordExpiry = function(to, from, next) {
      const user = this.$store.state.auth.userInfo
      if (!user) return next()

      if (user.password_expired && to.name !== 'ChangePassword') {
        return next({
          name: 'ChangePassword',
          query: { redirect: to.fullPath }
        })
      }

      next()
    }
  }
}

然后在 main.js 里 use 一下,就可以在各个路由守卫里用 this.$checkPasswordExpiry 了,省得重复写。

关于“记住密码”和自动填充的兼容问题

有个隐藏坑:用户点了“修改密码”页面,浏览器自动填充了旧密码,这时候你得清空密码框。不然用户可能直接提交旧密码,以为改了,其实没生效。

我的做法是:进入页面时主动 clear 表单,并且监听 focus 事件打个标记,防止自动填充干扰用户体验。

mounted() {
  this.$nextTick(() => {
    // 清空密码框,防止自动填充
    this.form.old_password = ''
    this.form.new_password = ''
    this.form.confirm_password = ''

    // 可选:加个 delay 防止 autofill 劫持
    setTimeout(() => {
      this.autoFillDetected = false
    }, 1000)
  })
}

结语

以上是我踩坑后的总结,希望对你有帮助。密码过期这事看起来小,真出问题就是线上事故——用户登不进系统,客服电话被打爆。

核心就一句:**前端别自作聪明算过期时间,一切以 API 返回为准**。剩下的就是用户体验上的微调了。

这个技巧的拓展用法还有很多,比如结合 JWT 过期、双因素认证提醒等,后续会继续分享这类博客。

有更优的实现方式欢迎评论区交流,我现在这个方案也不是完美的,比如轮询间隔还得根据业务权衡,但至少目前跑得稳。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论