1. 案例背景与需求
1.1 需求描述
实现一个用户登录功能:
- 页面提供用户名和密码输入框,点击登录按钮,通过AJAX提交数据。
- 后端验证用户名密码(固定为
lisi/admin),验证成功则将用户名存入Session,并返回true;否则返回false。 - 前端根据返回结果,成功则跳转到主页,失败则弹出提示。
- 主页通过另一个AJAX请求获取当前登录的用户名并显示。
1.2 为什么用AJAX?
为了更好的用户体验,不刷新整个页面即可完成登录。AJAX允许异步通信,页面无闪动。
1.3 涉及接口
- 登录接口:
/user/login(POST) - 获取登录用户接口:
/user/getLoginUser(GET)
1.4 结果演示


2. 接口文档定义
2.1 登录接口
| 项目 | 内容 |
|---|---|
| 请求路径 | /user/login |
| 请求方法 | POST |
| 接口描述 | 用户登录,验证用户名密码 |
请求参数
| 参数名 | 类型 | 是否必须 | 说明 |
|---|---|---|---|
| username | String | 是 | 用户名 |
| password | String | 是 | 密码 |
请求示例(表单格式):
username=lisi&password=admin
响应数据
- Content-Type :
application/json;charset=UTF-8 - 响应体 : JSON布尔值
true或false
响应示例:
json
true
2.2 获取登录用户接口
| 项目 | 内容 |
|---|---|
| 请求路径 | /user/getLoginUser |
| 请求方法 | GET |
| 接口描述 | 获取当前登录用户的用户名 |
请求参数
无
响应数据
- Content-Type :
text/plain;charset=UTF-8 - 响应体: 用户名(字符串),如果未登录则返回空或 null
响应示例:
lisi
2.3 为什么这样定义?
- 登录接口返回JSON布尔值,便于前端直接判断。
- 获取用户接口返回纯文本,因为只是简单字符串。
- 登录接口使用POST,因为提交数据;获取用户接口使用GET,因为获取资源。
3. 前端开发:基于接口文档实现
3.1 登录页面(login.html)
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>登录页面</title>
</head>
<style>
/* 页面整体居中+浅背景 */
body {
font-family: "Microsoft YaHei", sans-serif;
background-color: #f8f9fa;
margin: 0;
min-height: 100vh;
display: flex;
justify-content: center; /* 水平居中 */
align-items: center; /* 垂直居中 */
}
/* 登录表单容器:白色背景+轻微阴影+圆角 */
.login-box {
background: white;
padding: 30px 40px;
border-radius: 8px;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
width: 300px;
}
/* 标题居中+深蓝色 */
h1 {
color: #2d3748;
text-align: center;
margin-top: 0;
margin-bottom: 20px;
}
/* 输入框美化:浅边框+圆角+聚焦变色 */
input[type="text"], input[type="password"] {
width: 100%;
padding: 8px;
margin: 8px 0 12px 0;
border: 1px solid #dee2e6;
border-radius: 4px;
box-sizing: border-box;
color: #2d3748;
}
input[type="text"]:focus, input[type="password"]:focus {
outline: none;
border-color: #4299e1;
}
/* 登录按钮:居中+蓝色+轻微hover效果 */
input[type="button"] {
width: 100%;
background-color: #4299e1;
color: white;
border: none;
padding: 10px;
border-radius: 4px;
cursor: pointer;
margin-top: 8px;
font-size: 14px;
}
input[type="button"]:hover {
background-color: #3182ce;
}
</style>
<body>
<div class="login-box">
<h1>用户登录</h1>
用户名:<input name="userName" type="text" id="userName"><br>
密码:<input name="password" type="password" id="password"><br>
<input type="button" value="登录" onclick="login()">
</div>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.4/jquery.min.js"></script>
<script>
function login() {
//实现登录的话,那么这里边我们使用ajax,因为他可以做到异步通信,而不需要刷新这个页面
$.ajax({
method: "post",
url: "/user/login",
data: {
username: $("#userName").val(),
password: $("#password").val()
},
success: function(result) {//注意:这里只是看HTTP请求是否成功,不涉及是否返回值
//如果成功
if(result) {
//调转页面
location.href="index.html";
}else{
alert("用户名或者密码错误,请确认!");
}
}
});
}
</script>
</body>
</html>
3.2 主页(index.html)
html
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>用户登录首页</title>
</head>
<style>
body {
font-family: "Microsoft YaHei", sans-serif;
background-color: #f8f9fa;
margin: 0;
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
font-size: 18px;
}
/* 标签深灰色,用户名蓝色加粗 */
.welcome-text {
color: #2d3748;
}
#loginUser {
color: #4299e1;
font-weight: bold;
margin-left: 8px;
}
</style>
<body>
登录人: <span id="loginUser"></span>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.4/jquery.min.js"></script>
<script>
//使用ajax请求获取后台的用户登录信息
$.ajax({
method: "get",
url: "/user/getLoginUser",
success: function(result){
$("#loginUser").text(result);
}
});
</script>
</body>
</html>
3.3 关键点解释
- 登录请求 :
data是一个对象,jQuery默认会将其转换为application/x-www-form-urlencoded格式,并设置Content-Type为该格式。这里我们没有手动设置contentType,因为后端接口期待的是表单格式(从接口文档看,参数是username和password,后端没有用@RequestBody,所以默认从请求参数取值,正好匹配表单格式)。如果后端要求JSON,则需要手动设置。 - 响应处理 :
success中的result是 jQuery 根据响应头自动解析后的结果。由于登录接口返回的是application/json,jQuery会将其解析为布尔值,所以可以直接用if(result)。 - 获取用户请求:GET请求,无参数,响应是纯文本,jQuery直接作为字符串,插入到span中。
3.4 浏览器发送的登录请求报文
4. 后端开发:根据接口文档实现
4.1 Controller代码
java
package com.zhongge.demo;
import jakarta.servlet.http.HttpSession;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @ClassName UserLoginController
* @Description TODO 用户登录控制器
* @Author 笨忠
* @Date 2026-02-22 15:13
* @Version 1.0
*/
@RequestMapping("/user")
@RestController
public class UserLoginController {
/**
* 用户登录
* @param username 用户名
* @param password 密码
* @param session session对象,用于存储用户信息
* @return true-登录成功 false-登录失败
*/
@RequestMapping("/login")
public boolean login(String username, String password, HttpSession session) {
//参数校验:使用的是工具类StringUtils
if (!StringUtils.hasLength(username) || !StringUtils.hasLength(password)) {
return false;//如果参数是null或者是为""那么就是登录失败
}
//用户校验
if ("lisi".equals(username) && "admin".equals(password)) {
//存储用户信息
session.setAttribute("username", "lisi");
return true;
}
return false;//如果程序到这里 那么就是登录失败
}
/**
* 获取用户的登陆信息【用户名】
* @param session 存储用户信息
* @return 用户信息
*/
@RequestMapping("/getLoginUser")
public String getUserInfo(HttpSession session) {
//根据sessionid找到对应的session对象,然后再根据对应的键找到对应的值
String username = (String) session.getAttribute("username");
return username;
}
}
4.2 关键点解释
@RestController:等同于@Controller+@ResponseBody,所有方法返回值直接写入响应体。login方法参数 :String username, String password没有注解,SpringMVC从请求参数中获取(因为前端发的是表单格式)。HttpSession session由Spring自动注入。- 返回值
boolean:Spring会使用MappingJackson2HttpMessageConverter将true/false序列化为JSON布尔值,并设置响应头Content-Type: application/json。因为返回的是对象(boolean是基本类型,但会被自动装箱为Boolean),不是String,所以默认使用JSON转换器。 getUserInfo返回值String:Spring使用StringHttpMessageConverter,设置响应头text/plain,符合接口文档。- Session的使用:登录成功时将用户名存入session,后续请求通过session获取。session通过cookie中的JSESSIONID维持。
4.3 响应报文:
4.4 后端处理获取用户请求的响应报文:
5. 深入理解:为什么登录接口不用 @RequestBody?
因为前端发送的是表单格式,后端可以直接从请求参数中获取。如果前端发送的是JSON,则必须用 @RequestBody。接口文档定义了参数格式为表单,所以前后端都遵守了这个约定。
6. 接口文档的作用再强调
- 明确参数格式 :本例中参数是
application/x-www-form-urlencoded,所以后端不加注解就能接收。 - 明确响应格式 :登录接口返回JSON,前端才能用
result直接判断。 - 避免歧义:如果前端发JSON而后端用表单接收,就会出错。
7. 常见问题
Q1:为什么登录接口返回的是布尔值,而获取用户接口返回的是字符串?
根据需求,登录只需要知道成功与否,布尔值足够;获取用户需要显示用户名,所以返回字符串。接口设计要符合业务语义。
Q2:如果登录失败,后端返回 false,前端 if(result) 会怎样?
false 被转为JSON false,前端 if(false) 不成立,进入else,弹出错误。
Q3:为什么主页的AJAX请求能拿到用户名?
因为浏览器在登录成功后收到了 Set-Cookie,后续请求会自动携带该cookie,后端就能识别会话。
8.详细讲解整个过程中HTTP通信的流程
我们一步步拆解,把每个细节都讲透。
(一)、HTTP 请求中,参数到底可以放在哪里?
首先,HTTP 请求中传递参数的位置主要有两个:
- URL 的查询字符串(Query String) :位于 URL 的
?之后,例如GET /user/login?username=lisi&password=admin。 - 请求体(Request Body):位于 HTTP 报文的数据部分,例如 POST 请求中可以携带表单数据或 JSON 字符串。
为什么要有两个位置?
因为 HTTP 协议定义了不同的请求方法(GET、POST、PUT 等),它们对参数的位置有不同的约定和限制:
-
GET 请求 :通常用于获取资源,参数放在 URL 的查询字符串中,因为 GET 没有请求体(或者说不应该用请求体,虽然理论上可以,但大多数服务器忽略 GET 的请求体)。

