Node.js 模拟 CAS 登录:Axios + Cheerio 自动获取智慧江财 3 个关键参数

在校园信息化系统中,统一身份认证(CAS)是常见的登录方式。对于开发者而言,掌握如何通过代码模拟这一流程,不仅能提升工作效率,还能为后续自动化操作奠定基础。本文将深入探讨如何利用 Node.js 生态中的 Axios 和 Cheerio 库,完整实现智慧江财系统的登录流程,重点解决动态参数获取、会话维持等核心问题。

1. 准备工作与环境搭建

在开始编码之前,我们需要确保开发环境配置正确。以下是必要的工具和库:

  • Node.js :建议使用最新的 LTS 版本(如 16.x 或更高)
  • npm/yarn :用于管理项目依赖
  • TypeScript (可选):提供更好的类型检查和代码提示

创建项目并安装依赖:

mkdir cas-login-demo && cd cas-login-demo
npm init -y
npm install axios cheerio qs @types/node --save

对于使用 TypeScript 的开发者,还需要额外配置:

npm install typescript ts-node @types/axios @types/cheerio --save-dev

2. 登录流程分析与关键参数识别

智慧江财的 CAS 登录流程主要包含以下几个关键步骤:

  1. 初始请求 :获取登录页面,从中提取动态参数
  2. 参数加密 :对密码进行 RSA 加密处理
  3. 提交登录 :携带所有必要参数发起 POST 请求
  4. 会话维持 :处理服务器返回的 Cookie 和重定向

其中,三个核心动态参数需要特别关注:

参数名称 获取方式 作用
lt 从登录页面 HTML 中提取 防止 CSRF 攻击的一次性令牌
JSESSIONID 从响应头 Set-Cookie 中获取 会话标识符
sessionMapKey 从响应头 Set-Cookie 中获取 会话映射键

3. 核心代码实现

3.1 初始化请求与参数获取

首先创建一个类来封装登录逻辑:

import axios from 'axios';
import cheerio from 'cheerio';
import qs from 'qs';

class CasLogin {
  private readonly loginUrl = 'https://ssl.jxufe.edu.cn/cas/login';
  private readonly rsaPublicKey = {
    n: '5598e3b75d21a2989274e222fa59ab07d829faa29b544e3a920c4dd287aed9302a657280c23220a35ae985ba157400e0502ce8e44570a1513bf7146f372e9c842115fb1b86def80e2ecf9f8e7a586656d12b27529f487e55052e5c31d0836b2e8c01c011bca911d983b1541f20b7466c325b4e30b4a79652470e88135113c9d9',
    e: '10001'
  };

  constructor(private username: string, private password: string) {}

  async startLogin() {
    try {
      // 第一步:获取登录页面并提取关键参数
      const { lt, JSESSIONID, sessionMapKey } = await this.fetchLoginParams();
      
      // 第二步:加密密码
      const encryptedPassword = this.rsaEncrypt(this.password);
      
      // 第三步:提交登录
      await this.submitLogin(lt, JSESSIONID, sessionMapKey, encryptedPassword);
      
      console.log('登录成功!');
    } catch (error) {
      console.error('登录失败:', error.message);
    }
  }

  private async fetchLoginParams() {
    const response = await axios.get(this.loginUrl, {
      headers: { 'User-Agent': 'Mozilla/5.0' }
    });

    // 从响应头获取 Cookie
    const cookies = response.headers['set-cookie'];
    const JSESSIONID = cookies[0].match(/JSESSIONID=([^;]+)/)[1];
    const sessionMapKey = cookies[1].match(/sessoinMapKey=([^;]+)/)[1];

    // 从 HTML 中提取 lt 参数
    const $ = cheerio.load(response.data);
    const lt = $('input[name="lt"]').val() as string;

    return { lt, JSESSIONID, sessionMapKey };
  }
}

3.2 RSA 加密实现

密码加密是登录过程中的关键环节。我们需要实现 RSA 加密功能:

// 在 CasLogin 类中添加以下方法
private rsaEncrypt(plainText: string): string {
  // 这里简化了实现,实际项目中应使用完整的 RSA 库
  const { RSAKey } = require('./rsa');
  const rsa = new RSAKey();
  rsa.setPublic(this.rsaPublicKey.n, this.rsaPublicKey.e);
  return rsa.encrypt(plainText);
}

注意:RSA 加密模块需要单独实现或从源代码中提取。确保公钥参数(n,e)与目标系统一致,这些参数可能会定期更新。

3.3 登录请求提交

获取所有必要参数后,我们可以构造登录请求:

