← 返回 Java核心图解 目录

📡 网络与安全图解

6大核心主题 · 10个SVG图解 · 面试+生产双杀

① TCP三次握手与四次挥手

三次握手 = 打电话——"喂?听得到吗?""听到了!""好的我们开始聊!"
四次挥手 = 礼貌挂电话——"我说完了""好的我还有一句""我也说完了""好的再见"

三次握手时序图

TCP 三次握手 Client Server SYN, seq=x SYN_SENT LISTEN SYN+ACK, seq=y, ack=x+1 SYN_RCVD ACK, ack=y+1 ESTABLISHED ESTABLISHED 为什么3次不是2次? 防止历史连接请求导致服务器误建立连接——若只有2次,Server无法确认Client收到了SYN+ACK

四次挥手时序图

TCP 四次挥手 Client(主动关闭) Server(被动关闭) FIN, seq=u FIN_WAIT_1 "我说完了,不再发数据" ACK, ack=u+1 CLOSE_WAIT FIN_WAIT_2 "好的我知道了,但我还有数据" ⏳ 半关闭状态:Client不再发数据,但Server可能还在发 FIN, seq=w LAST_ACK "我也说完了" ACK, ack=w+1 TIME_WAIT CLOSED "好的再见!" 等待2MSL CLOSED 为什么4次不是3次? Server的FIN和ACK不能合并(可能还有数据要发);TIME_WAIT等2MSL确保最后一个ACK到达Server

TCP状态转换速记

TCP 状态转换速记 ─── 主动端(Client) ─── CLOSED SYN SYN_SENT SYN+ACK ESTABLISHED FIN FIN_WAIT_1 ACK FIN_WAIT_2 FIN TIME_WAIT 2MSL CLOSED ─── 被动端(Server) ─── LISTEN SYN SYN_RCVD ACK ESTABLISHED FIN CLOSE_WAIT FIN LAST_ACK 面试高频考点: CLOSE_WAIT过多?→ 说明Server没有正确关闭连接(代码bug,忘了close());TIME_WAIT过多?→ 短连接频繁创建销毁,可用连接池或SO_REUSEADDR缓解 大量TIME_WAIT会占用端口资源(客户端端口耗尽),但不会导致文件描述符耗尽(连接已关闭)
TcpHandshakeDemo.java
📂 src/main/java
TcpHandshakeDemo.java
// Java Socket 体验三次握手与四次挥手 import java.net.*; import java.io.*; public class TcpHandshakeDemo { public static void main(String[] args) throws Exception { // ===== Server端 ===== ServerSocket server = new ServerSocket(8080); // server.accept() 内部完成:LISTEN → SYN_RCVD → ESTABLISHED Socket client = server.accept(); // ===== Client端 ===== // new Socket() 内部完成:CLOSED → SYN_SENT → ESTABLISHED Socket conn = new Socket("localhost", 8080); // ===== 数据传输 ===== BufferedReader in = new BufferedReader( new InputStreamReader(conn.getInputStream())); PrintWriter out = new PrintWriter(conn.getOutputStream(), true); out.println("Hello from Client"); String resp = in.readLine(); // ===== 四次挥手 ===== conn.close(); // Client发FIN → FIN_WAIT_1 → FIN_WAIT_2 → TIME_WAIT client.close(); // Server发FIN → CLOSE_WAIT → LAST_ACK → CLOSED // ===== 优化:设置SO_REUSEADDR,避免TIME_WAIT阻塞端口 ===== // server.setReuseAddress(true); } } // 用 netstat 观察TCP状态 // $ netstat -an | grep 8080 //tcp 0 0 127.0.0.1:8080 127.0.0.1:xxxx TIME_WAIT
三次握手核心是"确认双方的发送和接收能力都正常";四次挥手核心是"双方都需要单独关闭自己的发送方向"。TIME_WAIT存在的原因:(1) 确保最后一个ACK到达 (2) 让旧连接的延迟报文消失。
状态所在端含义面试常见问题
SYN_SENTClient已发SYN,等待SYN+ACK超时重传SYN(默认5次,间隔1s/2s/4s/8s/16s)
SYN_RCVDServer已发SYN+ACK,等待ACKSYN Flood攻击就是利用此状态耗尽半连接队列
ESTABLISHED双方连接建立,可传输数据三次握手完成后的稳定状态
FIN_WAIT_1Client已发FIN,等待ACK如果对面宕机,会一直等待(超时后关闭)
FIN_WAIT_2Client收到ACK,等待对方FIN如果Server不close(),Client会一直卡在此状态
CLOSE_WAITServer收到FIN,已回ACK,等应用层close()大量CLOSE_WAIT = 代码bug(忘了close())
LAST_ACKServer已发FIN,等待最后ACK如果ACK丢失,会重发FIN
TIME_WAITClient等待2MSL后关闭大量TIME_WAIT = 短连接过多,用连接池缓解

