登录页与接口调用(前端)
一个最小可用的登录页:输入用户名密码 → POST JSON 到后端 → 拿到令牌后跳转首页。核心是处理三种状态:加载中(按钮禁用)、失败(展示后端 message)、成功(保存令牌跳转)。
完整页面
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>登录</title>
<style>
.login-box { width: 320px; margin: 100px auto; padding: 32px; border: 1px solid #ddd; border-radius: 8px; }
.form-item { margin-bottom: 16px; }
input { width: 100%; padding: 8px; box-sizing: border-box; }
button { width: 100%; padding: 10px; cursor: pointer; }
.msg { color: #e53935; min-height: 20px; font-size: 13px; }
</style>
</head>
<body>
<form id="login-form" class="login-box">
<h2>用户登录</h2>
<div class="form-item">
<label>用户名</label>
<input id="username" name="username" autocomplete="username" required>
</div>
<div class="form-item">
<label>密码</label>
<input id="password" name="password" type="password" required minlength="6">
</div>
<p id="msg" class="msg"></p>
<button type="submit" id="submit-btn">登录</button>
</form>
<script>
const form = document.getElementById('login-form');
const msg = document.getElementById('msg');
const btn = document.getElementById('submit-btn');
form.addEventListener('submit', async (e) => {
e.preventDefault(); // 阻止表单默认同步提交
const payload = {
username: form.username.value.trim(),
password: form.password.value
};
if (!payload.username || !payload.password) {
msg.textContent = '请输入用户名和密码';
return;
}
btn.disabled = true;
btn.textContent = '登录中...';
try {
const res = await fetch('http://localhost:8080/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
const body = await res.json();
if (res.ok && body.code === 0) {
sessionStorage.setItem('token', body.data.token);
location.href = '/index.html';
} else {
msg.textContent = body.message || '登录失败,请重试';
}
} catch (err) {
msg.textContent = '网络异常,请稍后重试';
} finally {
btn.disabled = false;
btn.textContent = '登录';
}
});
</script>
</body>
</html>
关键点
- 请求方式:
fetch发 POST,body 是JSON.stringify()后的 JSON 字符串,必须设置Content-Type: application/json,后端才能用@RequestBody反序列化。 - 表单校验:
required、minlength由浏览器先校验一次,减少无意义请求;JS 里再做一次非空判断兜底。 - 失败提示:后端返回统一结构(
code/message/data),前端只展示message,绝不出来自堆栈的原始报错。
令牌存储与携带
// 后续请求统一封装:自动附加 Authorization 头
async function api(url, options = {}) {
const token = sessionStorage.getItem('token');
return fetch(url, {
...options,
headers: {
'Content-Type': 'application/json',
...(token ? { Authorization: `Bearer ${token}` } : {}),
...options.headers
}
});
}
:::warning 令牌安全
生产环境推荐后端通过 Set-Cookie(HttpOnly + Secure + SameSite)下发令牌,前端 JS 完全碰不到,天然防 XSS 窃取。上面的 sessionStorage 仅作演示;令牌禁止存入 localStorage,也绝不能拼进 URL 查询参数。
:::
面试常问
- 为什么用 JSON 而不是表单提交? 结构化、可扩展、前后端契约清晰;只有文件上传才用
multipart/form-data。 - 密码要不要前端加密? 必须走 HTTPS 传输;前端 MD5 不增加安全性,真正的底线是密码绝不能明文入库——后端用 BCrypt 哈希存储。
- 失败要不要自动重试? 4xx 是业务错误,重试没有意义,直接提示;只有 5xx 或网络异常才考虑有限次重试(最多 3 次)。