网易云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{} {// 获得客户端请求的所有cookiecookies := c.Request.Cookies()// 因为这个请求需要这个cookie 故添加一个cookiesOS := &http.Cookie{Name: "os", Value: "pc"}cookies = append(cookies, cookiesOS)// 构建请求参数,util.Options为请求选项的封装,对应原项目的 optionsoptions := &util.Options{Crypto:  "weapi",Ua:      "pc",Cookies: cookies,}// data为请求的body的所需原数据data := make(map[string]string)data["username"] = service.Emailif service.Password != "" {// 密码进行MD5h := md5.New()h.Write([]byte(service.Password))data["password"] = hex.EncodeToString(h.Sum(nil))} else {data["password"] = service.Md5password}data["rememberLogin"] = "true"// 将数据发往request 包括 请求方法,连接,数据,请求选项 返回网易云的数据返回和set-cookiereBody, 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()// 写入cookiec.SetCookie(cookie.Name, cookie.Value, 60*60*24, "", cookie.Domain, false, false)}reBody["cookie"] = cookiesStrreturn reBody
}

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

// 定义的请求选项的结构体
type Options struct {Crypto  stringUa      stringCookies []*http.CookieToken   stringUrl     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"] = methodreg, _ := regexp.Compile(`/\w*api/`)linuxApiData["url"] = reg.ReplaceAllString(url, "/api/")linuxApiData["params"] = datadata = 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的数据写入eapiDatafor 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里的数据写入cookiereq.SetCookie(&http.Cookie{Name: key, Value: value, Path: "/"})}// 将header写入eapiDataeapiData["header"] = headerdata = Eapi(options.Url, eapiData)reg, _ := regexp.Compile(`/\w*api/`)url = reg.ReplaceAllString(url, "/eapi/")}var resp *requests.Responsevar err errorif method == "POST" {var form requests.Datas = dataresp, err = req.Post(url, form)} else {resp, err = req.Get(url)}// 如果请求发生错误 写入错误即相应响应码if err != nil {answer["code"] = 520answer["err"] = err.Error()return answer, nil}// 获取返回的cookiecookies := resp.Cookies()// 读取返回的bodybody := resp.Content()// 对数据进行尝试zlib解压b := bytes.NewReader(body)var out bytes.Bufferr, err := zlib.NewReader(b)// 如果err为空,证明解压正常,覆盖body里的值if err == nil {io.Copy(&out, r)body = out.Bytes()}// 将json字符串转化为对象写入answererr = json.Unmarshal(body, &answer)// 出错说明不是jsonif err != nil {// 可能是纯页面if strings.Index(string(body), "<!DOCTYPE html>") != -1 {answer["code"] = 200answer["html"] = string(body)return answer, cookies}// 如果不是纯页面未知数据,则返回错误answer["code"] = 500answer["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-" + digesteapiType := 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…),核心已经完成了,剩下的慢慢来吧,开个坑,下一个项目玩玩区块链

网易云API Golang版开发历程相关推荐

  1. 调用Nodejs版网易云API时,遇到code:-462报错解决方案

    在今年(2023)四月份,我在调用基于Nodejs版网易云API时,突然出现了下面这个报错: 很明显,网易云在调用API时需要我们进行账户验证,我很自然想到可能是需要登录.那么查阅API文档,我们可以 ...

  2. 关于NodeJS版网易云API,获取歌词对象不完整问题的解决方案

    在参考了开源项目NeteaseCloudMusicApi中调用网易云API获取歌词方式后,我发现其提供的API链接存在获取对象不完整的问题,如下图所示: 下图是开源项目中部署的路由代码: 在经过自己查 ...

  3. 音乐无界限,听见好时光—网易云音乐Linux版震撼来袭!

    为了带来更好的音乐体验,实现对音乐高品质的追求,经过网易云音乐与深度科技团队长达半年多的联合开发,大家期待已久的网易云音乐正式登陆 Linux 平台! 网易云音乐是一款专注于发现与分享的音乐产品,依托 ...

  4. 在网易云音乐网页版上加下载按键进行下载歌曲

    源由 原理 代码解决 思路 Ajax 请求函数 获取 id 和歌名 点击下载 利用 a 标签 利用 audio 标签 更改歌曲名 a 标签的 download 属性 利用 Ajax 请求歌曲内容 利用 ...

  5. 网易云音乐Android版使用的开源组件

    转自:http://www.jianshu.com/p/f31ab96a32f3 网易云音乐Android版从第一版使用到现在,全新的 Material Design 界面,更加清新.简洁.同样也是音 ...

  6. 网易云音乐电脑版怎么下载电台节目 主播电台节目下载教程

    网易云音乐不仅可以听歌,还可以在主播电台中,听到各类主播的声音,下面我们就来讲讲网易云音乐电脑版怎么下载电台节目,一起来看教程吧! 网易云音乐电脑版怎么下载电台节目 主播电台节目下载教程 网易云音乐P ...

  7. 网易云api及 asrsea 加密参数文档

    网易云api及 asrsea 加密参数文档 detail /weapi/v3/song/detail?csrf_token=bd0c8c8504a92cd653d53a7dd1c01ba4 " ...

  8. 网易云音乐刷听歌量_网易云音乐极速版悄然上线!听歌体验同之前没有差别

    了解更多热门资讯.玩机技巧.数码评测.科普深扒,点击右上角关注我们 ---------------------------------- 7月2日消息,"网易云音乐极速版"App在 ...

  9. [转]网易云音乐Android版使用的开源组件

    原文链接 网易云音乐Android版从第一版使用到现在,全新的 Material Design 界面,更加清新.简洁.同样也是音乐播放器开发者,我们确实需要思考,相同的功能,会如何选择.感谢开源,让我 ...

最新文章

  1. matlab最大化函数,求助,最大化一个函数
  2. linux下常用压缩格式的压缩与解压方法
  3. 证明是一个偶像,数学家在这个偶像前折磨自己!
  4. docker 创建容器报: Error response from daemon: C: drive is not shared.
  5. 在GDI+中如何实现以左下角为原点的笛卡尔坐标系
  6. python string.format()_python string format
  7. 1. 少了一个PermMissingElem Find the missing element in a given permutation.
  8. VS、C#配置R语言开发环境
  9. webpack 分离css html,【已解决】ReactJS中Webpack打包时分离css
  10. Android—Gradle教程(一)
  11. Logistic回归模型C统计量及95%可信区间计算
  12. 网站为什么要做外链?
  13. 琵琶行 (白居易诗作)
  14. 腾讯微云和坚果云哪个好用?
  15. html5团队特效,CSS3团队成员介绍卡片特效
  16. PPT文档中如何插入超链接
  17. Android Studio 连接夜神模拟器的方法
  18. python爬取微博评论点赞数_Python selenium爬取微博数据代码实例
  19. 解读 Java 并发队列 BlockingQueue
  20. 2019年Apache Spark技术交流社区原创文章回顾

热门文章

  1. snippet编写学习
  2. 计算机网络结课答辩,计算机网络技术结课论文.doc
  3. POJ 3323 搜索
  4. 给定一组相关对应的数值,通过for循环使用MATLAB利用一次函数计算出中间的其他数值大小
  5. 水漫金山:OpenCV漫水填充算法(Floodfill)
  6. Python模块---海龟(turtle)
  7. 基于MATLAB的无人机遥感数据预处理与农林植被性状估算
  8. Swap 2 Secrets via Homomorphic Properties of Shamir Secret Sharing
  9. android 标题栏,状态栏和导航栏的区别
  10. UE4官方滚球项目源码笔记