golang使用resty库实现模拟请求正方教务 | 青训营

385 阅读5分钟

resty库介绍

resty是Go语言中的一个流行的HTTP客户端库,主要特征有:

  • 简单的API接口,易于上手使用。
  • 支持设置请求头、查询参数、表单数据、JSON数据等。
  • 支持同步和异步两种请求方式。
  • 支持重试请求、追踪请求、设置超时、绑定请求上下文等功能。
  • 默认启用了KeepAlive,可以重用TCP连接提高效率。
  • 支持中间件,可以针对请求和响应进行自定义处理。
  • 轻量级设计,没有外部依赖。

resty通过组合基础的net/http库提供了更高层级的抽象,可以方便地发送HTTP请求,而不需要自行处理诸如连接池、请求构造等细节。它的API设计简洁易用,可以很容易地集成到Go语言的web应用或者服务中。

安装resty库

go get -u github.com/go-resty/resty/v2

基本使用方式如下:

import "github.com/go-resty/resty/v2"

client := resty.New()
resp, err := client.R().
        Get("http://example.com/api/items")

resty通过链式调用组织请求参数,表现力强而且灵活。它在Go语言的HTTP客户端库中使用广泛,是构建Go Web应用的一个不错选择。

正方教务获取SessionID

清除Cookie打开沈阳理工大学教务官网

这里我们检查一下监听到的网络请求

首先第一个请求就是获取一个未登录状态的cookie,接下来我们模拟登陆一下,看一下会发生什么请求。

登录详解

点击登陆后的第一个请求是一个防止用户用户同时登陆的登出请求,这个不会对我们的爬取过程造成影响,可以忽略。

我们来看第二个请求,其中的参数有:

  • time:query参数,一个13位时间戳
  • csrftoken:会话验证token,用于防范跨站请求。
  • language:语言,不重要
  • yhm:学号
  • mm:加密过后的密码

注意:这个上面显示有两个重复的mm,实测只写一个mm不会影响登陆验证。

如果登录成功,会相应一个302重定向,此处注意,发送请求时要设置禁用重定向,重定向后就获取不到这次请求的响应cookie了,登陆成功响应cookie的内容是验证所有接口的校验,有了这段sessionID,就可以查询教务的任意信息了。

csrftoken

这个我们直接右键检查页面源代码,搜索就可以找到了,后续可以使用正则表达式进行匹配获取。

密码加密

从上面的可以看出,密码是加密后进行验证的,那我们如何做到加密密码呢。

搜索一下mm,我们发现这个前端进行了一个rsa加密,其中有两个重要参数modulus(模数)exponent(指数)

接下来搜索modulus,找到公匙的来源,我们定位到这个请求。

这个请求携带cookie并有两个13位时间戳的参数。

总结登录过程

  1. 进入登录页面请求,获取cookie与csrftoken
  2. 通过这个token获取rsa公匙
  3. 密码加密,携带参数请求登录,拿到登陆状态的cookie

代码实现

基本的结构体对象

const baseUrl = "https://jxw.sylu.edu.cn/xtgl"

var client *resty.Client

type SyluClient struct {
	Username  string
	Client    *resty.Client
	Cookie    *http.Cookie
	Csrftoken string
	Time      string
	PublicKey PublicKeyReponse
}

type PublicKeyReponse struct {
	Modulus  string `json:"modulus"`
	Exponent string `json:"exponent"`
}

初始化

  1. 创建SyluClient结构体对象syluclient,用于保存登录会话信息。
  2. 使用resty.New()创建一个resty客户端对象client。
  3. 发送初始化请求,获取Cookie和csrftoken。
  4. 使用正则表达式提取csrftoken保存到syluclient中。
  5. 构造获取公钥的请求,包含时间戳、随机数查询参数,以及刚才获取的Cookie。
  6. 发送请求获取公钥,并解析响应保存到syluclient。
  7. 返回构造好的带有会话信息的syluclient对象。
func New() (*SyluClient, error) {

	//创建登录会话结构体
	syluclient := new(SyluClient)

	client = resty.New()

	//请求主页面,获取初始cookie与csrftoken
	initResp, err := client.R().SetHeaders(pkg.BaseHttpHeaders()).Get(baseUrl + "/login_slogin.html")
	if err != nil {
		fmt.Println("init请求失败" + err.Error())
		return nil, err
	}

	syluclient.Cookie = initResp.Cookies()[0]

	Findcsrftoken := regexp.MustCompile(`id="csrftoken" name="csrftoken" value="([^"]+)"`)
	syluclient.Csrftoken = Findcsrftoken.FindStringSubmatch(string(initResp.Body()))[1]

	//获取公匙
	time := pkg.NowTime()
	getPublicKeyResp, err := client.R().SetHeaders(pkg.BaseHttpHeaders()).
		SetQueryParams(map[string]string{
			"time": time,
			"_":    time,
		}).SetCookie(syluclient.Cookie).Get(baseUrl + "/login_getPublicKey.html")
	if err != nil {
		fmt.Println("获取公匙失败" + err.Error())
	}

	json.Unmarshal([]byte(getPublicKeyResp.String()), &syluclient.PublicKey)

	return syluclient, nil
}