② HTTPS握手过程

加密信封——先打电话交换密码本(非对称加密),然后用密码本加密信件(对称加密)。非对称加密安全但慢,对称加密快但需要先安全交换密钥。HTTPS的"混合加密"方案两者结合。
HTTPS 握手过程(TLS 1.2) Client(浏览器) Server(网站) 非对称加密阶段 ClientHello: 加密套件列表 + 随机数R1 ServerHello: 选定加密套件 + 随机数R2 + 证书 验证证书合法性 生成预主密钥R3 用证书公钥加密R3,发送给Server 私钥解密得到R3 R1+R2+R3 → 会话密钥 R1+R2+R3 → 会话密钥 对称加密 AES对称加密传输数据 AES对称加密传输数据
HttpsConfig.java
📂 src/main/java
HttpsConfig.java
// Spring Boot 配置 HTTPS @Bean public ServletWebServerFactory servletContainer() { TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory(); tomcat.addAdditionalTomcatConnectors(createSslConnector()); return tomcat; } private Connector createSslConnector() { Connector connector = new Connector("org.apache.coyote.http11.Http11NioProtocol"); Http11NioProtocol proto = (Http11NioProtocol) connector.getProtocolHandler(); proto.setSSLEnabled(true); proto.setKeystoreFile("classpath:keystore.p12"); proto.setKeystorePass("changeit"); proto.setKeyAlias("tomcat"); return connector; }
非对称加密(RSA/ECC)只在握手阶段使用,用于安全交换密钥;后续数据传输全部使用对称加密(AES),因为AES比RSA快几百倍!

对称加密 vs 非对称加密

对比维度对称加密 (AES)非对称加密 (RSA/ECC)
密钥数量1个(加密解密同一把钥匙)2个(公钥加密,私钥解密)
速度极快(硬件加速)慢(几百~上千倍差距)
用途大量数据加密密钥交换、数字签名
HTTPS中的角色加密实际数据传输握手阶段交换会话密钥
密钥分发困难(如何安全传密钥?)简单(公钥可公开)
CertificateVerify.java
📂 src/main/java
CertificateVerify.java
// Java 验证 HTTPS 证书链 import javax.net.ssl.*; import java.security.cert.*; public class CertificateVerify { public static void main(String[] args) throws Exception { URL url = new URL("https://www.baidu.com"); HttpsURLConnection conn = (HttpsURLConnection) url.openConnection(); conn.connect(); Certificate[] certs = conn.getServerCertificates(); for (Certificate cert : certs) { X509Certificate x509 = (X509Certificate) cert; System.out.println("颁发者: " + x509.getIssuerX500Principal()); System.out.println("使用者: " + x509.getSubjectX500Principal()); System.out.println("有效期: " + x509.getNotBefore() + " ~ " + x509.getNotAfter()); } conn.disconnect(); } } // 用 keytool 生成自签名证书(开发用) // keytool -genkeypair -alias myapp -keyalg RSA -keysize 2048 \ // -validity 365 -keystore keystore.p12 -storetype PKCS12

③ HTTP/1.1 vs HTTP/2 vs HTTP/3

