# JavaWeb2
# 1.Thymeleaf渲染流程
1)前端发起请求
2)servlet处理请求,准备渲染数据
// 1. 查询数据(比如水果列表、分页信息)
List<Fruit> fruitList = fruitDao.getFruitList(keyword,pageNo);
int pageCount = (fruitCount + 4) / 5;
// 2. 将数据存入作用域(Thymeleaf只能读取Request/Session/Application作用域的数据)
req.setAttribute("fruitList", fruitList); // Request作用域(推荐)
req.getSession().setAttribute("pageCount", pageCount); // Session作用域
2
3
4
5
6
7
3)调用Thymeleaf模板处理器
// ViewBaseServlet的核心逻辑(简化版)
protected void processTemplate(String templateName, HttpServletRequest req, HttpServletResponse resp) {
// 1. 获取Thymeleaf的核心引擎对象(TemplateEngine)
TemplateEngine templateEngine = getTemplateEngine(req.getServletContext());
// 2. 设置模板上下文(封装Request/Session数据)
WebContext context = new WebContext(req, resp, req.getServletContext());
// 3. 指定模板文件(比如"index"对应/templates/index.html)
String templatePath = "/" + templateName + ".html";
// 4. 核心:渲染模板 → 生成HTML字符串
String html = templateEngine.process(templatePath, context);
// 5. 将HTML写入响应,返回给浏览器
resp.getWriter().write(html);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
4)Thymeleaf 解析模板文件(语法处理)
Thymeleaf 会逐行解析模板文件中的特殊语法,替换为真实数据
5)生成最终 HTML 字符串(数据填充)
6)响应数据浏览器渲染
# 2.DispatchServlet 的三个演变阶段
1)原始阶段 —— 每个操作一个 Servlet(最早期)
// 新增水果的Servlet
@WebServlet("/addFruit.do")
public class AddFruitServlet extends ViewBaseServlet {
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) {
// 仅处理添加水果逻辑
}
}
// 编辑水果的Servlet
@WebServlet("/editFruit.do")
public class EditFruitServlet extends ViewBaseServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) {
// 仅处理编辑水果逻辑
}
}
// 删除水果的Servlet
@WebServlet("/delFruit.do")
public class DelFruitServlet extends ViewBaseServlet {
// ...
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
问题:Servlet 数量爆炸,每个 Servlet 只处理一个小操作,代码分散,维护困难。
2)基础调度 —— 单 Servlet 多操作
为了解决 Servlet 泛滥问题,演变出 一个业务模块一个 Servlet,通过operate参数区分操作,这是 DispatchServlet 的雏形:
@WebServlet("/fruit.do")
public class FruitServlet extends ViewBaseServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) {
// 1. 统一处理编码(通用逻辑)
req.setCharacterEncoding("UTF-8");
resp.setContentType("text/html;charset=UTF-8");
// 2. 获取操作类型,分发请求
String operate = req.getParameter("operate");
switch (operate) {
case "index": index(req, resp); break; // 列表
case "add": add(req, resp); break; // 新增
case "edit": edit(req, resp); break; // 编辑
// 其他操作...
}
}
// 各操作的具体逻辑
private void index(...) { ... }
private void add(...) { ... }
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
进步点:
- 一个业务模块一个 Servlet,减少 Servlet 数量;
- 统一处理通用逻辑(编码、异常、日志),避免重复代码;
- 通过
operate参数分发请求,职责更清晰。
仍存在的问题:
- 每个业务模块仍要写一个 Servlet(比如
UserServlet、OrderServlet),还是有冗余; - 操作逻辑(
index/add/edit)和 Servlet 耦合,不利于扩展; - URL 规则(
/fruit.do)需要手动维护,容易出错。
也可通过反射优化
Method[] declaredMethods = this.getClass().getDeclaredMethods();
for (Method method : declaredMethods) {
if(operate.equals(method.getName())){
try {
//找到和operate同名的方法,反射调用
method.invoke(this, req, resp);
return;
} catch (InvocationTargetException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
}
throw new RuntimeException("operate值非法");
2
3
4
5
6
7
8
9
10
11
12
13
14
15
3)成熟 DispatchServlet—— 全局统一调度(Spring MVC 的 DispatchServlet)
核心是一个 DispatchServlet 处理所有请求,通过「URL→控制器方法」的映射关系分发请求,也是 Spring MVC 的核心
流程
浏览器发送请求——》进入DispatchServlet——》解析url,映射handleMapping——》controller、service、mapper——》viewResolve解析模板路径——》view渲染——》返回html
@WebServlet("*.do")
public class DispatcherServlet extends ViewBaseServlet{
private Map<String,Object> beanMap = new HashMap<>();
public DispatcherServlet(){
}
public void init() throws ServletException {
super.init();//调用的是ViewBaseServlet中的init()方法
try {
InputStream inputStream = getClass().getClassLoader().getResourceAsStream("applicationContext.xml");
//1.创建DocumentBuilderFactory
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
//2.创建DocumentBuilder对象
DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder() ;
//3.创建Document对象
Document document = documentBuilder.parse(inputStream);
//4.获取所有的bean节点
NodeList beanNodeList = document.getElementsByTagName("bean");
for(int i = 0 ; i<beanNodeList.getLength() ; i++){
Node beanNode = beanNodeList.item(i);
if(beanNode.getNodeType() == Node.ELEMENT_NODE){
Element beanElement = (Element)beanNode ;
String beanId = beanElement.getAttribute("id");
String className = beanElement.getAttribute("class");
Class controllerBeanClass = Class.forName(className);
Object beanObj = controllerBeanClass.newInstance() ;
beanMap.put(beanId , beanObj) ;
}
}
} catch (ParserConfigurationException e) {
e.printStackTrace();
} catch (SAXException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
//设置编码
request.setCharacterEncoding("UTF-8");
//假设url是: http://localhost:8080/pro15/hello.do
//那么servletPath是: /hello.do
// 我的思路是:
// 第1步: /hello.do -> hello 或者 /fruit.do -> fruit
// 第2步: hello -> HelloController 或者 fruit -> FruitController
String servletPath = request.getServletPath();
servletPath = servletPath.substring(1);
int lastDotIndex = servletPath.lastIndexOf(".do") ;
servletPath = servletPath.substring(0,lastDotIndex);
Object controllerBeanObj = beanMap.get(servletPath);
String operate = request.getParameter("operate");
if(StringUtil.isEmpty(operate)){
operate = "index" ;
}
try {
Method[] methods = controllerBeanObj.getClass().getDeclaredMethods();
for(Method method : methods){
if(operate.equals(method.getName())){
//1.统一获取请求参数
//1-1.获取当前方法的参数,返回参数数组
Parameter[] parameters = method.getParameters();
//1-2.parameterValues 用来承载参数的值
Object[] parameterValues = new Object[parameters.length];
for (int i = 0; i < parameters.length; i++) {
Parameter parameter = parameters[i];
String parameterName = parameter.getName() ;
//如果参数名是request,response,session 那么就不是通过请求中获取参数的方式了
if("request".equals(parameterName)){
parameterValues[i] = request ;
}else if("response".equals(parameterName)){
parameterValues[i] = response ;
}else if("session".equals(parameterName)){
parameterValues[i] = request.getSession() ;
}else{
//从请求中获取参数值(除上述三种情况)
String parameterValue = request.getParameter(parameterName);
String typeName = parameter.getType().getName();
Object parameterObj = parameterValue ;
if(parameterObj!=null) {
if ("java.lang.Integer".equals(typeName)) {
parameterObj = Integer.parseInt(parameterValue);
}
}
parameterValues[i] = parameterObj ;
}
}
//2.controller组件中的方法调用
method.setAccessible(true);
Object returnObj = method.invoke(controllerBeanObj,parameterValues);
//3.视图处理
String methodReturnStr = (String)returnObj ;
if(methodReturnStr.startsWith("redirect:")){ //比如: redirect:fruit.do
String redirectStr = methodReturnStr.substring("redirect:".length());
response.sendRedirect(redirectStr);
}else{
super.processTemplate(methodReturnStr,request,response); // 比如: "edit"
}
}
}
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
优点:
通过反射统一处理了参数
不用每个模块都需要写一次反射代码
# 3.Service层的诞生
之前我们是 FruitController 直接调用 FruitDAO 的,当需要多个功能的时候要频繁的调用多个 DAO ; 现在我们要中间加一层服务层,FruitController 和 FruitService 通信,FruitService 调用 FruitDAO(实际公司中业务层体量很大)。
# 4.IOC 控制反转和DI 依赖注入的实现
1.之前在 Servlet 中,我们创建 service 对象 , FruitService fruitService = new FruitServiceImpl(); 2.这句话如果出现在 servlet 中的某个方法内部,那么这个 fruitService 的作用域(生命周期)应该就是这个方法级别;(这个方法结束,这个对象的生命周期就结束了) 3。如果这句话出现在 servlet 的类中,也就是说 fruitService 是一个成员变量,那么这个 fruitService 的作用域(生命周期)应该就是这个 servlet 实例级别 4。我们如果在 applicationContext.xml 中定义这个 fruitService,然后通过解析 XML,产生 fruitService 实例,存放在 beanMap 中,这个 beanMap 在一个 BeanFactory 中 5.因此,我们转移(改变)了之前的 service 实例、dao 实例等等他们的生命周期,控制权从程序员转移到 BeanFactory(IOC容器),这个现象我们称之为控制反转。 6.之前我们在控制层出现代码:FruitService fruitService = new FruitServiceImpl();那么,控制层和 service 层存在耦合。 7.之后,我们需要对各层之间进行解耦操作,将代码修改成 FruitService fruitService = null; 8.然后,在配置文件中配置:
<bean id="fruit" class="FruitController">
<property name="fruitService" ref="fruitService"/>
</bean>
2
3
9.使用反射技术将 FruitController 需要的 fruitService 注入进去,而不是之前主动去获取的方式,这就叫依赖注入。
配置文件:
<beans>
<bean id="fruitDAO" class="com.atguigu.fruit.dao.impl.FruitDAOImpl"/>
<bean id="fruitService" class="com.atguigu.fruit.service.impl.FruitServiceImpl">
<!-- property标签用来表示属性;name表示属性名;ref表示引用其他bean的id值-->
<property name="fruitDAO" ref="fruitDAO"/>
</bean>
<bean id="fruit" class="com.atguigu.fruit.controllers.FruitController">
<property name="fruitService" ref="fruitService"/>
</bean>
</beans>
2
3
4
5
6
7
8
9
10
BeanFactory 接口实现:
public interface BeanFactory {
Object getBean(String id);
}
2
3
实现BeanFactory接口:
public class ClassPathXmlApplicationContext implements BeanFactory {
private Map<String,Object> beanMap = new HashMap<>();
private String path = "applicationContext.xml" ;
public ClassPathXmlApplicationContext(){
this("applicationContext.xml");
}
public ClassPathXmlApplicationContext(String path){
if(StringUtil.isEmpty(path)){
throw new RuntimeException("IOC容器的配置文件没有指定...");
}
try {
InputStream inputStream = getClass().getClassLoader().getResourceAsStream(path);
//1.创建DocumentBuilderFactory
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
//2.创建DocumentBuilder对象
DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder() ;
//3.创建Document对象
Document document = documentBuilder.parse(inputStream);
//4.获取所有的bean节点把他们放进beanmap中
NodeList beanNodeList = document.getElementsByTagName("bean");
for(int i = 0 ; i<beanNodeList.getLength() ; i++){
Node beanNode = beanNodeList.item(i);
if(beanNode.getNodeType() == Node.ELEMENT_NODE){
Element beanElement = (Element)beanNode ;
String beanId = beanElement.getAttribute("id");
String className = beanElement.getAttribute("class");
Class beanClass = Class.forName(className);
//创建bean实例
Object beanObj = beanClass.newInstance() ;
//将bean实例对象保存到map容器中
beanMap.put(beanId , beanObj) ;
//到目前为止,此处需要注意的是,bean和bean之间的依赖关系还没有设置
}
}
//5.组装bean之间的依赖关系
for(int i = 0 ; i<beanNodeList.getLength() ; i++){
Node beanNode = beanNodeList.item(i);
if(beanNode.getNodeType() == Node.ELEMENT_NODE) {
Element beanElement = (Element) beanNode;
String beanId = beanElement.getAttribute("id");
NodeList beanChildNodeList = beanElement.getChildNodes();
for (int j = 0; j < beanChildNodeList.getLength() ; j++) {
Node beanChildNode = beanChildNodeList.item(j);
if(beanChildNode.getNodeType()==Node.ELEMENT_NODE && "property".equals(beanChildNode.getNodeName())){
Element propertyElement = (Element) beanChildNode;
String propertyName = propertyElement.getAttribute("name");
String propertyRef = propertyElement.getAttribute("ref");
//1) 找到propertyRef对应的实例
Object refObj = beanMap.get(propertyRef);
//2) 将refObj设置到当前bean对应的实例的property属性上去
Object beanObj = beanMap.get(beanId);
//通过反射设置对象的 propertyField 属性将它需要的其他类注入进去
Class beanClazz = beanObj.getClass();
Field propertyField = beanClazz.getDeclaredField(propertyName);
propertyField.setAccessible(true);
propertyField.set(beanObj,refObj);
}
}
}
}
} catch (ParserConfigurationException e) {
e.printStackTrace();
} catch (SAXException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
}
}
@Override
public Object getBean(String id) {
return beanMap.get(id);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
解释:
1.创建一个 BeanFactory,该实现类负责获取所有 bean 节点,组装 bean 之间的依赖关系。 2.将之前 DispatcherServlet 组件中初始化方法部分拿出来作为一个创建 BeanFactory 的代码,在DispatcherServlet 组件中只需要获取这个 beanfactory 就可以了, beanFactory = new ClassPathXmlApplicationContext();
# 5.过滤器
1)Filter也属于Servlet规范
2)Filter开发步骤:新建实现Filter接口,实现三个方法:init、doFilter、destory
配置Filter,也可用注解@WebFilter,也可使用xml文件<filter><filter-mapping>
3)配置时和servlet一样,也可配置通配符,例如@WebFilter("*.do")
4)过滤器链
如果采用注解方法,拦截顺序是按照全类名先后顺序排列
如果是xml方法配置,按照配置的先后顺序排列
示例:默认情况,只有读到initParams中的参数,才会给encoding赋值,否则默认utf-8
package com.demo.filter;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.annotation.WebInitParam;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
@WebFilter(urlPatterns = {"*.do"},initParams = {@WebInitParam(name = "encoding",value = "utf-8")})
public class GlobalFilter implements Filter {
private String encoding="utf-8";
@Override
public void init(FilterConfig filterConfig) throws ServletException {
String encodingStr = filterConfig.getInitParameter("encoding");
if (encodingStr != null){
encoding = encodingStr;
}
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
((HttpServletRequest)servletRequest).setCharacterEncoding(encoding);
filterChain.doFilter(servletRequest,servletResponse);
}
@Override
public void destroy() {
Filter.super.destroy();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 6.ThreadLocal
# 1.核心定义
ThreadLocal 是 Java 提供的一个线程本地存储工具类,它的核心作用是:为每个使用它的线程创建一个独立的变量副本。
# 2. 通俗比喻
- 没有 ThreadLocal 时:多个线程共用一个 “全局变量”,线程 A 修改后,线程 B 看到的就是修改后的值,会互相干扰;
- 有 ThreadLocal 时:相当于给每个线程发了一个 “专属储物柜”,线程 A 往自己的柜子里放东西,线程 B 往自己的柜子里放东西,彼此看不到、不干扰,取出的永远是自己放进去的那个值。
# 3.源码解析
set方法
// ThreadLocal的set方法
public void set(T value) {
// 1. 获取当前执行set方法的线程对象
Thread t = Thread.currentThread();
// 2. 获取当前线程t内部的ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
if (map != null)
// 3. 如果map存在,以当前ThreadLocal实例(this)为key,存入value
map.set(this, value);
else
// 4. 如果map不存在,为当前线程t创建ThreadLocalMap,并存入第一个键值对
createMap(t, value);
}
2
3
4
5
6
7
8
9
10
11
12
13
get方法
public T get() {
// 1. 还是先拿当前线程
Thread t = Thread.currentThread();
// 2. 还是拿当前线程的ThreadLocalMap(和set时的map是同一个)
ThreadLocalMap map = getMap(t);
if (map != null) {
// 3. 以当前ThreadLocal实例(this)为key,从map中取值
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// 4. 没取到就返回初始值
return setInitialValue();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 7.监听器
| 监听大类 | 监听器接口 | 核心作用 | 核心方法 | 关键特点 / 常用场景 |
|---|---|---|---|---|
| ServletContext(全局上下文) | ServletContextListener | 监听 ServletContext 的创建 / 销毁(应用启停) | 1. contextInitialized ():应用启动执行 2. contextDestroyed ():应用关闭执行 | 全局唯一,应用级别;用于初始化连接池、加载配置、释放全局资源 |
| ServletContext(全局上下文) | ServletContextAttributeListener | 监听全局属性的添加 / 删除 / 替换 | 1. attributeAdded ():添加属性 2. attributeRemoved ():删除属性 3. attributeReplaced ():替换属性 | 监控全局缓存 / 参数变更;所有用户共享 |
| 👤 HttpSession(用户会话) | HttpSessionListener | 监听 Session 的创建 / 销毁(用户上线 / 下线) | 1. sessionCreated ():Session 创建(首次访问) 2. sessionDestroyed ():Session 销毁(超时 / 手动失效) | 统计在线用户数、记录用户登录 / 退出时间;每个用户一个 Session |
| HttpSession(用户会话) | HttpSessionAttributeListener | 监听 Session 属性的添加 / 删除 / 替换 | 1. attributeAdded ():添加属性 2. attributeRemoved ():删除属性 3. attributeReplaced ():替换属性 | 监控用户会话数据(如登录状态、购物车)变更 |
| HttpSession(用户会话) | HttpSessionBindingListener | 监听对象与 Session 的绑定 / 解绑 | 1. valueBound ():对象绑定到 Session 2. valueUnbound ():对象从 Session 解绑 | 无需配置(由被绑定对象实现);对象存入 / 移除 Session 时触发,用于对象初始化 / 清理 |
| HttpSession(用户会话) | HttpSessionActivationListener | 监听 Session 的钝化 / 活化(序列化 / 反序列化) | 1. sessionWillPassivate ():Session 钝化前 2. sessionDidActivate ():Session 活化后 | 需实现 Serializable 接口;处理服务器内存不足时 Session 的持久化 / 恢复 |
| ServletRequest(单次请求) | ServletRequestListener | 监听 Request 的创建 / 销毁(请求开始 / 结束) | 1. requestInitialized ():请求创建 2. requestDestroyed ():请求销毁 | 记录请求处理时长、统计接口访问量、统一设置请求编码;每次请求一个 Request |
| ServletRequest(单次请求) | ServletRequestAttributeListener | 监听 Request 属性的添加 / 删除 / 替换 | 1. attributeAdded ():添加属性 2. attributeRemoved ():删除属性 3. attributeReplaced ():替换属性 | 监控请求级数据传递(如 Controller 向 JSP 传参);请求结束后属性销毁 |
# 8.cookie\session\token
1.cookie
Http默认无状态的,分不清是哪个用户来进行操作,所以就有了cookie
1)介绍
cookie 是客户端会话跟踪技术,它是存储在客户端浏览器的,我们使用 cookie 来跟踪会话,我们就可以在浏览器第一次发起请求来请求服务器的时候,我们在服务器端来设置一个cookie。
服务器端在给客户端在响应数据的时候,会自动的将 cookie 响应给浏览器,浏览器接收到响应回来的 cookie 之后,会自动的将 cookie 的值存储在浏览器本地。接下来在后续的每一次请求当中,都会将浏览器本地所存储的 cookie 自动地携带到服务端。
2)流程
服务器会 自动 的将 cookie 响应给浏览器。
浏览器接收到响应回来的数据之后,会 自动 的将 cookie 存储在浏览器本地。
在后续的请求当中,浏览器会 自动 的将 cookie 携带到服务器端。
3)大小:单个 Cookie≤4KB,一个域名下最多存 20 个左右;
4)优缺点
优点:HTTP协议中支持的技术(像Set-Cookie 响应头的解析以及 Cookie 请求头数据的携带,都是浏览器自动进行的,是无需我们手动操作的)
缺点:
移动端APP(Android、IOS)中无法使用Cookie
不安全,用户可以自己禁用Cookie
Cookie不能跨域
2.session
但是cookie存储的数据又太小,又是明文传输,太不安全了,于是就有了session
1)流程
- 获取session
浏览器在第一次请求服务器的时候,我们就可以直接在服务器当中来获取到会话对象Session。如果是第一次请求,会话对象是不存在的,这个时候服务器会自动的创建一个会话对象Session,Session 并分配一个SessionID。
- 响应Cookie (JSESSIONID)
接下来,服务器端在给浏览器响应数据的时候,它会将 SessionID 通过 Cookie 响应给浏览器。其实在响应头当中增加了一个 Set-Cookie 响应头。这个 Set-Cookie 响应头对应的值是不是cookie? cookie 的名字是固定的 JSESSIONID 代表的服务器端会话对象 SessionID。浏览器会自动识别这个响应头,然后自动将Cookie存储在浏览器本地。

- 查找Session
接下来,在后续的每一次请求当中,都会将 Cookie 的数据获取出来,并且携带到服务端。接下来服务器拿到JSESSIONID这个 Cookie 的值,也就是 Session 的ID。拿到 ID 之后,就会从众多的 Session 当中来找到当前请求对应的会话对象Session。
2) 核心特点
- 生命周期:默认 30 分钟超时(可配置),浏览器关闭后 Cookie 失效,但 Session 仍在服务器保留至超时;
- 安全性:比 Cookie 高(信息存在服务器),但依赖 Cookie 传递 SessionID,若 SessionID 被窃取(如 CSRF 攻击),仍有风险;
- 核心问题:分布式系统中,Session 存在服务器本地,会导致 “会话共享” 问题(用户请求到不同服务器,找不到 Session)。
3)优缺点
优点:Session是存储在服务端的,安全
缺点:
服务器集群环境下无法直接使用Session
移动端APP(Android、IOS)中无法使用Cookie
用户可以自己禁用Cookie
Cookie不能跨域
3.token
由于session不好很好的处理跨域和集群问题,所以token出现了
1)流程
用户登录,服务器分发token
客户端保存token
再次请求时,客户端携带token访问
服务器解析token,有效未过期就放行
2)核心特点
- 存储位置:客户端(LocalStorage/Cookie/ 内存),服务器不存储 Token(JWT);
- 无状态:服务器无需保存 Token,仅通过加密算法验证有效性,适合分布式系统;
- 安全性:可签名 / 加密(如 JWT 的 HS256/RSA),防篡改;支持跨域(CORS),适合前后端分离 / 移动端;
- 核心问题:Token 一旦签发,无法主动作废(除非服务器维护黑名单例如redis);Token 过长会增加请求开销。
3)优缺点
优点:
支持PC端、移动端
解决集群环境下的认证问题
减轻服务器的存储压力(无需在服务器端存储)
缺点:需要自己实现(包括令牌的生成、令牌的传递、令牌的校验)
对比表
| 维度 | Cookie | Session | Token(如 JWT) |
|---|---|---|---|
| 存储位置 | 客户端(浏览器) | 服务器端 | 客户端(任意) |
| 状态性 | 无状态(仅存数据) | 有状态(服务器存会话) | 无状态(服务器不存) |
| 安全性 | 低(明文,易窃取) | 中(服务器存储,依赖 Cookie) | 高(加密签名,防篡改) |
| 大小限制 | ≤4KB | 无(服务器内存决定) | 无(但过长影响性能) |
| 跨域支持 | 差(同源策略限制) | 差(依赖 Cookie) | 好(请求头携带) |
| 分布式支持 | 无(仅客户端) | 差(需会话共享) | 好(无状态) |
| 核心用途 | 传递 SessionID、存非敏感信息 | 存储用户敏感会话信息 | 前后端分离 / 跨域 / 分布式认证 |