获取登陆状态cookie

  1. 从参数中获取用户名和密码。
  2. 使用RSA公钥对密码进行加密,得到加密后的密码enpassword。
  3. 构造登录表单数据,包含csrftoken、用户名、加密后的密码等。
  4. 设置禁止重定向,发送登录POST请求。
  5. 根据响应结果判断登录是否成功:
  • 如果返回重定向错误,说明登录成功,则更新登录后得到的新Cookie。
  • 如果返回网络错误,返回服务器连接失败。
  • 否则登录失败,返回账号或密码错误。
  1. 保存登录状态,返回结果。
func (syluclient *SyluClient) Login(username string, password string) error {
	syluclient.Username = username
	enpassword, err := pkg.Rsa(password, syluclient.PublicKey.Modulus, syluclient.PublicKey.Exponent)
	if err != nil {
		return err
	}
	//发送登录请求,并禁止重定向
	loginresponse, err := client.SetRedirectPolicy(resty.NoRedirectPolicy()).R().SetFormData(map[string]string{
		"csrftoken": syluclient.Csrftoken,
		"language":  "zh_CN",
		"yhm":       username,
		"mm":        enpassword,
	}).SetQueryParam("time", pkg.NowTime()).SetCookie(syluclient.Cookie).SetHeaders(pkg.BaseHttpHeaders()).
		Post(baseUrl + "/login_slogin.html")

	//如果重定向了,代表登陆成功]

	if err != nil && err.Error() == "Post "/xtgl/login_slogin.html": auto redirect is disabled" {
		fmt.Println("登陆成功")
		syluclient.Cookie = loginresponse.Cookies()[1]
		fmt.Println(loginresponse.Cookies()[1].String())
		return nil
	} else if err != nil {
		return errors.New("服务器连接失败:" + err.Error())
	} else {
		return errors.New("账号或密码错误")
	}

}

工具函数

  1. BaseHttpHeaders 函数构造了一个通用的HTTP请求头,包含User-Agent、Content-Type等信息,可以设置到resty的请求中。
  2. NowTime 函数利用time包生成当前时间的毫秒级时间戳,可以作为各种请求的时间参数。
  3. Rsa 函数实现了RSA加密逻辑,主要步骤是:
  • Base64解码获得modulus和exponent
  • 构造公钥
  • 生成随机数
  • 使用公钥对密码加密
  • Base64编码并返回加密后的密码
func BaseHttpHeaders() map[string]string {
	return map[string]string {
		"User-Agent":    "Mozilla/5.0 (Windows NT 6.1; WOW64; rv:29.0) Gecko/20100101 Firefox/29.0",
		"Content-Type":  "application/x-www-form-urlencoded;charset=uft-8",
		"Cache-Control": "no-cache",
	}
}

func NowTime() string {
	// 获取当前时间的毫秒级时间戳
	timestamp := time.Now().UnixNano() / 1000000

	// 返回毫秒级时间戳
	return strconv.FormatInt(timestamp, 10)
}

func Rsa(password string, modulus string, exponent string) (string,error) {
	modulusBytes, err := base64.StdEncoding.DecodeString(modulus)
	if err != nil {
		fmt.Println("modulus err" + err.Error())
		return "",err
	}

	exponentBytes, err := base64.StdEncoding.DecodeString(exponent)
	if err != nil {
		fmt.Println("exponent err" + err.Error())
		return "",err
	}

	// 解析公钥
	pubKey := &rsa.PublicKey{
		N: new(big.Int).SetBytes(modulusBytes),
		E: int(new(big.Int).SetBytes(exponentBytes).Int64()),
	}

	// 加密密码
	bypassword := []byte(password)
	encryptedBytes, err := rsa.EncryptPKCS1v15(rand.Reader, pubKey, bypassword)
	if err != nil {
		panic(err)
	}

	// Base64 编码加密后的密码
	encryptedPassword := base64.StdEncoding.EncodeToString(encryptedBytes)

	return encryptedPassword,nil
}