HTTP/1.1 = 单车道(排队,一辆车走完下一辆才能走);HTTP/2 = 多车道高速(并行,多条车道同时跑);HTTP/3 = 直升机(不怕堵车,因为基于UDP/QUIC,每条流独立)。
HTTP/1.1 vs HTTP/2 vs HTTP/3 请求-响应对比 HTTP/1.1 单车道 · 队头阻塞 请求1 → 响应1 请求2 → 响应2 请求3 → 响应3 队头阻塞! 响应1慢 → 全部等待 每个请求需独立TCP连接 或管线化(支持差) ⏱ 效率:低 HTTP/2 多车道 · 多路复用 流1: 请求→响应 流2: 请求→响应 流3: 请求→响应 ↕ 同时在一条TCP连接上 TCP层仍有队头阻塞 流ID区分不同请求 HPACK头部压缩 ⏱ 效率:中高 HTTP/3 直升机 · QUIC(UDP) 流1: 独立UDP流 流2: 独立UDP流 流3: 独立UDP流 无队头阻塞! 每个流完全独立 0-RTT连接建立 内置TLS 1.3加密 ⏱ 效率:最高
对比维度HTTP/1.1HTTP/2HTTP/3
传输层TCPTCPQUIC (基于UDP)
多路复用❌ 不支持✅ 同一TCP连接并行流✅ 独立QUIC流
队头阻塞❌ HTTP层阻塞⚠️ TCP层仍阻塞✅ 完全消除
头部压缩❌ 无✅ HPACK✅ QPACK
连接建立TCP 1-RTT + TLS 1-2RTTTCP 1-RTT + TLS 1-RTTQUIC 0-1 RTT (含TLS)
HTTP/1.1的问题是"队头阻塞+连接开销大";HTTP/2用多路复用解决了HTTP层队头阻塞,但TCP层仍有;HTTP/3用QUIC(UDP)彻底消除队头阻塞,且连接建立更快。
application.properties
📂 src/main/resources
application.properties
# Spring Boot 启用 HTTP/2 server.http2.enabled=true server.ssl.enabled=true # HTTP/2浏览器要求必须HTTPS # 用 curl 测试 HTTP 版本 # curl -I --http2 https://your-server.com # 输出: HTTP/2 200 # QUIC/HTTP3 目前Java生态支持有限 # 可关注 Netty 的 quic 模块或 Jetty 的 HTTP/3 实现

④ JWT认证流程

电影票——买票时查身份证(登录验证)→ 给你一张票(JWT)→ 以后凭票进场(每次请求带JWT),检票员不需要打电话核实你的身份。

JWT认证时序图

JWT 认证时序图 用户 Server POST /login {username, password} 验证身份 → 生成JWT Access Token + Refresh Token GET /api/xxx Authorization: Bearer <token> 验证JWT签名 → 返回数据 200 OK + 数据 Access Token过期 → 用Refresh Token换新 新的Access Token(Refresh Token轮换)

JWT结构分解图

JWT = Header.Payload.Signature Header (Base64Url编码) { "alg": "HS256", "typ": "JWT" } . Payload (Base64Url编码) { "sub": "1234567890", "name": "John", "iat": 1516239022 } . Signature (HMAC-SHA256) HMACSHA256( base64(header)+ base64(payload), secret) 算法 + 类型 告诉服务器怎么验证 用户数据 + 标准声明 ⚠️ 不要放敏感信息(可解码) 防篡改签名 任何改动 → 签名校验失败 关键安全提醒 JWT不是加密!Payload是Base64编码,任何人都能解码。签名只保证完整性,不保证机密性。
JwtUtil.java
📂 src/main/java
📂 util
JwtUtil.java
// Spring Boot + jjwt 实现 JWT 认证 import io.jsonwebtoken.*; public class JwtUtil { private static final String SECRET = "my-secret-key-at-least-256-bits"; private static final long ACCESS_EXP = 1000 * 60 * 30; // 30分钟 private static final long REFRESH_EXP = 1000 * 60 * 60 * 24 * 7; // 7天 // 生成 Access Token public static String generateAccessToken(String userId) { return Jwts.builder() .setSubject(userId) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + ACCESS_EXP)) .signWith(SignatureAlgorithm.HS256, SECRET) .compact(); } // 验证 Token public static Claims parseToken(String token) { return Jwts.parser() .setSigningKey(SECRET) .parseClaimsJws(token) .getBody(); } }
JWT的Payload是Base64编码(不是加密!),不要存放密码等敏感信息。Access Token有效期要短(15-30分钟),Refresh Token有效期长(7天),且Refresh Token应只在后端使用,不暴露给前端JS。

