网易云API Golang版开发历程

406 阅读6分钟

网易云API Golang版开发历程

原项目(node.js) 网易云音乐 API

本项目 (golang) 网易云音乐 API

api文档
请不要用于商业用途

想法的开始

事情的开始还是一开始在B站上看到了一个仿网易云网页版的VUE项目,当时挺喜欢的就fork了一下,打算继续完善这个项目就当Vue项目练手了,当时以为整个项目是有后端的,后来仔细一看发现是用了网易云音乐 API这个node项目伪造请求向网易云请求数据。后来稍微看了一下这个项目,虽然我不会用node但是好歹我也是会百度的,大概还是看出了核心代码(如何伪造请求)在哪里,感觉应该也不是太难,就打算巩固一下golang就想用golang实现一下。

解析原项目

说来丢人,看不懂node是如何接受请求的,没看到在哪定义了路由,十分疑惑(虽然并不影响我)。首先项目基本逻辑:

  • 接受客户端请求
  • 预处理:放行请求,允许跨域,拿出cookie(app.js)
  • 构造伪请求,封装必要数据(module,util/request.js)
  • 将数据进行加密,构造特定的请求参数(util/crypto.js)
  • 向网易云发送请求(util/request.js)
  • 解析返回数据,将数据返回给客户端,对于登录请求,还要写入cookie

整体的流程还是很好理解的,整个项目的重点在于util/request.jsutil/crypto.js 这两个包,一个负责发请求,一个负责加密。

构建golang项目

项目采用gin来处理路由,以singo为脚手架快速搭建web应用程序,采用asmcos/requests 发送请求。

重点代码

1.请求数据封装传递

// 邮箱登录接口为例
// 将客户端发送的请求绑定到结构体中
type LoginEmailService struct {
	Email       string `json:"email" form:"email"`
	Password    string `json:"password" form:"password"`
	Md5password string `json:"md5_password" form:"md5_password"`
}

func (service *LoginEmailService) LoginEmail(c *gin.Context) map[string]interface{} {

	// 获得客户端请求的所有cookie
	cookies := c.Request.Cookies()
    // 因为这个请求需要这个cookie 故添加一个
	cookiesOS := &http.Cookie{Name: "os", Value: "pc"}
	cookies = append(cookies, cookiesOS)

    // 构建请求参数,util.Options为请求选项的封装,对应原项目的 options
	options := &util.Options{
		Crypto:  "weapi",
		Ua:      "pc",
		Cookies: cookies,
	}
    // data为请求的body的所需原数据
	data := make(map[string]string)
	data["username"] = service.Email
	if service.Password != "" {
        // 密码进行MD5
		h := md5.New()
		h.Write([]byte(service.Password))
		data["password"] = hex.EncodeToString(h.Sum(nil))
	} else {
		data["password"] = service.Md5password
	}
	data["rememberLogin"] = "true"

	// 将数据发往request 包括 请求方法,连接,数据,请求选项 返回网易云的数据返回和set-cookie
	reBody, cookies := util.CreateRequest("POST", `https://music.163.com/weapi/login`, data, options)

	cookiesStr := ""
	
    
	for _, cookie := range cookies {
		if cookiesStr != "" {
			cookiesStr = cookiesStr + ";"
		}
		cookiesStr = cookiesStr + cookie.String()
        // 写入cookie
		c.SetCookie(cookie.Name, cookie.Value, 60*60*24, "", cookie.Domain, false, false)
	}

	reBody["cookie"] = cookiesStr

	return reBody
}

2.请求函数(大体与原项目逻辑一致)

// 定义的请求选项的结构体
type Options struct {
	Crypto  string
	Ua      string
	Cookies []*http.Cookie
	Token   string
	Url     string
}

