
做前端这么多年登录验证几乎是每个项目都绕不开的坎。别看不起这个“基础登录验证”里面埋的坑比想象中多得多。用JavaScript实现基础登录验证看起来就是把用户名密码填进去点个按钮但背后涉及表单校验、状态管理、接口交互、安全防护等一系列问题。这篇文章我就从实际开发角度把用原生JavaScript做登录验证的完整链路拆开讲清楚包括设计思路、核心代码、常见坑和排查技巧适合刚入门前端、或者准备自己动手写一个完整登录模块的同学参考。1. 登录验证的整体设计与思路拆解1.1 前端验证和后端验证到底各管什么很多人一上来就纠结登录验证到底该在前端做还是后端做我的答案是两个都要做但职责完全不同。前端验证的核心目的是“提升用户体验”和“减少无效请求”——用户填了个格式明显不对的邮箱、密码长度只有3位这时候根本不需要发给后端直接在前端提示“格式不对”就行了省得用户等半天网络往返才发现自己输错了。而后端验证才是真正决定“你能不能登录”的关口它要查数据库、比对密码哈希、签发token这套逻辑绝不能依赖前端。但很多教程只讲后端或者只讲前端导致新手经常搞混。用JavaScript实现基础登录验证我们关注点在前端部分怎么把表单数据拿全、怎么做格式校验、怎么把请求发出去、怎么处理返回结果、怎么记住登录状态。这些搞清楚了配合任意后端语言都能无缝衔接。注意前端验证永远只是“锦上添花”不能当作安全屏障。真正的安全校验必须在后端做前端所有校验都可以被绕过——直接改JS代码或者用Postman发请求就绕过了。1.2 方案选型原生JavaScript还是框架现在前端环境里Vue、React遍地都是但用原生JavaScript实现基础登录验证依然有不可替代的价值。第一它不依赖任何框架任何项目里都能用哪怕你后面用Vue核心逻辑也是相通的只是换了个语法壳子。第二它可以让你把“登录验证”这件事的本质看透——数据从哪来、怎么校验、状态存哪里、怎么恢复这些东西在框架里往往被封装成了看似简单的命令但底层原理一模一样。我建议初学者一定要用原生JS手写一遍登录验证等到你理解了每一步再用框架里的对应功能就会觉得“原来如此”。这篇文章的代码我都用原生JavaScript写方便你直接复制到浏览器里跑也方便你理解每一行在干什么。在设计思路上我把一个基础登录拆成四块表单获取与校验、请求模拟与处理、登录态存储、页面状态恢复。别小看这四块每一块都有隐藏的地雷下面我一个个拆开讲。2. 核心功能拆解表单校验与密码处理2.1 表单数据获取与基础非空校验拿到一个登录表单最原始的做法是给按钮绑定click事件然后document.getElementById去取值。但这里有一个关键点必须阻止form的默认提交行为否则页面会刷新你的JS白写了。比如下面这段form idloginForm input typetext idusername placeholder用户名 input typepassword idpassword placeholder密码 button typesubmit登录/button /formconst form document.getElementById(loginForm); form.addEventListener(submit, function (e) { e.preventDefault(); // 阻止表单默认提交这是第一步 const username document.getElementById(username).value.trim(); const password document.getElementById(password).value.trim(); if (!username || !password) { alert(用户名和密码不能为空); return; } // 继续后续逻辑 });这里我特别用了trim()原因很现实用户复制粘贴用户名时很容易带一个看不见的空格如果不去掉后端比对的时候就会神秘地失败。这类问题排查起来特别费时间所以前端取值时养成trim()的习惯能省很多事。非空校验是最基础的但建议你把错误提示做成红色小字显示在对应输入框下方而不是用alert()弹窗。alert()会阻塞页面体验很差而且弹窗一多用户会烦。后面我会专门写一个校验反馈函数可以复用。2.2 用正则校验邮箱、手机号和密码强度基础非空校验只是开胃菜真正的规则校验才是核心。不同业务对用户名和密码的规则要求不一样常见的登录名可以是邮箱、手机号、或者自定义用户名。我们在前端至少需要一个正则表达式来判断“格式对不对”避免把明显非法数据发给后端。比如邮箱校验我常用的规则是function isValidEmail(email) { return /^[a-zA-Z0-9._%\-][a-zA-Z0-9.\-]\.[a-zA-Z]{2,}$/.test(email); }手机号校验要注意中国大陆手机号目前是11位开头是1第二位通常是3、4、5、6、7、8、9所以可以写function isValidPhone(phone) { return /^1[3-9]\d{9}$/.test(phone); }密码强度这块基础登录一般要求至少8位包含字母和数字。有些产品会要求大小写字母数字特殊字符的组合但那种往往用在注册页面登录页面只要校验“不能为空”就够了因为密码格式不对在注册时已经被拦住了。不过如果你想让登录页也做强度提示可以这样function getPasswordStrength(pwd) { let score 0; if (pwd.length 8) score; if (/[a-z]/.test(pwd) /[A-Z]/.test(pwd)) score; if (/\d/.test(pwd)) score; if (/[^a-zA-Z0-9]/.test(pwd)) score; return score; // 0-4 }这里有个很容易忽略的细节正则表达式不要每次都在循环里用字面量创建如果在一段代码里反复调用正则建议写成一个函数或者在模块顶部定义常量避免重复创建带来的微小性能损耗。当然登录验证这种低频操作无所谓但好习惯要从一开始养成。2.3 密码加密的误区与正确姿势很多初学者有个天大的误解以为在前端JS里把密码用MD5或者SHA256哈希一下就“安全”了。每次看到有人这么写我就着急——前端做哈希根本防不住真正的攻击者因为攻击者不需要你的前端代码直接抓到请求就能伪造。而且如果你把MD5后的结果作为密码发送实际上这个哈希值本身就成了密码这叫“哈希即口令”反而更危险。那前端到底要不要处理密码我的建议是不需要做任何加密直接把原始密码放在HTTPS请求里发送。HTTPS已经在传输层做了加密密码在网络上不会明文暴露。你唯一要做的是确认你的接口地址是https://开头而不是http://。如果公司内网环境没有HTTPS那是另一个层面的安全问题不是前端JS能解决的。实用建议前端不要自己设计加密方案把密码加密交给后端的bcrypt等专业密码哈希算法处理。前端做得越多越容易画蛇添足。你可以在提交前给密码做个简单的全角半角处理比如把中文输入法下的全角空格、全角字母转成半角避免用户切输入法导致密码不对。function normalizePassword(pwd) { return pwd.replace(/[\uFF01-\uFF5E]/g, function (ch) { return String.fromCharCode(ch.charCodeAt(0) - 0xFEE0); }).replace(/\u3000/g, ); }这个函数很实用尤其是移动端用户经常忘记切输入法全角符号会让密码验证失败。把这个函数加在提交前能少收到不少“我密码明明对啊”的工单。3. 登录状态管理与模拟接口实战3.1 用 localStorage 还是 sessionStorage 保存登录态前端验证通过后后端会返回一个登录凭证比如token。前端要把这个token存下来之后每次请求带上它。存哪里两个选择localStorage和sessionStorage。区别很简单sessionStorage当前标签页关闭就没了刷新页面还在。localStorage永久保存除非手动清除或代码删除。对于“记住我”这个需求通常是如果用户勾选了“记住我”就用localStorage否则用sessionStorage。但这里有个安全考量尤其是如果用XSS漏洞获取到localStorage里的token攻击者就能冒充用户。所以现在很多团队倾向于把token放在httpOnly的Cookie里由后端设置JS读不到前端自动携带这样XSS也拿不到token。但基础登录验证场景下后端可能还没做Cookie方案为了演示我还是先用localStorage。你要清楚这个选择背后的利弊。如果项目安全等级高务必和后端商量改成httpOnlyCookie方案。function saveToken(token, remember) { if (remember) { localStorage.setItem(token, token); } else { sessionStorage.setItem(token, token); } }注意存token的时候不要把用户密码存进去只存服务端签发的token。密码在登录成功后就应该从内存里丢掉不要再保留在变量里。有些人图方便把整个用户对象包含密码字段塞进localStorage这是事故高发点。3.2 模拟后端接口Promise setTimeout在没有真实后端的时候怎么把登录流程跑通用Promise加setTimeout模拟一个异步接口这在前端开发里特别常用。你可以把它当作一个替身后端等后端接口写好了直接替换成fetch调用就行。下面我写一个模拟登录接口function mockLogin(username, password) { return new Promise((resolve, reject) { setTimeout(() { // 模拟一个硬编码账号 const validUser admin; const validPwd 123456; if (username validUser password validPwd) { resolve({ code: 0, message: 登录成功, token: mock-token- Date.now(), user: { name: admin, avatar: } }); } else { reject({ code: 1001, message: 用户名或密码错误 }); } }, 800); // 模拟网络延迟800ms }); }有人会问为什么要用Promise因为真实网络请求是异步的前端在等待响应的过程中不能把页面卡死。用Promise能让你的代码更好地适应后续换成fetch或axios。而且setTimeout模拟延迟还能顺便做一个loading状态防止用户重复点击。调用的时候务必加上try...catch或者.catch()来处理失败很多新手容易只处理成功的情况失败的时候直接报错“Cannot read property token of undefined”。看下面这个标准调用写法async function handleLogin(e) { e.preventDefault(); const username document.getElementById(username).value.trim(); const password document.getElementById(password).value.trim(); // 前端校验略... const loginBtn document.getElementById(loginBtn); loginBtn.disabled true; loginBtn.textContent 登录中...; try { const res await mockLogin(username, password); if (res.code 0) { saveToken(res.token, document.getElementById(remember).checked); window.location.href /dashboard.html; } else { showError(res.message); } } catch (err) { showError(err.message || 网络异常请稍后重试); } finally { loginBtn.disabled false; loginBtn.textContent 登录; } }finally这里很关键无论成功失败都要恢复按钮状态否则报错后按钮一直是“登录中...”灰色状态用户只能刷新页面。3.3 登录状态的守卫与退出登录登录态存好了接下来要处理一个基础但核心的问题哪些页面需要登录才能访问。这就是前端路由守卫单页应用里尤为常见。原生JS的基础实现思路是在页面加载时检查token是否存在不存在则跳转到登录页。function requireAuth() { const token localStorage.getItem(token) || sessionStorage.getItem(token); if (!token) { window.location.href /login.html; return false; } return true; } // 在需要保护的页面顶部调用 requireAuth();但这只是“前端看得见”的守卫真正的保护必须放在后端接口上——即使有人直接打开页面HTML没有token调接口也会被后端拒绝。前端守卫只为用户体验让没登录的人尽快看到登录页而不是看到一堆报错数据。退出登录就简单了清掉token跳回登录页function logout() { localStorage.removeItem(token); sessionStorage.removeItem(token); window.location.href /login.html; }这里建议同时清两个存储位置防止有的用户勾了“记住我”有的没勾在同一个浏览器里切换状态导致残留。4. 常见问题与排查技巧实录4.1 刷新页面后登录状态丢失这个问题几乎是老生常谈了但还是不断有人踩。前面提到了sessionStorage只在当前标签页有效如果用户开了个新标签页登录状态就没了这不算bug是设计如此。但如果你明明用的是localStorage刷新后还是掉登录就要排查一下是不是代码里有清storage的逻辑被误执行了。我见过一个奇葩案例业务代码里在某个公共模块加载时调用了localStorage.clear()原因是想清缓存结果把登录token也清了。所以写清除缓存功能时一定要精确指定key不要用clear()一把梭。如果你需要排查刷新丢状态在浏览器控制台执行localStorage.getItem(token)看看有没有值没有值说明存储那步就没成功有值但页面跳转登录页那说明守卫逻辑读错了key。4.2 表单校验不通过但不提示原因很多新手写校验时习惯用if...else if最后每个分支里只是return false用户完全不知道哪里错了。正确的做法是把错误信息动态展示到对应的表单控件旁边。至少要做到每次校验时先把所有错误提示清空然后对每个失败项写入具体提示比如“用户名不能为空”“邮箱格式不正确”“密码长度不足8位”。一个简单实现function showError(inputId, message) { const input document.getElementById(inputId); const errorDiv input.parentElement.querySelector(.error-msg); if (errorDiv) { errorDiv.textContent message; errorDiv.style.display block; } input.classList.add(input-error); } function clearAllErrors() { document.querySelectorAll(.error-msg).forEach(function (el) { el.textContent ; el.style.display none; }); document.querySelectorAll(.input-error).forEach(function (el) { el.classList.remove(input-error); }); }样式层面给.error-msg加一个红色的color: #e53935字号12px。给.input-error加一个红色边框用户一眼就能看到哪个输入框有问题。这比alert或者控制台报错友好太多了。4.3 防止重复提交与按钮loading处理用户手快点了两次登录按钮会产生两个并发请求后端如果没做幂等处理可能出现token互相覆盖、验证码失效等问题。前端防重复提交的做法就是我在调用接口前把按钮disabled在finally里恢复。但要注意一个细节在disabled后要防止回车键再次触发提交因为disabled的按钮在form中不会触发submit事件但如果用户是在输入框里按回车form依然会提交。所以除了按钮禁用还要在提交处理函数开头判断一个标志位let isSubmitting false; async function handleLogin(e) { e.preventDefault(); if (isSubmitting) return; isSubmitting true; try { // ... } finally { isSubmitting false; } }这个标志位比单纯禁按钮更可靠。我建议两个都做一个是从交互上防一个是从逻辑上防。还有个小技巧在用户切换输入框内容时如果之前有错误提示应该及时清除而不是等到提交才清。给每个输入框加input事件监听在用户重新输入时去掉对应的红色边框和错误文案体验会好很多。5. 进阶技巧与个人实操体会5.1 加一个简单的记住密码功能虽然“记住密码”听起来老套但实际需求很常出现。常规做法是登录成功后如果勾选了“记住我”把用户名不是密码存到localStorage下次进入登录页时回填。密码不要存密码明文存浏览器是高风险行为浏览器自带密码管理器会帮你处理轮不到你写代码存。function rememberUsername(username) { if (document.getElementById(remember).checked) { localStorage.setItem(rememberedUsername, username); } else { localStorage.removeItem(rememberedUsername); } } // 页面初始化时回填 window.addEventListener(DOMContentLoaded, function () { const remembered localStorage.getItem(rememberedUsername); if (remembered) { document.getElementById(username).value remembered; document.getElementById(remember).checked true; } });5.2 使用 JWT 的前端处理思路既然热搜词里出现了JWT我就多提一嘴。JWTJSON Web Token是目前最常见的登录凭证格式后端登录成功后返回一个长字符串里面分成三段用点号分隔。前端拿到JWT后同样是存到localStorage或httpOnly Cookie里之后每次请求在Authorization请求头里带上fetch(/api/user/info, { headers: { Authorization: Bearer token } })这里有个小知识点JWT的有效期通常在token本身里携带比如exp字段。前端可以解码JWT的payload部分来判断token是否过期但不要试图在前端篡改token内容因为JWT有签名后端会验证。基础登录里我一般建议大家直接依赖后端的401响应来判过期前端代码不用太复杂const res await fetch(/api/user/info, { headers: { Authorization: Bearer token } }); if (res.status 401) { logout(); // token失效清空并跳转登录 }5.3 关于密码传输和存储的最后提醒我知道很多新手学到这里还是忍不住想问那就真的什么安全措施都不做吗其实你该做的事是申请并强制使用HTTPS不在前端存储密码不把密码写进日志不使用不安全的第三方CDN脚本。这些比你在JS里加十层加密都管用。如果公司没有HTTPS你应该推动运维配置而不是试图用JS解决。我曾经遇到一个项目前端把密码用AES加密后传给后端后端解密做验证中间还自己设计了密钥交换逻辑。结果有次密钥写死了前端代码一泄露所有用户密码都能被解开。最后我们痛定思痛全部改成HTTPS 后端bcrypt反而再没出过安全问题。所以说前端在做登录验证时最该学会的是“不要乱加戏”。5.4 从基础登录到完整权限系统的扩展思路当你把基础登录验证跑通后再往后扩展就比较顺了。你可以在此基础上增加验证码图形验证码或短信验证码、登录试错锁定比如连续输错5次锁定10分钟、基于角色的权限路由比如普通用户和管理员看到不同页面、记住登录设备管理查看当前登录的所有设备并支持下线等。这些功能虽然听起来复杂但核心的“前端拿凭证、存状态、带凭证请求”这个模式不会变。我个人的做法是平时写一个小工具页面把所有常用的前端验证逻辑积累下来包括邮箱正则、密码强度、防重复提交、token存储等随用随取。这次写登录验证也算是对自己常用代码的一次整理。如果你也想自己做一套登录验证我建议不用把目标定太大先把“提交——校验——请求——存token——跳转——退出”这条主链路走通再慢慢加细节。就像跑步一样先学会走路再学加速跑起来才不容易摔跤。