简介:一份面向Java开发者的简易人人网(校内)自动登录资源。作者因网上老版本登录代码已失效,自己写了新版接口适配的基础版本,抛开了老版本中失效的加密与请求逻辑,适合需要快速了解模拟登录流程,或想基于旧项目改造登录模块的人。资源为7z压缩包,共4个文件,含1个Java源码和3个zip依赖库,分别覆盖HTTP通信、日志与编码处理,包体约4.31MB,结构清晰,添加依赖后可运行。目前已有43116人学习/下载,说明这一需求仍较普遍。由于定位是简易版,资源重在抛砖引玉:通过源码可查看登录请求的URL构造、参数封装和基础调用逻辑,对比新版接口与旧版差异;拿到后自行补全自动上传、状态保持等后续业务即可,适合作为Java网络编程与模拟登录的入门练手素材,也可作为开发相关脚本前的快速验证样例。
1. 人人网(校内)自动登录:用 Java 代码把“手动点登录”这件事变成可重试的自动化任务
某天半夜,某开发者的定时任务又断在“自动登录”这一步,日志里只剩一串 302 跳转错误——账号密码都没变,Cookie 却被服务端悄悄清了。这种问题在挂着人人网(校内)老账号跑数据同步任务时几乎每个月都来一次。这份“人人网(校内)自动登录”Java 资源解决的问题就是:用代码模拟浏览器完成账号登录、保持会话、登录失败后自动重试,让日常巡检、数据抓取和同步任务不再依赖人工手动打开网页。适合写自动化脚本的 Java 开发者、维护旧业务系统的技术人,以及想弄明白“登录请求到底怎么回事”的初学者。它不依赖浏览器环境,一个 JDK 项目就能跑起来。
2. 登录原理与资源结构:为什么模拟登录能替换手动点按钮
在拆代码之前,先花十分钟把“登录”这件事的原理捋清楚。很多初学者上来就抱着 HttpPost 猛写,结果要么登录成功但拿不到数据,要么明明账号密码都对却被判定为异常访问,根本原因是只复制了请求,没复制浏览器的会话行为。
2.1 一场登录请求的完整链路:从打开登录页到拿到会话 Cookie
人人网(校内)这类老平台虽然界面换过多次,但登录逻辑一直遵循一套经典流程:先 GET 登录页拿到动态参数,再 POST 提交账号密码,服务端校验通过后返回 Set-Cookie,浏览器之后带着这个 Cookie 访问其他页面。模拟登录就是把这条链路用 Java 代码复刻一遍。
我在实现时通常会分成四步:
- GET 登录页:这一步不只是拿页面数据,更重要的是取出表单里隐藏的动态参数。常见做法是用 Jsoup 或正则抓取页面里的 hidden 字段,比如 token、时间戳、表单 action 地址。
- 处理密码:浏览器里的密码不会明文传输,常见做法是抓取登录页引用的 JS 文件,找到加密函数,然后在 Java 里复刻。校内平台的加密逻辑一般是对密码做一次或两次摘要运算,有些还会拼接固定 salt。
- POST 提交:把账号、加密后的密码、token 以及其他隐藏字段拼成表单数据提交。
- 接收 Cookie:服务端校验通过后会下发会话 Cookie。这一步的坑在于:HttpClient 默认不一定保存所有 Cookie,更不会自动处理重定向,所以
HttpClientContext和CookieStore必须显式配置。
为什么老平台要用这套流程?因为它们的登录接口不属于公开 API,没有专门为第三方提供 token 换取机制,模拟浏览器是唯一稳定可行的自动化方式。你也可以用 Selenium 驱动真实浏览器,但那样做内存占用高、部署麻烦,Java 后端服务里跑一堆浏览器进程不现实。轻量级 HttpClient 加 Jsoup 是更符合“自动化脚本”定位的选择。
2.2 资源包代码结构:我建议按这个顺序阅读
这份资源解压后,第一眼不要急着跑 Main,先按下面顺序读,效率会高很多:
- 配置文件:一般叫
config.properties或application.properties。里面放着登录 URL、账号、密码、请求超时时间、重试次数。这些参数是脚本能不能跑通的地基,先把 URL 和账号密码核对清楚。 - 加密工具类:
PasswordUtil或EncryptUtil。这里封装了密码摘要逻辑,也是登录失败时最常需要改动的地方。 - 核心登录类:
LoginClient或AutoLogin。它负责组装请求、提交表单、保存 Cookie,是整个资源的心脏。 - 会话验证类:
SessionValidator。登录不能“凭感觉成功”,要有一个方法去访问用户主页或某个需要登录才能看到的接口,根据返回内容判断会话是否有效。 - 调用入口:
Main或Runner。演示怎么把上面的组件串起来。
这个顺序就是“配置 → 加密 → 提交 → 验证”的执行顺序,也和你排查问题的顺序一致:先看配置对不对,再看加密对不对,再看提交成不成功,最后看会话是否真的有效。
3. 用 HttpClient 走通登录三步:抓页面、算密码、维持会话
这一章直接给可复现的代码。我用的是 Apache HttpClient 4.x 加 Jsoup 1.x,这个组合在 Java 项目里最常见,依赖少、文档全。如果你用的是 JDK 11 及以上,也可以换成java.net.http.HttpClient,思路完全一样,只是 API 写法不同。
3.1 第一步:抓取登录页并提取动态参数
先写一个方法把登录页拉下来,解析出表单信息。
public LoginPageInfo parseLoginPage() throws IOException { String loginPageUrl = config.getLoginPageUrl(); // 用 Jsoup 模拟浏览器 GET 请求,带上 UA 和超时时间 Document doc = Jsoup.connect(loginPageUrl) .userAgent(config.getUserAgent()) .timeout(8000) .get(); // 定位登录表单,拿到提交地址 Element form = doc.selectFirst("form[id=loginForm], form[action]"); if (form == null) { throw new IllegalStateException("登录表单未找到,页面结构可能已变化"); } String action = form.attr("action"); // 提取表单里所有 hidden 字段:token 和时间戳是关键 Map<String, String> hiddenFields = new HashMap<>(); for (Element input : form.select("input[type=hidden]")) { hiddenFields.put(input.attr("name"), input.val()); } return new LoginPageInfo(action, hiddenFields); }这段代码做了三件事:发起 GET 请求、定位表单、提取 hidden 字段。form[id=loginForm], form[action]这个选择器是双保险,优先按 ID 找,找不到就退回按表单属性找,避免页面结构调整时直接崩掉。
hiddenFields里的 token 字段一定要重视。老平台常见的做法是在服务端生成一次性 token,登录成功后即失效,所以这个 token 必须每次登录时现取现用,不能把第一次拿到的值写死在代码里。timeout 参数建议设 8000 到 10000 毫秒,太短容易在弱网环境误判失败,太长会让重试逻辑反应迟钝。
拿到这些参数后,最好打个日志输出 action 地址和字段名,用来和浏览器的开发者工具里看到的表单做比对。如果字段名对不上,后面的请求肯定会失败。
3.2 第二步:构造 POST 请求,密码加密与参数提交
接着是登录的核心提交逻辑。
public String login(String username, String rawPassword) throws Exception { LoginPageInfo pageInfo = parseLoginPage(); // 密码加密:这是登录成败最关键的一行,需要和页面 JS 保持一致 String encryptedPassword = encryptPassword(rawPassword); HttpPost post = new HttpPost(pageInfo.getActionUrl()); post.setHeader("User-Agent", config.getUserAgent()); post.setHeader("Referer", config.getLoginPageUrl()); List<NameValuePair> params = new ArrayList<>(); params.add(new BasicNameValuePair("email", username)); params.add(new BasicNameValuePair("password", encryptedPassword)); params.add(new BasicNameValuePair("token", pageInfo.getToken())); // 把所有 hidden 字段也带上,服务端可能校验非空 for (Map.Entry<String, String> entry : pageInfo.getHiddenFields().entrySet()) { params.add(new BasicNameValuePair(entry.getKey(), entry.getValue())); } post.setEntity(new UrlEncodedFormEntity(params, StandardCharsets.UTF_8)); CloseableHttpResponse response = httpClient.execute(post); // 读取响应体 String responseBody = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8); EntityUtils.consume(response.getEntity()); return responseBody; }referer头是一个很关键的参数。很多老接口会校验请求来源,不带 Referer 或者带错 Referer,服务端会直接拒绝。User-Agent我一般固定成一个常规浏览器的值,不要用代码默认的 Java 版本标识,否则容易被识别成脚本。
密码加密方法encryptPassword的实现在资源包里单独一个类,我一般会先把登录页引用的那个 JS 文件下载下来,找到加密函数,然后用 Java 复刻一遍。常见做法是对密码做摘要运算,必要时拼接时间戳或固定字符串。这一步没有快捷方式,必须逐个字符对齐。
注意表单提交用的是UrlEncodedFormEntity,也就是application/x-www-form-urlencoded,这和浏览器默认的表单提交方式一致。如果你看到接口返回 JSON,那就需要改成 JSON 实体,但校内这类老平台通常还是走传统表单。
3.3 第三步:用同一个 CookieStore 验证登录状态
登录请求发出去只是开始,真正的考验是后续请求能不能带上正确的 Cookie。
public boolean validateLogin() throws IOException { String profileUrl = config.getProfileUrl(); HttpGet get = new HttpGet(profileUrl); get.setHeader("User-Agent", config.getUserAgent()); get.setHeader("Referer", config.getLoginPageUrl()); CloseableHttpResponse response = httpClient.execute(get); String body = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8); EntityUtils.consume(response.getEntity()); // 判断是否登录成功:返回页面里包含用户标识,比如昵称或退出按钮 return body.contains(config.getUserMarker()) || body.contains("logout") || !body.contains("loginForm"); }这段代码成功的前提是httpClient在创建时配置了持久的CookieStore,并且在同一个客户端实例上执行登录和后续请求。代码大概是这样的:
CookieStore cookieStore = new BasicCookieStore(); CloseableHttpClient httpClient = HttpClientBuilder.create() .setDefaultCookieStore(cookieStore) .setRedirectStrategy(new LaxRedirectStrategy()) .build();LaxRedirectStrategy解决的是 302 跳转问题。校内登录成功后通常会有一个临时重定向,如果不配置这个策略,HttpClient 默认只会重定向 GET 请求,POST 的 302 会直接抛出异常或者返回一个空响应。这一点算是模拟登录的必经之坑。
验证方式不要只用 HTTP 状态码。服务端返回 200 不代表你登录成功,可能是返回了一个包含错误提示的页面。最可靠的做法是抓取用户主页或需要登录才能访问的接口,看返回值里有没有“退出登录”“我的主页”“个人资料”这类关键标识。资源包里通常会在配置文件中留一个字段让你填用户昵称,方便做标记匹配。
4. 验证码与登录频率:把真实登录的意外情况写进脚本
第一次跑通登录后,很多人以为完事了,结果第二天发现脚本又停了。原因多半是触发了验证码,或者登录频率太高被服务端暂时封禁。真实场景里的自动登录,必须把这两件事当默认情况来处理,而不是等它发生了慌张。
4.1 验证码从哪来:图片保存、人工输入与识别通道预留
我在处理登录返回时,会先检查响应里有没有验证码标识。常见做法是登录失败后服务端返回一个 JSON,里面包含captchaImage字段,值是 Base64 编码的图片。先把它取出来存成文件。
public String checkAndSaveCaptcha(String responseBody) throws IOException { if (responseBody == null || !responseBody.contains("captcha")) { return null; } JSONObject json = JSON.parseObject(responseBody); String base64Image = json.getString("captchaImage"); if (base64Image == null || base64Image.isEmpty()) { return null; } // 移除 Data URL 前缀,还原纯 Base64 String pureBase64 = base64Image; if (pureBase64.startsWith("data:image")) { pureBase64 = pureBase64.substring(pureBase64.indexOf(",") + 1); } byte[] imageBytes = Base64.getDecoder().decode(pureBase64); Path outputPath = Paths.get("captcha_" + System.currentTimeMillis() + ".png"); Files.write(outputPath, imageBytes); return outputPath.toString(); }保存验证码后,最直接的处理方式是人工输入:脚本停下来,打印提示“请在浏览器里打开 xxx,输入验证码”,然后从控制台读取用户输入,再带着验证码重新提交登录请求。这种方案适合跑批任务里偶尔才出现一次验证码的场景,简单可靠,不需要额外对接任何服务。
如果你想把验证码识别也自动化,建议在代码里预留一个接口抽象。定义一个CaptchaRecognizer接口,里面只有一个String recognize(String imagePath)方法。初期可以写一个固定返回 null 的默认实现,后续要接识别服务时,只要换一个实现类,不需要改动登录主流程。这个抽象层最大的意义,是让业务逻辑和识别逻辑解耦,未来不管换什么方案都不会改乱代码。
验证码图片处理有几个细节:保存前先确认图片格式,有些接口返回的是 WebP,直接存成 png 后缀会打不开。拿到字节流后可以试试用ImageIO.read读取,读不出来就说明格式不标准,需要先做格式转换。另一个细节是验证码有有效期,一般是 30 到 60 秒,人工输入时要提醒操作者别慢慢悠悠。
4.2 登录频率控制:失败重试的冷却时间怎么设
自动登录脚本最容易犯的错,就是失败后立刻重试,短时间连续几十次请求,结果触发服务端的风控机制,把自己 IP 送进临时黑名单。我一般在登录模块里加一个简单的滑动窗口计数器,限制单位时间内的登录尝试次数。
private int loginFailCount = 0; private long lastFailTime = 0; public void beforeLoginRetry() throws InterruptedException { if (loginFailCount < 3) { return; } long elapsed = System.currentTimeMillis() - lastFailTime; long cooldown = 10 * 60 * 1000L; // 连续失败 3 次,冷却 10 分钟 if (elapsed < cooldown) { long waitMillis = cooldown - elapsed; System.out.println("登录失败次数过多,等待 " + waitMillis / 1000 + " 秒后重试"); Thread.sleep(waitMillis); } loginFailCount = 0; }这个逻辑是在每次登录失败时调用loginFailCount++并更新lastFailTime,下次重试前先检查。10 分钟的冷却时间是我常用的初始值,你可以按自己的场景调:如果账号是多人共享的,建议调到 30 分钟;如果是自己单账号巡检,5 到 10 分钟即可。
除了重试间隔,还有两个参数值得控制:请求超时时间和重试总次数。前者我建议设 15 秒,上限 30 秒;后者设 3 到 5 次,超过这个数就放弃,发告警通知,而不是无限重试。无限重试的脚本等于把自己打包送进风控名单,没有任何收益。
日志也要打清楚:每次登录失败,至少记录当前是第几次失败、失败原因(密码错误 / 验证码错误 / 超时 / 接口异常)、距离下次重试还有多久。这样凌晨跑批失败时,第二天早上看日志能一眼定位问题,不用猜。
5. 常见问题排查:自动登录脚本最容易踩的五个坑
模拟登录的报错不算复杂,但每条都能让人折腾半天。这一章我按自己踩过的坑整理成排查记录,按“现象 → 原因 → 解决”的顺序写,基本都是可以照抄的排查路径。
第一个坑:登录返回 200,但后续请求仍是游客状态
现象:登录接口响应正常,HTTP 状态码 200,但紧接着访问用户主页,页面还是未登录页面,拿不到任何个人数据。原因:服务端登录成功后下发了一个新的 Cookie,但 HttpClient 在执行后续请求时没有带上,或者 302 重定向时重定向请求丢失了部分 Cookie。这是最典型的会话保持失败。解决:确认httpClient是在带DefaultCookieStore的情况下创建的,并且登录和后续请求用的是同一个客户端实例。检查代码里有没有重新new一个客户端,如果有,合并成一个。另外检查重定向策略是否设置,建议用LaxRedirectStrategy。
第二个坑:密码加密结果和 JS 不一致
现象:账号密码没问题,但登录接口返回“密码错误”或“账号异常”。原因:资源包里的加密工具类写的是某种摘要算法,但当前登录页 JS 里的加密逻辑已经变了,可能增加了一个时间戳拼接,或者换了一种摘要变体。解决:打开浏览器开发者工具,找到登录按钮提交时执行的 JS 文件,搜索password相关的函数,把完整的加密流程抄下来,再对照 Java 工具类逐行修改。注意有些加密会先对密码做一次摘要,再对“摘要 + 固定 salt”做第二次摘要,参数顺序不能反。资源包里通常有注释说明算法变更点,修改时先看注释再动手。
第三个坑:保存的验证码图片打不开
现象:脚本把验证码 Base64 字符串写成了 png 文件,但双击打开提示文件损坏或格式不对。原因:接口返回的可能不是标准 PNG,而是 WebP 或 JPEG,只是后缀容易被代码写成 png;另一种情况是 Base64 字符串里包含了data:image/webp;base64,前缀,代码没有剥掉就做解码。解决:保存前先打印一份 Base64 字符串的前 50 个字符,确认有没有前缀;如果图片是 WebP,找一个支持 WebP 的解码库,或者让后端同事把接口改成返回标准 PNG。做格式探测时可以用ImageIO.read(imageBytes)判断能否正常读取,读不出来就打印原始字节的魔数,对照文件格式表定位真实类型。
第四个坑:登录重试直接把 IP 送进了临时黑名单
现象:脚本第一次跑得好好的,某天失败了一次,自动重试 5 次后,后续所有请求都返回 403 或者空白页,连登录页都打不开。原因:重试逻辑没有加冷却,短时间频繁请求触发了服务端的风控策略,IP 被拉进临时黑名单。解决:给登录模块加上至少 10 分钟的失败冷却,重试次数控制在 3 次以内,并固定 User-Agent。已经进了黑名单的 IP 只能等它自动解除,不要硬试。如果是办公网出口 IP,还要小心整个网段一起被限制,那种情况需要找网络管理员换出口。
第五个坑:token 每次请求都变,复用旧 token 直接失败
现象:第一次抓登录页时拿到 token 并写进了代码,第二天登录直接报参数错误。原因:登录页的 token 是服务端动态生成的一次性值,每次 GET 都会变,登录提交后立即失效。解决:把 token 提取逻辑放进登录方法内部,每次提交前都重新 GET 一次登录页,用最新的 token 构造请求;不要把 token 缓存成静态变量反复使用。同时注意检查代码里是否有“首次启动时把所有参数抓一遍”的设计,如果有,改成“每次登录前只抓 token 和隐藏字段”。
这五个坑排查下来,登录模块基本就稳定了。实际项目里可能还有其他小问题,但只要把思路统一到“模拟浏览器行为”上来,就不会偏离太远。
6. 进阶技巧:把登录做成一次登录、长期有效的会话持久化
很多自动化任务不需要频繁登录,一天跑一两次就够了。如果每次启动都重新走一遍登录流程,一是慢,二是增加被风控的概率。更稳妥的做法是:登录成功后把 Cookie 持久化到本地文件,下次启动先加载旧 Cookie 验证是否有效,失效再自动重新登录。这样既省时间,又减少登录接口的调用频率。
public boolean tryLoadPersistentSession() { Path cookieFile = Paths.get(config.getCookieFilePath()); if (!Files.exists(cookieFile)) { return false; } try { BasicCookieStore loadedStore = new BasicCookieStore(); List<String> lines = Files.readAllLines(cookieFile, StandardCharsets.UTF_8); for (String line : lines) { String[] parts = line.split("\t"); if (parts.length < 3) { continue; } // 每行格式:名称 域名 值 BasicClientCookie cookie = new BasicClientCookie(parts[0], parts[2]); cookie.setDomain(parts[1]); cookie.setPath("/"); loadedStore.addCookie(cookie); } // 把恢复出来的 CookieStore 设置为当前客户端的状态 httpClient.setCookieStore(loadedStore); return validateLogin(); } catch (IOException e) { return false; } } public void savePersistentSession() throws IOException { List<String> lines = new ArrayList<>(); for (Cookie cookie : httpClient.getCookieStore().getCookies()) { lines.add(cookie.getName() + "\t" + cookie.getDomain() + "\t" + cookie.getValue()); } Files.write(Paths.get(config.getCookieFilePath()), lines, StandardCharsets.UTF_8); }核心思路就是把内存里的CookieStore序列化成文件,启动时反序列化回来。这里有个很关键的点:setCookieStore在部分 HttpClient 版本中并不支持运行时替换,更推荐的做法是在构建客户端时就用同一个BasicCookieStore实例,把加载出来的 Cookie 往这个实例里加。代码里写成setCookieStore只是为了演示意图,实现时留意一下版本差异。
Cookie 文件本质上就是文本文件,每行保存 Cookie 的名称、域名、值。保存路径建议放在项目的运行目录下,不要放在代码目录里,避免打包部署时被清理。加载之后必须做一次会话验证——像第 3.3 节那样访问用户主页,确认 Cookie 还活着。如果验证失败,就清空文件,走完整登录流程重新登录,再保存一遍。
从那以后,我每次写登录脚本都会强制走一遍“抽取动态参数 — 持久化会话 — 失效重登”这三步流程,不再允许自己偷懒跳过任何一环。这套思路不仅能用在人人网(校内)这类老平台上,其他基于表单登录的站点也适用。希望帮到你。
本文还有配套的精品资源,点击获取