-
POST 请求 :通常用于提交数据,参数可以放在请求体中,也可以放在 URL 中(但一般放在请求体中),因为请求体容量更大,可以传输更多数据,且不会暴露在地址栏。

但请注意:参数放在哪里,并不完全由 HTTP 方法决定,还取决于前端发送时的编码方式和 Content-Type。
(二)、前端是如何决定参数位置的?
以我们的登录案例为例,前端使用 jQuery 的 $.ajax 发送 POST 请求:
javascript
$.ajax({
method: "post",
url: "/user/login",
data: {
username: $("#userName").val(),
password: $("#password").val()
},
...
});
这里 data 是一个 JavaScript 对象,jQuery 默认会做以下事情:
- 判断请求方法 :因为是
post,所以 jQuery 会将数据放在 请求体 中。 - 确定 Content-Type :由于没有显式指定
contentType,jQuery 默认使用application/x-www-form-urlencoded; charset=UTF-8。 - 编码数据 :将对象转换为
key1=value1&key2=value2格式的字符串(并对特殊字符进行 URL 编码),然后将这个字符串作为请求体发送。 - 设置请求头 :自动添加
Content-Type: application/x-www-form-urlencoded; charset=UTF-8。
最终发出的 HTTP 请求:

所以,这里的参数是放在请求体中的,而不是 URL 的查询字符串中。查询字符串是空的。
(三)、为什么有时候参数会在 URL 中?
如果你将 method 改为 "get",那么 jQuery 会默认将数据编码后附加到 URL 的查询字符串中,而不使用请求体。例如:
javascript
$.ajax({
method: "get",
url: "/user/login",
data: { username: "lisi", password: "admin" }
});
此时,jQuery 会构造 URL:/user/login?username=lisi&password=admin,然后发送 GET 请求,请求体为空。响应头中不会有 Content-Type(因为 GET 通常没有请求体)。
为什么 GET 要用查询字符串?
因为 HTTP 规范中,GET 请求的语义是"获取资源",不应该带有请求体(虽然技术上可以,但很多服务器不支持或忽略)。所以浏览器和库都约定将参数放在 URL 中。
(四)、后端如何获取参数?为什么不加注解也能拿到?
在我们的后端代码中:
java
@RequestMapping("/login")
public boolean login(String username, String password, HttpSession session) { ... }
参数 username 和 password 没有加任何注解,SpringMVC 却能正确获取到前端传来的值。这是为什么?
4.1 SpringMVC 的参数解析机制
当请求到达时,SpringMVC 会遍历所有注册的 参数解析器(HandlerMethodArgumentResolver) ,寻找能处理当前方法参数的解析器。对于没有注解的简单类型参数(如 String、Integer 等),Spring 会使用 ServletRequestMethodArgumentResolver (或者更精确地说,RequestParamMethodArgumentResolver 在处理没有注解时也会起作用,但逻辑是类似的)。它的工作逻辑是:
- 查看请求参数来源 :SpringMVC 会将 所有可用的请求参数 合并到一个 Map 中。这些请求参数来自两个地方:
- URL 查询字符串 (如
?username=xxx) - POST 请求的表单数据 (即
application/x-www-form-urlencoded格式的请求体)
- URL 查询字符串 (如
- 参数名匹配 :遍历方法参数名(如
username),在请求参数 Map 中查找同名的键,如果找到,就将对应的值赋值给该参数。
关键点:SpringMVC 并不关心参数是来自 URL 还是请求体,它统一当作"请求参数"处理。只要请求中包含了这些键值对,无论位置如何,都能被绑定。
4.2 为什么 JSON 格式的参数不行?
如果前端发送的是 JSON 格式(Content-Type: application/json),请求体内容是 {"username":"lisi","password":"admin"},那么 SpringMVC 默认的请求参数 Map 中不会包含这些键值对 ,因为 JSON 不是 application/x-www-form-urlencoded 格式,容器不会自动解析 JSON 为参数。此时,必须使用 @RequestBody 注解,触发消息转换器来解析 JSON。
总结:
- 表单格式 (
application/x-www-form-urlencoded)的请求体会被 Servlet 容器解析成参数,放入请求参数 Map,所以不加注解也能获取。 - JSON 格式 的请求体不会被自动解析成参数,必须用
@RequestBody。
(五)、HttpSession 到底做了什么?
5.1 会话的创建与识别
当你第一次调用 session.setAttribute() 时(前提是方法参数中声明了 HttpSession),SpringMVC 会从当前请求中获取会话对象。获取过程如下:
- 容器(Tomcat)检查请求中是否包含名为
JSESSIONID的 Cookie。 - 如果有,则根据这个 ID 查找对应的
HttpSession对象(存储在服务器内存中)。 - 如果没有,则创建一个新的
HttpSession对象,生成一个新的唯一 ID(如123456),并在响应中添加Set-Cookie: JSESSIONID=123456; Path=/; HttpOnly头。浏览器收到后会保存这个 Cookie,后续请求会自动带上。
5.2 数据的存储
session.setAttribute("username", "lisi") 将键值对保存在该会话对象中。这个会话对象与特定的 JSESSIONID 绑定,后续请求只要携带相同的 Cookie,就能通过 session.getAttribute("username") 取出数据。
5.3 在我们的案例中
- 登录成功时,调用
session.setAttribute("username", "lisi"),此时如果没有 JSESSIONID,则创建新会话,响应中返回 Set-Cookie。 - 浏览器跳转到
index.html时,会携带这个 Cookie(因为同域名下所有请求都会自动携带)。 - 主页的 AJAX 请求
/user/getLoginUser中,后端根据 Cookie 找到之前的 session,通过session.getAttribute("username")获取用户名并返回。
(六)、完整的通信过程(含报文细节)
具体的简图如下:

该图的解析如下:
6.1 登录请求
前端构造请求:
-
方法:POST
-
URL:
/user/login -
请求头(由浏览器和 jQuery 自动添加):
Host: localhost:8080 Connection: keep-alive Content-Length: 27 Content-Type: application/x-www-form-urlencoded; charset=UTF-8 Accept: */* Origin: http://localhost:8080 Referer: http://localhost:8080/login.html Cookie: 如果有之前的 Cookie(但首次登录可能没有) -
请求体:
username=lisi&password=admin

后端处理:
-
Tomcat 接收请求,解析请求头,将请求体按
application/x-www-form-urlencoded解析成参数 Map,放入HttpServletRequest的属性中。 -
SpringMVC 的
DispatcherServlet根据 URL 找到login方法。 -
参数解析器遍历,对于
String username,从请求参数 Map 中取键为username的值"lisi"并赋值;同样给password赋值"admin"。

-
方法执行,验证通过,
session.setAttribute("username", "lisi")。- 此时,容器检查到没有
JSESSIONIDCookie,于是创建新会话,生成 ID28194A95DE378C03D2CFB9A9FFDF7E4C,在后续响应中会自动添加Set-Cookie头(注意:SpringMVC 不直接操作响应,但容器会在响应时自动添加)。

- 此时,容器检查到没有
-
方法返回
true。 -
SpringMVC 的返回值处理器(因为类上有
@RestController)选择合适的消息转换器。由于项目有 Jackson,将true转为 JSON 字符串"true",并设置响应头Content-Type: application/json;charset=UTF-8。

-
Tomcat 组装响应报文:

前端接收:
- 浏览器收到响应,jQuery 根据
Content-Type: application/json将"true"解析为布尔值true。 success回调执行,if(result)为真,执行location.href="index.html",浏览器跳转到主页。
6.2 主页请求用户信息
浏览器加载 index.html,其中包含 AJAX 请求:
javascript
$.ajax({
method: "get",
url: "/user/getLoginUser",
success: function(result) { ... }
});
构造请求:
-
方法:GET
-
URL:
/user/getLoginUser -
请求头(浏览器自动添加):

后端处理 :

-
Tomcat 接收请求,根据 Cookie 中的
JSESSIONID=28194A95DE378C03D2CFB9A9FFDF7E4C找到对应的HttpSession对象。 -
SpringMVC 找到
getUserInfo方法,参数HttpSession session被自动注入(就是刚才找到的那个会话)。 -
方法内
session.getAttribute("username")返回"lisi"。 -
返回字符串
"lisi"。 -
SpringMVC 的返回值处理器选择
StringHttpMessageConverter,将字符串直接写入响应体,设置Content-Type: text/plain;charset=UTF-8。 -
响应报文:

前端接收:
- jQuery 根据
Content-Type: text/plain将响应体作为字符串,传给result。 $("#loginUser").text(result)将"lisi"显示在页面上。
9. 总结
本案例展示了表单格式的AJAX交互,以及session的使用。那么下一期我们将介绍如果我们交互的格式是JSON,那么我们该怎么做?
老铁们别忘了点赞👍、关注、加收藏,咱们下期见👋~
写在最后
最后或许我们网络通信大家还有一点迷糊,于是呢我去向AI查了一些相关的资料:彻底搞懂 HTTP 通信:从字节流到对象,前后端数据交换全解析