一、Socket 编程
Socket(套接字)是网络通信的端点,是对 TCP/IP 协议的编程抽象,用于实现两台主机间的数据交换。
通俗来说:
-
可以把 Socket 理解为“电话插口”,插上后客户端和服务端才能“通话”。
-
Socket = IP 地址 + 端口号(唯一标识一个通信端)
1.1 TCP 通信原理与 Java 实现
1)TCP 特点
特性 | 描述 |
---|---|
面向连接 | 通信前需建立连接(三次握手) |
可靠传输 | 保证数据顺序、完整、无丢失 |
基于字节流 | 数据以流的形式传输 |
双向通信 | 可同时读写 |
2)通信流程图(标准 TCP 模型)
客户端 服务端| ---------> connect() ----------> || <-------- accept() <------------ || ---------> OutputStream --------|| <--------- InputStream ---------|| ---------> close() ------------>|
3)服务端代码示例(ServerSocket)
import java.io.*; // 导入输入输出相关类(BufferedReader、InputStreamReader、PrintWriter 等)
import java.net.*; // 导入网络相关类(ServerSocket、Socket 等)public class TCPServer { // 定义一个名为 TCPServer 的类public static void main(String[] args) throws IOException { // 主函数,抛出 IO 异常,避免必须 try-catchServerSocket serverSocket = new ServerSocket(8888); // 创建一个服务器端的 ServerSocket,监听 8888 端口System.out.println("服务端启动,等待连接..."); // 控制台提示:服务端已启动,等待客户端连接Socket socket = serverSocket.accept(); // 监听并接受客户端连接(阻塞式等待,直到客户端连接为止)System.out.println("客户端已连接:" + socket.getInetAddress()); // 打印连接上来的客户端 IP 地址// 读取客户端消息BufferedReader reader = new BufferedReader( // 创建 BufferedReader,从 socket 的输入流中读取数据new InputStreamReader(socket.getInputStream())); // 使用 InputStreamReader 把字节流转为字符流String msg = reader.readLine(); // 读取客户端发送的一行字符串(以换行符为结束标志)System.out.println("收到客户端消息: " + msg); // 打印收到的客户端消息// 给客户端回应PrintWriter writer = new PrintWriter( // 创建 PrintWriter 向客户端发送数据socket.getOutputStream(), true); // 获取 socket 的输出流,true 表示自动刷新缓冲区writer.println("你好,客户端,我收到你的消息了。"); // 向客户端发送一条消息作为回应socket.close(); // 关闭与客户端通信的 socket(释放资源)serverSocket.close(); // 关闭服务器的 ServerSocket(停止监听)}
}
4)客户端代码示例(Socket)
import java.io.*; // 导入 Java 输入输出相关类(如 BufferedReader、PrintWriter 等)
import java.net.*; // 导入 Java 网络通信相关类(如 Socket)public class TCPClient { // 定义一个 TCPClient 客户端类public static void main(String[] args) throws IOException { // 主函数,抛出 IOException 处理网络IO异常Socket socket = new Socket("127.0.0.1", 8888); // 创建 Socket 对象,连接本地 IP 的 8888 端口(连接服务端)// 发送消息PrintWriter writer = new PrintWriter( // 创建输出流 writer,用于向服务端发送数据socket.getOutputStream(), true); // 获取 socket 的输出流,true 表示自动刷新缓冲区writer.println("你好,我是客户端"); // 向服务端发送一行字符串// 读取回应BufferedReader reader = new BufferedReader( // 创建输入流 reader,用于读取服务端返回的数据new InputStreamReader(socket.getInputStream())); // 将字节输入流包装成字符输入流,再用 BufferedReader 包装方便读取String response = reader.readLine(); // 读取服务端返回的一行文本(以换行符为结束标志)System.out.println("服务端回应: " + response); // 打印服务端返回的内容socket.close(); // 通信完成后关闭 socket 释放资源}
}
5)多线程服务端(支持多个客户端连接)
while (true) { // 无限循环,持续接收客户端连接Socket socket = serverSocket.accept(); // 阻塞等待客户端连接,一旦有客户端连接就返回 Socketnew Thread(() -> { // 为每个连接创建一个新线程,避免阻塞主线程try {// 读取客户端发送的消息BufferedReader reader = new BufferedReader( // 创建 BufferedReader 包装输入流new InputStreamReader(socket.getInputStream())); // 从 socket 获取输入流,并转为字符流String msg = reader.readLine(); // 读取客户端发送的一行消息System.out.println("客户端消息:" + msg); // 打印客户端发送的内容// 向客户端发送回应PrintWriter writer = new PrintWriter( // 创建 PrintWriter 用于向客户端输出socket.getOutputStream(), true); // 获取 socket 的输出流,true 表示自动 flushwriter.println("服务端回应:" + msg); // 将收到的消息再返回给客户端作为回应socket.close(); // 关闭连接,释放资源} catch (IOException e) {e.printStackTrace(); // 异常处理,打印错误信息}}).start(); // 启动新线程,异步处理该客户端请求
}
1.2 UDP 通信原理与 Java 实现
1)UDP 特点
特性 | 描述 |
---|---|
无连接 | 不建立连接,直接发送数据 |
不可靠 | 可能丢包、乱序 |
面向报文 | 一次发送 = 一个数据报 |
快速高效 | 适合实时场景,如直播、游戏 |
2)通信模型
发送方:DatagramSocket.send(DatagramPacket)
接收方:DatagramSocket.receive(DatagramPacket)
3)UDP 发送端
import java.net.*; // 导入 Java 网络通信相关类(DatagramSocket、DatagramPacket、InetAddress)public class UDPClient { // 定义一个名为 UDPClient 的类public static void main(String[] args) throws Exception { // 主函数,抛出所有异常DatagramSocket socket = new DatagramSocket(); // 创建 UDP 套接字,用于发送数据(系统自动分配端口)String msg = "Hello UDP Server"; // 要发送的字符串消息byte[] data = msg.getBytes(); // 将字符串消息转换成字节数组(UDP 传输的是字节)InetAddress address = InetAddress.getByName("127.0.0.1"); // 获取本机 IP 地址(目标地址)DatagramPacket packet = new DatagramPacket( // 创建 UDP 数据报(数据包)data, data.length, // 数据内容和数据长度address, 9090); // 目标 IP 和目标端口(即服务端的监听端口)socket.send(packet); // 发送数据报System.out.println("发送完成"); // 控制台打印发送成功socket.close(); // 关闭 socket,释放资源}
}
4)UDP 接收端
import java.net.*; // 导入 Java 网络通信相关类(DatagramSocket、DatagramPacket)public class UDPServer { // 定义一个名为 UDPServer 的类public static void main(String[] args) throws Exception { // 主函数,抛出所有异常DatagramSocket socket = new DatagramSocket(9090); // 创建 DatagramSocket 并绑定端口 9090,等待接收客户端发送的 UDP 数据byte[] buffer = new byte[1024]; // 创建一个字节数组作为接收缓冲区,最大可接收 1024 字节数据DatagramPacket packet = new DatagramPacket(buffer, buffer.length); // 创建接收用的数据包对象,使用上述缓冲区socket.receive(packet); // 阻塞等待接收客户端发送的数据包(接收到后将数据写入 packet 中)String msg = new String(packet.getData(), 0, packet.getLength()); // 将接收到的字节数据转为字符串(只取有效长度部分)System.out.println("收到客户端信息:" + msg); // 打印接收到的客户端消息内容socket.close(); // 关闭 socket,释放绑定的端口资源}
}
1.3 TCP vs UDP 对比
特性 | TCP | UDP |
---|---|---|
是否连接 | 需要连接(三次握手) | 无连接 |
可靠性 | 可靠,保证顺序、无丢包 | 不可靠,可能丢包、乱序 |
传输单位 | 字节流 | 数据报(Datagram) |
速度 | 较慢 | 快 |
场景 | 文件传输、网页、数据库连接 | 视频流、语音、DNS、广播等 |
二、URL、HttpURLConnection 请求发送
Java 的标准库提供了 java.net.URL
和 java.net.HttpURLConnection
,它们是 HTTP 客户端的核心类,用于从 Java 应用中发送 GET、POST 等请求,获取 Web 服务器的响应数据。
2.1 URL 类:用于封装链接地址
URL url = new URL("https://httpbin.org/get");
常用方法:
url.getProtocol(); // 返回 "https"
url.getHost(); // 返回 "httpbin.org"
url.getPath(); // 返回 "/get"
url.openConnection(); // 返回 URLConnection 对象(默认是 HttpURLConnection)
2.2 HttpURLConnection 详解(发送请求的核心)
获取连接
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
设置请求方法(GET、POST 等)
conn.setRequestMethod("GET"); // 或 POST
设置连接参数(常用配置)
conn.setConnectTimeout(5000); // 连接超时时间
conn.setReadTimeout(5000); // 读取超时时间
conn.setRequestProperty("User-Agent", "Java-HttpClient"); // 设置请求头
2.3 GET 请求示例(带参数)
import java.io.*; // 导入输入输出相关类(如 BufferedReader、InputStreamReader)
import java.net.*; // 导入网络相关类(如 URL、HttpURLConnection)public class GetExample { // 定义一个名为 GetExample 的类public static void main(String[] args) throws Exception { // 主函数,抛出所有异常以简化处理String params = "name=test&age=22"; // 定义 GET 请求的参数字符串(URL 编码格式)URL url = new URL("https://httpbin.org/get?" + params); // 创建 URL 对象,将参数拼接到 URL 后面(GET 请求通过 URL 传参)HttpURLConnection conn = (HttpURLConnection) url.openConnection(); // 打开连接,并强制转换为 HttpURLConnection 对象conn.setRequestMethod("GET"); // 设置请求方法为 GET(默认其实就是 GET,但写清楚更规范)BufferedReader reader = new BufferedReader( // 创建字符缓冲输入流,用于读取响应内容new InputStreamReader(conn.getInputStream())); // 从连接中获取输入流(即响应体内容)String line; // 定义每行读取的临时变量while ((line = reader.readLine()) != null) { // 逐行读取响应内容,直到为 null(即读完)System.out.println(line); // 输出当前行内容到控制台}reader.close(); // 关闭读取流,释放资源conn.disconnect(); // 断开 HTTP 连接,释放网络资源}
}
2.4 POST 请求示例(表单提交)
import java.io.*; // 导入输入输出相关类
import java.net.*; // 导入网络相关类public class PostExample { // 定义 PostExample 类public static void main(String[] args) throws Exception { // 主函数,抛出异常URL url = new URL("https://httpbin.org/post"); // 创建目标 URL,指向 httpbin 的 POST 测试接口HttpURLConnection conn = (HttpURLConnection) url.openConnection(); // 打开 HTTP 连接conn.setRequestMethod("POST"); // 设置请求方法为 POSTconn.setDoOutput(true); // 允许向连接写入请求体(必须设置)conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); // 设置请求头,表明发送的是表单数据// 写入请求参数(表单格式)String params = "username=test&password=123456"; // POST 参数,URL 编码格式字符串OutputStream os = conn.getOutputStream(); // 获取连接的输出流os.write(params.getBytes()); // 将参数转换成字节流写入请求体os.flush(); // 刷新缓冲区,确保所有数据发送出去os.close(); // 关闭输出流// 读取服务器响应BufferedReader reader = new BufferedReader( // 包装输入流,方便按行读取响应内容new InputStreamReader(conn.getInputStream())); // 获取连接的输入流(响应体)String line;while ((line = reader.readLine()) != null) { // 循环读取每一行System.out.println(line); // 打印响应内容到控制台}reader.close(); // 关闭输入流conn.disconnect(); // 断开 HTTP 连接,释放资源}
}
2.5 设置请求头(模拟浏览器)
常用请求头设置:
conn.setRequestProperty("User-Agent", "Mozilla/5.0");
conn.setRequestProperty("Referer", "https://example.com/");
conn.setRequestProperty("Cookie", "session=abc123; token=xyz");
2.6 获取响应状态与头部信息
int code = conn.getResponseCode(); // 200、302、404...
String contentType = conn.getHeaderField("Content-Type");
String setCookie = conn.getHeaderField("Set-Cookie");
2.7 处理 gzip 压缩响应
某些服务器返回的数据是压缩的,需解压读取:
InputStream in = conn.getInputStream(); // 从 HttpURLConnection 获取响应的输入流(原始数据流)
String encoding = conn.getContentEncoding(); // 获取服务器响应头中的 Content-Encoding 字段(告诉你数据是否压缩)if ("gzip".equalsIgnoreCase(encoding)) { // 判断响应内容是否使用 gzip 压缩(忽略大小写比较)in = new GZIPInputStream(in); // 如果是 gzip,则用 GZIPInputStream 对流进行解压处理
}BufferedReader reader = new BufferedReader( // 用 BufferedReader 包装输入流,方便逐行读取字符数据new InputStreamReader(in)); // InputStreamReader 把字节流转换成字符流,结合编码使用
2.8 完整通用封装(支持 GET/POST)
public static String send(String urlStr, String method, String body, Map<String, String> headers) throws IOException {URL url = new URL(urlStr); // 创建 URL 对象,传入请求地址字符串HttpURLConnection conn = (HttpURLConnection) url.openConnection(); // 打开 HTTP 连接并强制转换为 HttpURLConnectionconn.setRequestMethod(method); // 设置请求方法(GET、POST 等)conn.setConnectTimeout(5000); // 设置连接超时时间(5秒)conn.setReadTimeout(5000); // 设置读取响应超时时间(5秒)if (headers != null) { // 如果传入了请求头集合for (Map.Entry<String, String> entry : headers.entrySet()) { // 遍历所有请求头conn.setRequestProperty(entry.getKey(), entry.getValue()); // 设置请求头键值对}}if ("POST".equalsIgnoreCase(method) && body != null) { // 如果是 POST 请求且请求体不为空conn.setDoOutput(true); // 允许向连接写入数据(必须)OutputStream os = conn.getOutputStream(); // 获取输出流os.write(body.getBytes()); // 将请求体写入输出流(默认字符集)os.close(); // 关闭输出流,完成请求体写入}BufferedReader reader = new BufferedReader( // 读取响应输入流,方便按行读取文本new InputStreamReader(conn.getInputStream())); // 从连接获取响应流并转换成字符流StringBuilder sb = new StringBuilder(); // 创建 StringBuilder 用于拼接响应内容String line;while ((line = reader.readLine()) != null) { // 循环逐行读取响应sb.append(line).append("\n"); // 将每行添加到 StringBuilder 并换行}reader.close(); // 关闭读取流,释放资源conn.disconnect(); // 断开 HTTP 连接return sb.toString(); // 返回拼接好的完整响应字符串
}
2.9 HttpURLConnection 使用建议
使用点 | 建议与说明 |
---|---|
多次请求同一主机 | 推荐使用 Apache HttpClient(连接池) |
请求体为 JSON | 设置 Content-Type: application/json 并用 Writer 传入 JSON 字符串 |
遇到跳转(302) | 默认不自动重定向,需要手动处理 |
设置代理抓包调试 | 可使用 System.setProperty("http.proxyHost", "127.0.0.1") |
异常处理 | 捕获 IOException , MalformedURLException |
2.10 常见问题
问题 | 原因 |
---|---|
请求超时 | 网络不可达、超时时间太短 |
POST 参数无效 | setDoOutput(true) 未设置 |
响应乱码 | 编码未设对,建议用 UTF-8 |
302 重定向失败 | HttpURLConnection 默认不跟随 POST 重定向 |
2.11 小结
URL url = new URL("...");
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET/POST");
conn.setRequestProperty(...);
conn.getOutputStream().write(...); // POST 情况
InputStream in = conn.getInputStream(); // 响应读取
conn.disconnect();
三、ServerSocket 创建服务端监听
ServerSocket
是 Java 网络编程中用于实现 TCP 服务端监听的类。它能在指定端口持续监听客户端连接,一旦有连接到达,就会生成一个 Socket
对象,用于与该客户端进行后续的通信。
3.1 核心概念:TCP 通信三步走
1. 服务端创建 ServerSocket(绑定端口)
2. 客户端发起连接 Socket.connect()
3. 服务端 accept() 接收连接,返回 Socket
3.2 ServerSocket 类常用方法
方法 | 作用 |
---|---|
new ServerSocket(port) | 创建监听端口 |
accept() | 阻塞式等待客户端连接 |
getInetAddress() | 获取连接地址 |
close() | 关闭服务端监听 |
3.3 最小运行示例(单客户端)
import java.io.*; // 导入输入输出相关类(BufferedReader、InputStreamReader、PrintWriter 等)
import java.net.*; // 导入网络相关类(ServerSocket、Socket 等)public class SimpleServer { // 定义一个名为 SimpleServer 的类public static void main(String[] args) throws IOException { // 主函数,抛出 IOException 以处理网络通信异常ServerSocket serverSocket = new ServerSocket(8888); // 创建服务器监听套接字,绑定端口 8888System.out.println("服务端启动,监听端口 8888"); // 控制台提示服务已启动Socket socket = serverSocket.accept(); // 阻塞等待客户端连接(连接成功返回 Socket 对象)System.out.println("客户端已连接:" + socket.getInetAddress()); // 打印连接的客户端 IP 地址BufferedReader reader = new BufferedReader( // 创建字符缓冲输入流,读取客户端发来的数据new InputStreamReader(socket.getInputStream())); // 获取输入流并转为字符流String msg = reader.readLine(); // 读取一行数据(以换行符为结束标志)System.out.println("收到消息:" + msg); // 打印客户端消息内容PrintWriter writer = new PrintWriter( // 创建输出流,用于发送数据给客户端socket.getOutputStream(), true); // 第二个参数 true 表示自动刷新(无需手动 flush)writer.println("服务端收到你的消息啦"); // 向客户端发送响应socket.close(); // 通信结束后关闭客户端连接serverSocket.close(); // 关闭服务端监听端口,释放资源}
}
3.4 配套客户端(测试使用)
import java.io.*; // 导入输入输出相关类(PrintWriter、BufferedReader 等)
import java.net.*; // 导入网络通信相关类(Socket)public class SimpleClient { // 定义客户端类 SimpleClientpublic static void main(String[] args) throws IOException { // 主函数,抛出 IOException 异常(用于网络通信处理)Socket socket = new Socket("127.0.0.1", 8888); // 创建一个 Socket,连接本机 IP 的 8888 端口(服务端)PrintWriter writer = new PrintWriter( // 创建字符输出流,向服务端发送数据socket.getOutputStream(), true); // 获取 socket 的输出流,true 表示自动刷新缓冲区writer.println("你好,我是客户端!"); // 发送一行文本数据给服务端BufferedReader reader = new BufferedReader( // 创建字符输入流,用于读取服务端返回的数据new InputStreamReader(socket.getInputStream())); // 获取 socket 的输入流,并转为字符流String response = reader.readLine(); // 读取服务端返回的一行字符串System.out.println("服务端回应:" + response); // 打印服务端返回的消息socket.close(); // 关闭 socket 连接,释放资源}
}
3.5 支持多客户端连接(多线程服务端)
一个服务端接收多个客户端,必须使用 多线程 处理每个连接。
import java.io.*; // 导入输入输出相关类(BufferedReader、PrintWriter 等)
import java.net.*; // 导入网络通信相关类(ServerSocket、Socket 等)public class MultiClientServer { // 主类:服务端public static void main(String[] args) throws IOException { // 主方法,抛出 IOException(用于处理网络通信错误)ServerSocket serverSocket = new ServerSocket(8888); // 创建服务器监听套接字,监听端口 8888System.out.println("服务端启动,监听端口 8888"); // 提示服务端已启动while (true) { // 无限循环,持续接受客户端连接Socket client = serverSocket.accept(); // 阻塞等待客户端连接,连接成功返回 Socketnew Thread(new ClientHandler(client)).start(); // 每一个客户端连接都启动一个新线程处理(多线程处理并发)}}
}class ClientHandler implements Runnable { // 定义客户端处理器类,实现 Runnable 接口(可被线程执行)private Socket socket; // 每个对象持有一个客户端连接 Socketpublic ClientHandler(Socket socket) { // 构造函数,接收客户端 Socket 并赋值this.socket = socket;}public void run() { // run 方法是线程执行的主体try {System.out.println("连接客户端:" + socket.getInetAddress()); // 打印客户端 IP 地址BufferedReader reader = new BufferedReader( // 获取输入流,用于读取客户端发来的数据new InputStreamReader(socket.getInputStream()));String msg = reader.readLine(); // 读取客户端发来的消息(阻塞直到有数据)System.out.println("收到消息:" + msg); // 打印收到的消息PrintWriter writer = new PrintWriter( // 获取输出流,用于回应客户端socket.getOutputStream(), true);writer.println("收到你的消息啦,线程ID:" + Thread.currentThread().getId()); // 回复内容中加入当前线程 IDsocket.close(); // 通信完成后关闭 Socket 连接} catch (IOException e) {e.printStackTrace(); // 异常打印(例如连接断开时)}}
}
3.6 ServerSocket 构造函数详解
new ServerSocket(port); // 默认本地地址监听
new ServerSocket(port, backlog); // 指定连接队列长度
new ServerSocket(port, backlog, InetAddress); // 指定本地 IP(如多网卡)
-
port
: 要监听的端口(0~65535,推荐 >= 1024) -
backlog
: 同时等待连接的最大数量(默认50) -
InetAddress
: 指定绑定地址(如绑定公网IP)
3.7 服务端监听常见应用场景
场景 | 实现思路 |
---|---|
HTTP Web 服务模拟 | 客户端发送 HTTP 请求报文,服务端解析并构造响应 |
聊天室服务端 | 多客户端连接,广播消息给所有人 |
命令控制接口 | 服务端接受控制指令,执行并返回结果 |
文件传输服务 | 客户端上传/下载文件 |
3.8 服务端监听与请求结构(HTTP 示例)
客户端请求:
GET / HTTP/1.1
Host: localhost:8080
User-Agent: curl/7.58.0服务端可读取:
GET / HTTP/1.1
Host: localhost:8080
User-Agent: curl/7.58.0
服务端可返回:
HTTP/1.1 200 OK
Content-Type: text/plainHello from Server
3.9 常见错误排查
错误 | 说明 |
---|---|
BindException: Address already in use | 端口被占用 |
ConnectException: Connection refused | 客户端找不到服务端 |
SocketException: Socket closed | 连接未正常关闭 |
EOFException | 客户端提前关闭连接 |
3.10 小结
ServerSocket server = new ServerSocket(8888);while (true) {Socket client = server.accept(); // 阻塞接收连接new Thread(() -> {InputStream in = client.getInputStream();OutputStream out = client.getOutputStream();// 处理请求 + 返回响应}).start();
}
四、底层协议包抓包分析
抓包(Packet Sniffing)是逆向分析、协议还原、爬虫绕过、参数定位的核心技能之一。
抓包可以:
-
查看真实请求头/参数/内容
-
分析加密数据的位置和结构
-
识别 Cookie / Token / Headers
-
还原移动 APP / 小程序 / 加密 JS 的真实通信行为
4.1 抓包工具对比
工具 | 特点 | 适用场景 |
---|---|---|
Wireshark | 抓底层 TCP/IP 包,包含所有协议 | TCP/UDP/SSL 分析,低层协议 |
mitmproxy | 抓取 HTTPS 应用层请求,支持中间人证书 | 抓取 HTTP/HTTPS 请求、反爬分析 |
Fiddler | Windows 专用 GUI 抓包工具 | 类似 mitmproxy,图形界面 |
Charles | 跨平台 GUI 抓包工具 | 抓取手机/浏览器通信 |
mitmproxy
,它是 跨平台 + 支持脚本分析 + 免费 + 支持 CLI/GUI 的强力工具。
4.2 mitmproxy 抓包工具安装与使用
1)安装(需 Python 环境)
pip install mitmproxy
2)启动 Web GUI 界面(推荐)
mitmweb
默认监听 127.0.0.1:8080
,并打开 Web UI 界面(http://127.0.0.1:8081)
4.3 mitmproxy 实现“中间人”原理
[Java程序/浏览器] →→ mitmproxy(伪装服务器) →→ 目标网站mitmproxy 拦截请求并生成“伪造证书”
HTTPS 抓包必须让系统或浏览器信任 mitmproxy 的证书。
4.4 mitmproxy 证书安装(用于抓取 HTTPS)
手机抓包(Android):
-
手机设置代理为:WiFi → 高级 → HTTP代理 → 手动(填入
电脑IP:8080
) -
手机浏览器访问:
http://mitm.it
-
下载 Android 根证书并安装(系统或用户证书)
电脑抓包(Java 应用):
Java 默认不信任 mitmproxy 的证书。可用两种方式解决:
方法 1:禁用 SSL 验证(开发调试可用)
import javax.net.ssl.*; // 引入 Java 提供的 SSL 通信相关的类和接口public class SSLBypass {static {try {// 创建一个“信任所有证书”的 TrustManager 数组TrustManager[] trustAllCerts = new TrustManager[]{new X509TrustManager() {public void checkClientTrusted(java.security.cert.X509Certificate[] certs, String authType) {} // 不检查客户端证书public void checkServerTrusted(java.security.cert.X509Certificate[] certs, String authType) {} // 不检查服务端证书public java.security.cert.X509Certificate[] getAcceptedIssuers() { return null; } // 返回 null 表示接受所有}};// 初始化一个 SSLContext,使用上面的“信任所有证书”的 TrustManagerSSLContext sc = SSLContext.getInstance("SSL");sc.init(null, trustAllCerts, new java.security.SecureRandom());// 将默认的 SSLSocketFactory 替换成上面这个“信任所有证书”的版本HttpsURLConnection.setDefaultSSLSocketFactory(sc.getSocketFactory());// 创建并设置一个“信任所有主机名”的 HostnameVerifier(域名不匹配也通过)HostnameVerifier hv = (hostname, session) -> true;HttpsURLConnection.setDefaultHostnameVerifier(hv);} catch (Exception e) {e.printStackTrace(); // 捕获并打印异常(一般不会触发)}}
}
方法 2:导入 mitmproxy 证书到 Java 信任库
# 导出 mitmproxy 证书(例如下载到 mitmproxy-ca-cert.pem)
openssl x509 -inform PEM -in mitmproxy-ca-cert.pem -out mitmproxy.crt# 导入到 JDK 信任库
keytool -import -trustcacerts -alias mitmproxy -file mitmproxy.crt -keystore $JAVA_HOME/lib/security/cacerts -storepass changeit
4.5 Java 设置代理(让请求流经 mitmproxy)
设置系统代理(全局)
System.setProperty("http.proxyHost", "127.0.0.1");
System.setProperty("http.proxyPort", "8080");
System.setProperty("https.proxyHost", "127.0.0.1");
System.setProperty("https.proxyPort", "8080");
设置单次代理(推荐):
Proxy proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress("127.0.0.1", 8080));
HttpURLConnection conn = (HttpURLConnection) new URL("https://httpbin.org/get").openConnection(proxy);
4.6 实际抓包分析内容
一旦 Java 程序流量经过 mitmproxy,将在 mitmweb 中看到:
抓到的内容 | 说明 |
---|---|
请求方法与地址 | GET /api/login |
请求头(Headers) | User-Agent、Cookie、Referer |
请求体(POST body) | 登录用户名密码、form 表单内容 |
响应内容 | HTML 页面、JSON 数据、状态码 |
响应 Set-Cookie | 登录状态维持的重要标志 |
4.7 使用 mitmproxy 脚本进一步分析/修改流量
创建 modify_request.py
:
def request(flow):if flow.request.pretty_url.startswith("https://target.com/api"):print("请求参数:", flow.request.text)flow.request.headers["X-Intercepted"] = "True"
运行:
mitmdump -s modify_request.py
4.8 示意流程图
[ Java应用 ]↓ 代理设置 (127.0.0.1:8080)
[ mitmproxy ] ← 拦截 HTTPS 流量,解密数据↓
[ 真实服务器 ]
4.9 小结
步骤 | 工具/方式 |
---|---|
抓取网页或 APP 的请求 | mitmproxy、Fiddler、Charles |
设置 Java 代理 | Proxy 类 或 System.setProperty |
证书处理(HTTPS) | 导入证书 / 忽略验证 |
分析参数结构 | Headers、Form、Cookie、URL 参数 |
还原请求 | Java 模拟 GET/POST,构造对应参数 |
五、模拟登录、表单提交、cookie 操作
真实网站很多页面必须登录后才能访问,比如:
-
用户中心、购物车、订单页
-
发帖、点赞、评论等操作
-
管理后台数据采集
所以需要学会:
✔ 模拟登录 ➜ 模拟表单提交 ➜ 维持 Cookie ➜ 访问受限页面
真实网站登录流程简化图
1. GET 登录页面(拿 Cookie + token)
2. POST 表单提交(用户名+密码+token)
3. 响应中返回 Set-Cookie(登录成功)
4. 后续请求需带 Cookie
5.1 实战步骤详解(Java 实现)
Step 1:请求登录页(获取初始 Cookie)
URL loginPageUrl = new URL("https://example.com/login"); // 创建一个 URL 对象,指向登录页面地址
HttpURLConnection conn = (HttpURLConnection) loginPageUrl.openConnection(); // 打开连接,强制转换为 HttpURLConnection
conn.setRequestProperty("User-Agent", "Mozilla/5.0"); // 设置请求头中的 User-Agent,模拟浏览器访问String setCookie = conn.getHeaderField("Set-Cookie"); // 获取响应头中名为 Set-Cookie 的字段(服务器下发的 Cookie)
System.out.println("初始 Cookie:" + setCookie); // 打印获取到的 Cookie 字符串
Step 2:构造 POST 请求,提交表单(模拟用户登录)
URL loginAction = new URL("https://example.com/doLogin"); // 构造登录请求的 URL(POST 接口地址)
HttpURLConnection loginConn = (HttpURLConnection) loginAction.openConnection(); // 打开连接并强转为 HttpURLConnection
loginConn.setRequestMethod("POST"); // 设置请求方法为 POST
loginConn.setDoOutput(true); // 允许向请求体写数据(POST 必须)// 伪装成浏览器请求,设置常见请求头
loginConn.setRequestProperty("User-Agent", "Mozilla/5.0"); // 模拟浏览器的 User-Agent
loginConn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); // 设置请求体内容类型为表单格式
loginConn.setRequestProperty("Cookie", setCookie); // 附带之前获取的 Cookie(比如登录页返回的 Cookie)// 构造 POST 表单内容,注意参数需要 URL 编码
String body = "username=admin&password=123456"; // 登录参数(账号密码)
OutputStream os = loginConn.getOutputStream(); // 获取连接的输出流
os.write(body.getBytes()); // 将表单参数写入请求体(默认编码)
os.flush(); // 刷新缓冲区,确保数据发送出去
os.close(); // 关闭流// 读取服务器返回的新 Cookie(登录成功后通常会更新会话 Cookie)
String loginCookie = loginConn.getHeaderField("Set-Cookie"); // 获取响应头中新的 Set-Cookie
System.out.println("登录后的 Cookie:" + loginCookie); // 打印登录后返回的 Cookie 字符串
Step 3:访问登录后的页面(带上 Cookie)
URL userCenter = new URL("https://example.com/user/home"); // 构造用户主页 URL 地址
HttpURLConnection userConn = (HttpURLConnection) userCenter.openConnection(); // 打开连接并转为 HttpURLConnection
userConn.setRequestProperty("User-Agent", "Mozilla/5.0"); // 设置 User-Agent 模拟浏览器请求
userConn.setRequestProperty("Cookie", loginCookie); // 携带登录时服务器返回的 Cookie,保持登录状态BufferedReader reader = new BufferedReader( // 创建缓冲字符输入流,读取响应内容new InputStreamReader(userConn.getInputStream())); // 获取连接的输入流,读取响应体
String line;
while ((line = reader.readLine()) != null) { // 按行读取响应内容,直到读完System.out.println(line); // 打印每一行到控制台
}
reader.close(); // 关闭输入流,释放资源
5.2 Cookie 详解(登录状态维持)
-
服务端登录成功后返回
Set-Cookie
-
后续所有请求都需要带这个
Cookie
否则会被认为是“未登录” -
Java 中可以:
-
手动设置 Cookie
-
或使用
CookieManager
自动管理多个 Cookie
-
5.3 完整封装工具类
public class HttpUtils { // 定义 HttpUtils 工具类,封装 HTTP 登录和页面请求功能public static String loginAndGetCookie(String loginUrl, String params, String initCookie) throws IOException {URL url = new URL(loginUrl); // 创建登录接口的 URL 对象HttpURLConnection conn = (HttpURLConnection) url.openConnection(); // 打开连接,强制转换为 HttpURLConnectionconn.setRequestMethod("POST"); // 设置请求方法为 POST,表示发送数据conn.setDoOutput(true); // 允许向连接输出数据(写请求体)conn.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); // 设置请求体格式为表单格式conn.setRequestProperty("User-Agent", "Mozilla/5.0"); // 设置请求头 User-Agent,模拟浏览器访问if (initCookie != null) // 如果有传入初始 Cookie,则设置 Cookie 请求头conn.setRequestProperty("Cookie", initCookie);OutputStream os = conn.getOutputStream(); // 获取连接的输出流,用于写入请求体数据os.write(params.getBytes()); // 将登录参数(字符串)写入请求体,默认编码os.close(); // 关闭输出流,结束请求体写入return conn.getHeaderField("Set-Cookie"); // 从响应头获取服务器返回的 Set-Cookie,返回给调用方}public static String getPage(String urlStr, String cookie) throws IOException {URL url = new URL(urlStr); // 创建要访问页面的 URL 对象HttpURLConnection conn = (HttpURLConnection) url.openConnection(); // 打开连接,强转为 HttpURLConnectionconn.setRequestProperty("User-Agent", "Mozilla/5.0"); // 设置请求头 User-Agent,模拟浏览器访问if (cookie != null) // 如果传入 Cookie,设置 Cookie 请求头,带上登录态conn.setRequestProperty("Cookie", cookie);BufferedReader reader = new BufferedReader( // 创建缓冲字符输入流,方便逐行读取响应体new InputStreamReader(conn.getInputStream())); // 获取响应输入流并转换成字符流StringBuilder sb = new StringBuilder(); // 创建 StringBuilder 用于拼接读取的页面内容String line; // 定义变量存储每行读取的字符串while ((line = reader.readLine()) != null) // 循环读取响应的每一行,直到结束sb.append(line).append("\n"); // 将每行内容添加到 StringBuilder 并换行return sb.toString(); // 返回拼接完成的完整页面内容字符串}
}
5.4 如何分析真实网站的登录行为?
借助浏览器/mitmproxy 抓包:
Chrome:
-
打开 DevTools → Network → login 接口
-
查看:
-
请求方法(POST/GET)
-
表单字段名(username/password/captcha/token)
-
Headers(Referer/User-Agent/Cookie)
-
响应中的 Set-Cookie
-
mitmproxy:
抓 HTTPS 全流量,分析:
-
真实请求体参数
-
登录是否加密(sign/token)
-
Cookie 是登录成功的关键
5.5 应对验证码、token、重定向、加密参数
问题 | 处理方法 |
---|---|
登录有验证码 | 用 Python 识别图形验证码 / OCR + 人工辅助 |
表单中有 CSRF token | 抓登录页 HTML,提取隐藏字段 name="csrf_token" |
登录成功后 302 | 跟踪 Location 头,发起二次请求 |
有 JS 加密参数 | 需逆向 JS 算法,还原加密逻辑(推荐 mitmproxy + 调试) |
5.6 小结
模拟登录 = 抓登录请求结构 + 提交表单字段 + 解析返回Cookie + 维持 Cookie 请求受限页
如果要做自动化平台、信息采集或爬虫登录:
-
封装好 Cookie + 登录逻辑类
-
实现 Cookie 自动保存(本地文件/数据库)
-
搭配验证码识别或逆向 JS 还原参数