JWT vs Session 对比

对比维度SessionJWT
存储位置服务端(内存/Redis)客户端(localStorage/Cookie)
状态有状态无状态
扩展性需共享Session存储天然支持分布式
注销直接删除Session只能等过期(或加黑名单)
安全性SessionID泄露风险Payload可被解码
性能每次查存储只验证签名,无IO
体积SessionID很小Token较大(1-2KB)
适用场景单体应用/需要即时注销微服务/分布式/跨域
JwtAuthFilter.java
📂 src/main/java
📂 filter
JwtAuthFilter.java
// Spring Security JWT 过滤器 @Component public class JwtAuthFilter extends OncePerRequestFilter { @Autowired private JwtUtil jwtUtil; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { String header = request.getHeader("Authorization"); if (header != null && header.startsWith("Bearer ")) { String token = header.substring(7); try { Claims claims = jwtUtil.parseToken(token); UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken( claims.getSubject(), null, Collections.emptyList()); SecurityContextHolder.getContext().setAuthentication(auth); } catch (ExpiredJwtException e) { response.setStatus(401); response.getWriter().write("{\"error\":\"Token expired\"}"); return; } catch (JwtException e) { response.setStatus(401); response.getWriter().write("{\"error\":\"Invalid token\"}"); return; } } chain.doFilter(request, response); } }

⑤ OAuth2授权码流程

核心机制——酒店房卡:你不想把主密码(账号密码)直接给第三方应用,而是给一把有时效的临时房卡(授权码→Token)。第三方应用用这把房卡只能访问授权范围内的资源,拿不到你的主密码。
OAuth2 授权码流程 用户 Client(应用) Auth Server Resource Srv 1.点击"微信登录" 2.重定向 → Auth Server 3.用户登录并授权(浏览器) 4.重定向回Client + 授权码code 5.code + client_secret → Token (后端通信,不经过浏览器!) 6.access_token (+ refresh) 7.用token请求用户信息 8.返回用户信息 为什么需要授权码?Token不经过浏览器,通过后端用client_secret交换,防止Token泄露
OAuth2CallbackController.java
📂 src/main/java
📂 controller
OAuth2Callback.java
// OAuth2 用授权码换 Token 的核心代码 @RestController public class OAuth2CallbackController { @Value("${oauth2.client-id}") private String clientId; @Value("${oauth2.client-secret}") private String clientSecret; @Value("${oauth2.token-uri}") private String tokenUri; // 回调接口:接收授权码,换取Token @GetMapping("/oauth2/callback") public ResponseEntity<?> callback(@RequestParam String code) { // 关键:用 code + client_secret 换 token(后端通信!) RestTemplate restTemplate = new RestTemplate(); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); headers.setBasicAuth(clientId, clientSecret); MultiValueMap<String, String> body = new LinkedMultiValueMap<>(); body.add("grant_type", "authorization_code"); body.add("code", code); body.add("redirect_uri", "https://your-app.com/oauth2/callback"); ResponseEntity<Map> resp = restTemplate.exchange( tokenUri, HttpMethod.POST, new HttpEntity<>(body, headers), Map.class); Map tokenResp = resp.getBody(); String accessToken = (String) tokenResp.get("access_token"); return ResponseEntity.ok(tokenResp); } }
授权码流程的关键是"两步走"——先给授权码(经过浏览器,但code没用),再用code+client_secret换token(后端直接通信,安全)。这样token永远不会出现在浏览器URL或前端代码中。

OAuth2四种授权模式对比

