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位时间戳的参数。
总结登录过程
- 进入登录页面请求,获取cookie与csrftoken
- 通过这个token获取rsa公匙
- 密码加密,携带参数请求登录,拿到登陆状态的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"`
}
初始化
- 创建SyluClient结构体对象syluclient,用于保存登录会话信息。
- 使用resty.New()创建一个resty客户端对象client。
- 发送初始化请求,获取Cookie和csrftoken。
- 使用正则表达式提取csrftoken保存到syluclient中。
- 构造获取公钥的请求,包含时间戳、随机数查询参数,以及刚才获取的Cookie。
- 发送请求获取公钥,并解析响应保存到syluclient。
- 返回构造好的带有会话信息的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
- 从参数中获取用户名和密码。
- 使用RSA公钥对密码进行加密,得到加密后的密码enpassword。
- 构造登录表单数据,包含csrftoken、用户名、加密后的密码等。
- 设置禁止重定向,发送登录POST请求。
- 根据响应结果判断登录是否成功:
- 如果返回重定向错误,说明登录成功,则更新登录后得到的新Cookie。
- 如果返回网络错误,返回服务器连接失败。
- 否则登录失败,返回账号或密码错误。
- 保存登录状态,返回结果。
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("账号或密码错误")
}
}
工具函数
- BaseHttpHeaders 函数构造了一个通用的HTTP请求头,包含User-Agent、Content-Type等信息,可以设置到resty的请求中。
- NowTime 函数利用time包生成当前时间的毫秒级时间戳,可以作为各种请求的时间参数。
- 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
}