// 创建请求
func CreateRequest(method string, url string, data map[string]string, options *Options) (map[string]interface{}, []*http.Cookie) {
    // 初始化一个请求对象(详细用法请见 github.com/asmcos/requests)
	req := requests.Requests()
    // 设置请求头
	req.Header.Set("User-Agent", chooseUserAgent(options.Ua))
	csrfToken := ""
	music_U := ""
    // 定义返回对象
	answer := map[string]interface{}{}

	if method == "POST" {
		req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
	}
	if strings.Contains(url, "music.163.com") {
		req.Header.Set("Referer", "https://music.163.com")
	}
	if options.Cookies != nil {
		for _, cookie := range options.Cookies {
            // 将cookie写入请求体中 并且获取部分cookie的值(后面会有所使用)
			req.SetCookie(cookie)
			if cookie.Name == "__csrf" {
				csrfToken = cookie.Value
			}
			if cookie.Name == "MUSIC_U" {
				music_U = cookie.Value
			}
		}
	}
    // 根据不同的请求类型进入不同的加密函数
	if options.Crypto == "weapi" {
		data["csrf_token"] = csrfToken
        // 执行加密  下同Linuxapi(linuxApiData),Eapi(options.Url, eapiData)
		data = Weapi(data)
        // 正则替换请求url(其实没什么必要,因为url是自己传递的,不过原作者这样写了我也写一下吧)
		reg, _ := regexp.Compile(`/\w*api/`)
		url = reg.ReplaceAllString(url, "/weapi/")
	} else if options.Crypto == "linuxapi" {
		linuxApiData := make(map[string]interface{}, 3)
		linuxApiData["method"] = method
		reg, _ := regexp.Compile(`/\w*api/`)
		linuxApiData["url"] = reg.ReplaceAllString(url, "/api/")
		linuxApiData["params"] = data
		data = Linuxapi(linuxApiData)
		req.Header.Set("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.90 Safari/537.36")
		url = "https://music.163.com/api/linux/forward"
	} else if options.Crypto == "eapi" {
		eapiData := make(map[string]interface{})
         // 将data的数据写入eapiData
		for key, value := range data {
			eapiData[key] = value
		}
         // 随机种子
		rand.Seed(time.Now().UnixNano())
		header := map[string]string{
			"osver":       "",
			"deviceId":    "",
			"mobilename":  "",
			"appver":      "6.1.1",
			"versioncode": "140",
			"buildver":    strconv.FormatInt(time.Now().Unix(), 10),
			"resolution":  "1920x1080",
			"os":          "android",
			"channel":     "",
			"requestId":   strconv.FormatInt(time.Now().Unix()*1000, 10) + strconv.Itoa(rand.Intn(1000)),
			"MUSIC_U":     music_U,
		}

		for key, value := range header {
             // 将header里的数据写入cookie
			req.SetCookie(&http.Cookie{Name: key, Value: value, Path: "/"})
		}
         // 将header写入eapiData
		eapiData["header"] = header
		data = Eapi(options.Url, eapiData)
		reg, _ := regexp.Compile(`/\w*api/`)         // 将header写入eapiData
		eapiData["header"] = header
		data = Eapi(options.Url, eapiData)
		reg, _ := regexp.Compile(`/\w*api/`)

		url = reg.ReplaceAllString(url, "/eapi/")
	}
	var resp *requests.Response
	var err error
	if method == "POST" {
		var form requests.Datas = data
		resp, err = req.Post(url, form)
	} else {
		resp, err = req.Get(url)
	}
	
    // 如果请求发生错误 写入错误即相应响应码
	if err != nil {
		answer["code"] = 520
		answer["err"] = err.Error()
		return answer, nil
	}
    // 获取返回的cookie
	cookies := resp.Cookies()

    // 读取返回的body
	body := resp.Content()
    // 对数据进行尝试zlib解压
	b := bytes.NewReader(body)
	var out bytes.Buffer
	r, err := zlib.NewReader(b)
	// 如果err为空,证明解压正常,覆盖body里的值
	if err == nil {
		io.Copy(&out, r)
		body = out.Bytes()
	}

    // 将json字符串转化为对象写入answer
	err = json.Unmarshal(body, &answer)
	// 出错说明不是json
	if err != nil {
		// 可能是纯页面
		if strings.Index(string(body), "<!DOCTYPE html>") != -1 {
			answer["code"] = 200
			answer["html"] = string(body)
			return answer, cookies
		}
        // 如果不是纯页面未知数据,则返回错误
		answer["code"] = 500
		answer["err"] = err.Error()
		return answer, nil
	}
    // 查询answer 有无code字段,无这写入200(避免返回值中无code字段)
	if _, ok := answer["code"]; !ok {
		answer["code"] = 200
	}
	return answer, cookies
}

3.加密函数

// 代码没啥好解释的 按照原项目的代码的逻辑进行加密,变换编码,返回map[string]string(好奇原作者是如何知道加密规则的,这也太复杂了,加密函数调试了半天)
func Weapi(data map[string]string) map[string]string {
	text, _ := json.Marshal(data)
	secretKey, reSecretKey := NewLen16Rand()
	weapiType := make(map[string]string, 2)
	weapiType["params"] = base64.StdEncoding.EncodeToString(aesEncrypt([]byte(base64.StdEncoding.EncodeToString(aesEncrypt(text, "cbc", presetKey, iv))), "cbc", reSecretKey, iv))
	weapiType["encSecKey"] = hex.EncodeToString(rsaEncrypt(secretKey, publicKey))
	return weapiType
}

func Linuxapi(data map[string]interface{}) map[string]string {
	text, _ := json.Marshal(data)
	linuxapiType := make(map[string]string, 1)
	linuxapiType["params"] = strings.ToUpper(hex.EncodeToString(aesEncrypt(text, "ecb", linuxapiKey, nil)))
	return linuxapiType
}

func Eapi(url string, data map[string]interface{}) map[string]string {
	textByte, _ := json.Marshal(data)
	fmt.Println(string(textByte))
	message := "nobody" + url + "use" + string(textByte) + "md5forencrypt"
	h := md5.New()
	h.Write([]byte(message))
	digest := hex.EncodeToString(h.Sum(nil))
	dd := url + "-36cd479b6b5-" + string(textByte) + "-36cd479b6b5-" + digest
	eapiType := make(map[string]string, 1)
	eapiType["params"] = strings.ToUpper(hex.EncodeToString(aesEncrypt([]byte(dd), "ecb", eapiKey, nil)))
	return eapiType
}

收获

站在巨人的肩膀上,看得更高更远。

重构的一个很大的难点是原项目是node.js,是动态语言,go是静态语言,所以在定义一些用了传递数据的结构的是后要考虑周全的去设计,interface{}虽然可以接受任意类型,但是类型断言也很麻烦,能不用最好不要使用。在编写中,稍微接触了一些加密算法,还有go的各种编码的变换,收获了一些东西。还有json字符串与对象的巧妙换,假如要往json字符串中添加数据,可以将json序列化到map[string]interface{}中,interface{}可以接受任意结构,再将值写入map中,再序列化成json字符串。

最后

项目还在开发中(160多个api.....),核心已经完成了,剩下的慢慢来吧,开个坑,下一个项目玩玩区块链