授权模式安全性适用场景Token经过浏览器?
授权码(Authorization Code)✅ 最安全有后端的Web应用❌ 不经过
授权码+PKCE✅ 安全SPA/移动端(无client_secret)❌ 不经过
客户端凭证(Client Credentials)✅ 安全服务间调用(M2M)❌ 不经过
隐式(Implicit)⚠️ 不安全已废弃(OAuth2.1移除)⚠️ Token直接返回浏览器
密码(Password)⚠️ 不安全已废弃(需传密码)⚠️ 需传用户密码

⑥ CORS跨域原理

访客登记——你(浏览器)从A公司(域名A)去B公司(域名B)办事,B公司要求你先在前台登记(预检请求),确认你是被允许的访客后才能进入。
CORS 跨域请求流程 简单请求 GET/POST + 标准头 + text/plain等 浏览器 Server GET /api/data (Origin: a.com) Response: Access-Control-Allow-Origin: a.com ✅ 浏览器检查Origin → 匹配 → 放行给JS 预检请求 PUT/DELETE / 自定义Header / Content-Type: application/json 等 浏览器 Server ① OPTIONS /api/data (预检 Preflight) ② Allow-Origin/Methods/Headers → 允许 ✅ ③ PUT /api/data (实际请求) ④ 返回数据 ✅
CorsConfig.java
📂 src/main/java
📂 config
CorsConfig.java
// Spring Boot CORS 全局配置 @Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/api/**") .allowedOrigins("https://front.example.com") // 不要用 *! .allowedMethods("GET", "POST", "PUT", "DELETE") .allowedHeaders("Authorization", "Content-Type") .allowCredentials(true) // 允许Cookie .maxAge(3600); // 预检缓存1小时 } } // 或者用 @CrossOrigin 注解(方法级别) @CrossOrigin(origins = "https://front.example.com") @GetMapping("/api/data") public ResponseEntity<?> getData() { ... }
CORS是浏览器行为,不是服务器行为!服务器不拒绝跨域请求,是浏览器发现响应头没有Access-Control-Allow-Origin才拦截的。Postman等工具不受CORS限制,这也是为什么Postman能通但浏览器报错的原因。

CORS关键响应头

Access-Control-Allow-Origin: https://front.example.com # 允许的源(不要用*配合credentials) Access-Control-Allow-Methods: GET, POST, PUT, DELETE # 允许的HTTP方法 Access-Control-Allow-Headers: Authorization, Content-Type # 允许的请求头 Access-Control-Allow-Credentials: true # 允许携带Cookie Access-Control-Max-Age: 3600 # 预检请求缓存时间(秒) Access-Control-Expose-Headers: X-Custom-Header # 允许JS读取的响应头
SecurityConfig.java
📂 src/main/java
📂 config
SecurityConfig.java
// Spring Security CORS 过滤器(推荐方式) @Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http.cors(cors -> cors.configurationSource(corsConfigSource())) .csrf(csrf -> csrf.disable()) .authorizeHttpRequests(auth -> auth .anyRequest().authenticated()); return http.build(); } @Bean public CorsConfigurationSource corsConfigSource() { CorsConfiguration config = new CorsConfiguration(); config.setAllowedOrigins(List.of("https://front.example.com")); config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE")); config.setAllowedHeaders(List.of("Authorization", "Content-Type")); config.setAllowCredentials(true); config.setMaxAge(3600L); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/api/**", config); return source; } } // ⚠️ 常见坑:allowCredentials(true)时不能用allowedOrigins("*") // 必须指定具体域名!否则浏览器会报错。
调试CORS的3个技巧:(1) 打开浏览器DevTools → Network面板,看OPTIONS请求和响应头 (2) 响应头缺少Access-Control-Allow-Origin就是CORS配置问题 (3) Spring Security的CORS配置要放在csrf之前,否则可能被拦截。

🎯 面试速答 · 6个高频题

为什么TCP三次握手不能是两次?

核心原因:防止历史连接请求导致服务器误建立连接。