// 在 CasLogin 类中添加以下方法
private async submitLogin(
  lt: string,
  JSESSIONID: string,
  sessionMapKey: string,
  encryptedPassword: string
) {
  const formData = qs.stringify({
    username: this.username,
    password: encryptedPassword,
    lt,
    _eventId: 'submit',
    cryptoType: '1',
    _rememberMe: 'on',
    errors: '0',
    imageCodeName: ''
  });

  const response = await axios.post(this.loginUrl, formData, {
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded',
      'Cookie': `JSESSIONID=${JSESSIONID}; sessoinMapKey=${sessionMapKey}`,
      'User-Agent': 'Mozilla/5.0',
      'Referer': this.loginUrl
    },
    maxRedirects: 0,
    validateStatus: status => status >= 200 && status < 400
  });

  // 处理可能的302重定向
  if (response.status === 302) {
    const redirectUrl = response.headers.location;
    console.log('需要重定向到:', redirectUrl);
    // 这里可以继续处理重定向逻辑
  }
}

4. 错误处理与健壮性优化

在实际应用中,我们需要考虑各种异常情况:

  1. 网络请求失败 :添加重试机制
  2. 参数获取失败 :增加校验逻辑
  3. 登录状态验证 :检查返回内容确认是否登录成功

改进后的错误处理示例:

// 修改 startLogin 方法
async startLogin(maxRetry = 3) {
  let attempt = 0;
  while (attempt < maxRetry) {
    try {
      // ...原有登录逻辑...
      return; // 登录成功则直接返回
    } catch (error) {
      attempt++;
      if (attempt >= maxRetry) {
        throw new Error(`登录失败,已重试 ${maxRetry} 次: ${error.message}`);
      }
      // 指数退避重试
      await new Promise(resolve => setTimeout(resolve, 1000 * 2 ** attempt));
    }
  }
}

5. 会话维持与后续操作

成功登录后,通常需要维持会话以进行后续操作。以下是几种常见的会话处理策略:

  1. Cookie 持久化 :将获取的 Cookie 保存起来供后续请求使用
  2. 请求拦截器 :使用 Axios 的拦截器自动处理会话
  3. 会话存储 :将会话信息存储在数据库或缓存中

示例实现:

class SessionManager {
  private cookies: Record<string, string> = {};

  updateFromResponse(response: AxiosResponse) {
    const setCookie = response.headers['set-cookie'];
    if (setCookie) {
      setCookie.forEach(cookie => {
        const [keyValue] = cookie.split(';');
        const [key, value] = keyValue.split('=');
        this.cookies[key] = value;
      });
    }
  }

  getCookieHeader() {
    return Object.entries(this.cookies)
      .map(([key, value]) => `${key}=${value}`)
      .join('; ');
  }
}

// 使用示例
const session = new SessionManager();
// 在登录成功后调用
session.updateFromResponse(loginResponse);

// 后续请求中添加 Cookie
axios.get('https://ssl.jxufe.edu.cn/some/protected/resource', {
  headers: {
    Cookie: session.getCookieHeader()
  }
});

6. 完整流程图与工程化建议

为了更清晰地理解整个流程,以下是登录过程的序列图表示:

用户/客户端 -> Node.js 应用: 发起登录请求
Node.js 应用 -> CAS 服务器: GET /cas/login
CAS 服务器 --> Node.js 应用: 返回登录页面(含lt)
Node.js 应用 -> Node.js 应用: 提取lt, JSESSIONID, sessionMapKey
Node.js 应用 -> Node.js 应用: RSA加密密码
Node.js 应用 -> CAS 服务器: POST /cas/login (带所有参数)
CAS 服务器 --> Node.js 应用: 返回302重定向或成功响应
Node.js 应用 -> 用户/客户端: 返回登录结果

工程化建议:

  1. 配置管理 :将公钥参数、URL 等提取到配置文件中
  2. 日志记录 :添加详细的日志记录帮助调试
  3. 单元测试 :为关键功能编写测试用例
  4. 定时更新 :定期检查 RSA 公钥是否变更
// 配置示例 (config.ts)
export default {
  cas: {
    loginUrl: 'https://ssl.jxufe.edu.cn/cas/login',
    rsaPublicKey: {
      n: '5598e3b75d21a2989274e222fa59ab07d829faa29b544e3a920c4dd287aed9302a657280c23220a35ae985ba157400e0502ce8e44570a1513bf7146f372e9c842115fb1b86def80e2ecf9f8e7a586656d12b27529f487e55052e5c31d0836b2e8c01c011bca911d983b1541f20b7466c325b4e30b4a79652470e88135113c9d9',
      e: '10001'
    }
  },
  retry: {
    maxAttempts: 3,
    delayMs: 1000
  }
};

在实际项目中,我曾遇到因 RSA 公钥变更导致登录失败的情况。后来通过添加自动检测机制,在登录失败时尝试重新获取公钥参数,显著提高了系统的稳定性。另一个常见问题是会话过期,解决方案是实现自动重新登录逻辑,当检测到会话无效时透明地重新认证。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