假设Client发了一个SYN请求,但因为网络延迟很久才到达Server。Client以为丢了又发了一个新的SYN。如果只有两次握手,Server收到旧的SYN就会直接建立连接,浪费资源。三次握手中,Server发SYN+ACK后需要等Client确认,Client发现是旧请求就会发RST拒绝,避免建立无效连接。

TIME_WAIT为什么等2MSL?

两个原因:

1. 确保最后一个ACK到达对方:如果Client发的最后一个ACK丢了,Server会重发FIN。Client在TIME_WAIT状态能收到这个重发的FIN并重新发ACK。2MSL = 一个MSL用于ACK到达Server + 一个MSL用于可能的FIN重传到达Client。

2. 让旧连接的延迟报文消失:MSL是报文最大生存时间,等2MSL后旧连接的所有报文都已过期,不会影响新连接。

HTTPS为什么用非对称加密交换密钥,而不用它加密数据?

性能原因:非对称加密(RSA/ECC)比对称加密(AES)慢几百到上千倍。

所以HTTPS的设计思路是:用非对称加密安全地交换一个对称密钥(会话密钥),然后用这个对称密钥加密后续所有数据。既保证了密钥交换的安全性,又保证了数据传输的效率。这就是"混合加密"方案。

JWT和Session的区别?各有什么优缺点?

Session:服务端存储用户状态,通过SessionID关联。优点是服务端可随时注销;缺点是不易扩展(需共享Session存储)。

JWT:无状态,Token自带用户信息。优点是天然支持分布式、不需要服务端存储;缺点是无法主动让Token失效(只能等过期)、Payload可被解码(不能放敏感信息)、Token体积比SessionID大。

选择建议:微服务/分布式架构选JWT;单体应用或需要即时注销选Session。也可以混合使用:短期Access Token + 黑名单机制。

OAuth2授权码模式为什么比隐式模式更安全?

核心区别:Token是否经过浏览器。

授权码模式:浏览器只拿到授权码(code),code通过后端用client_secret换取Token。Token永远不会出现在浏览器URL或前端代码中,即使授权码泄露也无法获取Token。

隐式模式(已废弃):Token直接返回给浏览器,存在Token泄露风险(浏览器历史记录、Referer头等)。OAuth2.1已移除隐式模式。

CORS预检请求是什么?什么时候触发?

预检请求(Preflight)是浏览器在发送跨域"非简单请求"之前,自动发送的一个OPTIONS请求,用于询问服务器是否允许实际请求。

触发条件(满足任一即触发):

  • 请求方法不是GET/POST/HEAD
  • Content-Type不是application/x-www-form-urlencoded、multipart/form-data、text/plain
  • 设置了自定义Header(如Authorization)

注意:CORS是浏览器行为,服务端之间直接调用不受CORS限制。

📋 一页速查 · 网络安全核心概念

📡
TCP三次握手
SYN → SYN+ACK → ACK
确认双方收发能力正常
👋
TCP四次挥手
FIN → ACK → FIN → ACK
双方独立关闭发送方向
🔒
HTTPS握手
非对称交换密钥 → 对称加密数据
混合加密=安全+效率
🚀
HTTP版本演进
1.1队头阻塞 → 2多路复用 → 3无阻塞
从TCP到QUIC(UDP)
🎫
JWT认证
Header.Payload.Signature
无状态令牌,注意别放敏感数据
🔑
OAuth2授权码
code经浏览器 → token经后端
Token不暴露给前端
🌐
CORS跨域
简单请求直接发 → 复杂请求先预检
是浏览器行为不是服务器行为
🎯
面试核心
3次vs2次 · 4次vs3次 · 混合加密
JWT vs Session · 授权码安全性

04 - 网络与安全图解 · Java核心图解系列

从"背八股"到"看懂画面"

核心三原则速记

网络安全三原则:① HTTPS 是底线,TLS 1.2+ 证书必须覆盖全域名 → ② JWT 无状态但无法主动失效,敏感操作配合 Redis 黑名单 → ③ SQL 预编译 + 输入校验双保险,永不信前端