C# 网络编程之豆瓣OAuth2.0认证详解和遇到的各种问题及解决
最近在帮人弄一个豆瓣API应用,在豆瓣的OAuth2.0认证过程中遇到了各种问题,同时自己需要一个个的尝试与解决,最终完成了豆瓣API的访问.作者这里就不再吐槽豆瓣的认证文档了,毕竟人家也不容易.但是作者发现关于豆瓣OAuth认证过程的文章非常之少,所以想详细写这样一篇文章方便后面要做同样东西的人阅读.希望文章对大家有所帮助,尤其是想做豆瓣API开发的初学者.
(文章中蓝色字表示官方文档引用,红色字是可能遇到问题及注意,黑色字是作者叙述)
一.误区OAuth1.0认证过程
你遇到的第一个问题可能就是还在阅读"豆瓣
API OAuth认证"这篇官方文档,并且在疯狂的尝试.我就是这样花费了很长时间研究了OAuth1.0认证过程,但总是错误,因为他已经过时了.你需要使用OAuth2.0完成认证过程.阅读"使用OAuth2.0访问豆瓣API"官方文档.
但是OAuth1.0提供的思想还是可以的(安慰自己),而且很多关于豆瓣博客文档都是基于1.0的认证过程,其实已经过时.这里也简单讲讲它的基本过程和原理:(OAuth原理和过去Auth1.0认证成果案例)
它主要是通过Google-OAuth项目提供的C#语言的OAuth库,在自定义OAuth类中有计算签名值oauth_signature方法,签名方法HMAC-SHA1,还有计算时间戳oauth_timestamp等方法,然后在参照DoubanOAuthBasicSample项目完成它的认证流程,主要方法是getRequestToken、authorization和getAccessToken.
豆瓣OAuth1.0官方文档给出的具体四个步骤:获取未授权的Request Token、请求用户授权Request Token、使用授权后的Request Token换取Access Token、使用 Access Token 访问或修改受保护资源.具体豆瓣 OAuth1.0认证代码:OAuth类和DoubanOAuthBasicSample
其实,你只要知道它已经过时,不要在使用该方法即可.下面才是我想讲述的具体如何通过C#程序完成豆瓣的OAuth认证并访问需要授权的数据.
二.获取autorization_code
首先在你需要参考的具体豆瓣官方文档就是:http://developers.douban.com/wiki/?title=oauth2
使用OAuth2.0流程具体如下:(官方文档)
1.应用向豆瓣请求授权
2.豆瓣为用户显示一个授权页面,用户在此页面确认是否同意授权
3.如果用户同意授权,应用汇获取到一个访问令牌(access_token),通过此令牌用户可以访问授权数据
4.如果访问需要授权的Api,请使用https协议,加上access_token的Header.
下面详细讲解
首先你需要申请API Key,在完成申请过程中你需要注意3个值:API Key\Secret\回调地址.后面的程序需要应用,当使用的时候我会详细介绍.由于我是桌面客户端应用,授权流程为native-application flow.
根据它的具体流程我设计的界面如下图所示:
获取authorization_code步骤(官方文档)
通过在浏览器中访问下面地址,来引导用户授权并获取authorization_code. https://www.douban.com/service/auth2/auth
参数:
client_id 必选参数,应用的唯一标识,对应于APIKey
redirect_uri 必选参数,用户授权完成后的回调地址,通过回调地址获得用户的授权结果.与注册一致.
response_type 必选参数,此值可以为code或者token.在本流程中此值为code
scope 可选参数,申请权限的范围,如果不填,则使用缺省的scope.如果申请多个scope,使用逗号分隔
state 可选参数,用来维护请求和回调状态的附加字符串,授权完成回调时会附加此参数,应用根据此字符串来判断上下文关系. 注意:此请求必须是HTTP GET方式 返回结果:
当用户拒绝授权时,浏览器会重定向到redirect_uri,并附加错误信息 https://www.example.com/back?error=access_denied
当用户同意授权时,浏览器会重定向到redirect_uri,并附加autorization_code https://www.example.com/back?code=9b73a4248 首先,你需要定义变量如下,这些变量都是我们需要使用的.其中APIkey\secret\回调地址填写你自己的,下面的被我改过不是源数据(担心丢失)
#region 定义变量
//申请的API Key
public string apiKey = "00489f145c2576bd00d9dd3d147064";
//申请的API 密钥
public string apiKeySecret = "72c36131ace8ea";
//申请的回调地址 我的应用URL
public string myurl = "http://www.baidu.com/";
//获取authorization_code
public Uri GetAuthorizationCode = new Uri("https://www.douban.com/service/auth2/auth");
//获取access_token
public Uri GetAccessToken = new Uri("https://www.douban.com/service/auth2/token");
//浏览器返回autorization_code
public string autorizationCode = "";
//AccessToken授权成功后返回的Json数据
public string accessToken = "";
public string userName = "";
public string userId = "";
public string expiresIn = "";
public string refreshToken = "";
#endregion
然后,点击button1(浏览)按钮,同时设计视图中为webBrowser1控件添加DocumentCompleted事件(右键->属性->'闪电图标'事件).同时引用命名空间:using System.Web;\using System.Net;\using System.IO;
#region 第一步 浏览 获取authorization_code
//点击"浏览"显示豆瓣登录界面
private void button1_Click(object sender, EventArgs e)
{
//获取authorization_code构造URL
StringBuilder url = new StringBuilder(GetAuthorizationCode.ToString()); //可变字符串
//追加组合格式字符串
url.AppendFormat("?client_id={0}&", apiKey);
url.AppendFormat("redirect_uri={0}&", myurl);
url.AppendFormat("response_type={0}", "code");
//url.AppendFormat("scope={0}", "scope=shuo_basic_r,shuo_basic_w");
//显示输入URL
textBox1.Text = url.ToString();
string Input = url.ToString();
//将指定URL加载到WebBrowser控件中
webBrowser1.Navigate(Input);
}
//文件加载完成后发生事件
private void webBrowser1_DocumentCompleted(object sender, WebBrowserDocumentCompletedEventArgs e)
{
//获取当前文档URL
string reUrl = this.webBrowser1.Url.ToString();
textBox1.Text = reUrl;
//获取https://www.example.com/back?code=9b73a4248中code值
if (!string.IsNullOrEmpty(reUrl))
{
//获取问号后面字符串
string LastUrl = reUrl.Substring(reUrl.LastIndexOf("?") + 1, (reUrl.Length - reUrl.LastIndexOf("?") - 1));
//根据参数间的&号获取参数数组 可以获取多个参数此时只需一个code
string[] urlParam = LastUrl.Split('&');
foreach (string s in urlParam)
{
//将参数名与参数值赋值给数组 value[0]参数名称 value[1]参数值
string[] value = s.Split('=');
//MessageBox.Show("参数名称为:" + value[0] + " 参数值为:" + value[1]);
if (value[0] == "code")
{
autorizationCode = value[1];
}
}
}
//输出获取code值
if (autorizationCode != "") MessageBox.Show(autorizationCode);
}
#endregion
运行结果如下图所示,填写完用户邮箱和密码,点击授权后获取autorization_code如下:
这里你需要注意的有一下几点:
1.为什么我的回调地址设置为"http://www.baidu.com/"?
最初我设置的回调地址为自定义的一个网址,但是输入邮箱和密码后点击授权,总是HTTP 400错误,啥都不能获取.使用IE浏览器也是一样,但是google能获取.后来经过同学提醒(他做腾讯API,资料多,官方文档规范)可能回调地址需要能访问的网址,所以改成了百度,并且成功获取.因为这一步只需要获取code值,网址并不关键.(可能也有没考虑到的地方)
2.你需要知道该程序中必须使用GET方法=浏览可以直接访问.(除设置header,后面讲述)
在程序中我是直接调用webBrowser1.Navigate(Input);函数访问的,也可以使用GET方法访问.GET和POST的区别是你需要知道的,简单来说就是GET后面给的网址参数显示,而POST是隐式的.
程序和文档中说道的采用GET的方法,并带有参数的都是这样连接的.url+?+参数=参数值&参数=参数值...正如获取code中的URL,它通过浏览器是可以直接访问的.
https://www.douban.com/service/auth2/auth?client_id=00489f145c2576bd00d9dd3d147064&redirect_uri=http://www.baidu.com/&response_type=code 3.你可能会遇到的错误是113 缺少参数required_parameter_is_missing.
此时你在设置url时,通过StringBuilder可变字符串增加参数时,需要把所有的必须的参数填写.同时,注意你的&和?是否正确填写.你可以参考的官方文档错误报告如下: http://developers.douban.com/wiki/?title=oauth2 http://developers.douban.com/wiki/?title=api_v2 此时,你的第一步获取code已经完成,接下来是获取access_token的过程.
三.获取access_token
官方文档接受的获取access_token如下: https://www.douban.com/service/auth2/token 参数: client_id 必选参数,应用的唯一标识,对应于APIKey
client_sercet 必选参数,应用的唯一标识,对应于豆瓣secret
redirect_url 必选参数,用户授权后的回调地址
grant_type 必选参数,此值可为authorization_code或者refresh_token,此时为authorization_code
code 必选参数,上一步中获取的authorization_code
注意:此请求必须是HTTP POST方式 返回结果为Json格式数据如下图所示
//获取的数据装换为Json格式 此时返回的json格式的数据
{
"access_token":"0e63c03dfb66c4172b2b40b9f2344c",
"douban_user_name":"Eastmount",
"douban_user_id":"57279898",
"expires_in":604800,
"refresh_token":"84406d40cc58e0ae8cc147c2650aa2",
}
其中你需要注意的是四个地方:
1.如何使用POST方法发送请求获取应答,这再也不是通过浏览器就能直接访问的问题.
2.获取的JSON格式数据如何通过C#解析.
3.可能你的访问不成功,你需要在"我的应用"中添加测试用户.看看自己的是否添加.
4.你可能遇到101 错误的请求方法,invalid_request_method: GET因为你需要采用POST方法. 我讲解如何解析JSON格式的数据,这里参照很多人可能都使用过的方法.
你需要下载Newtonsoft.Json.dll文件并添加引用它,参考百度"C#使用json字符串"方法,最好先把该dll放置到C:\WINDOWS\Microsoft.NET\Framework文件夹中,里面有很多dll(我的程序是VS2012
.NET4.5).
在"解决方案"中鼠标右键引用->添加引用->浏览->添加该dll.然后添加命名空间using Newtonsoft.Json.Linq;将POST方法获取应答string转换为JSON格式转换赋值即可.具体代码如下,同时POST方法发送HTTP请求我是通过自定义函数sendMessage实现的.加载button2(授权)按钮事件.
#region 第二步 授权 获取access_token
private void button2_Click(object sender, EventArgs e)
{
//显示输入URL
textBox1.Text = GetAccessToken.ToString();
//获取access_token构造POST参数
StringBuilder url = new StringBuilder(""); // 可变字符串
//追加组合格式字符串
url.AppendFormat("client_id={0}&", apiKey);
url.AppendFormat("client_secret={0}&", apiKeySecret);
url.AppendFormat("redirect_uri={0}&", myurl);
url.AppendFormat("grant_type={0}&", "authorization_code");
url.AppendFormat("code={0}", autorizationCode);
//获取POST提交数据返回内容
string AccessContent = sendMessage(GetAccessToken.ToString(), url.ToString());
webBrowser1.DocumentText = AccessContent;
//获取的数据装换为Json格式 此时返回的json格式的数据
JObject obj = JObject.Parse(AccessContent);
accessToken = (string)obj["access_token"]; //access_token
userName = (string)obj["douban_user_name"]; //豆瓣用户名
userId = (string)obj["douban_user_id"]; //用户id
expiresIn = (string)obj["expires_in"]; //生命周期 604800秒=7天
refreshToken = (string)obj["refresh_token"]; //刷新令牌
MessageBox.Show(accessToken + "\n" + userName + "\n" +
userId + "\n" + expiresIn + "\n" + refreshToken);
}
//发送消息Post方法
public static string sendMessage(string strUrl, string PostStr)
{
try
{
//设置消息头
CookieContainer objCookieContainer = null;
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(strUrl);
request.Method = "Post";
request.ContentType = "application/x-www-form-urlencoded";
request.Referer = strUrl;
if (objCookieContainer == null)
objCookieContainer = new CookieContainer();
request.CookieContainer = objCookieContainer;
byte[] byteData = Encoding.UTF8.GetBytes(PostStr.ToString().TrimEnd('&'));
request.ContentLength = byteData.Length;
using (Stream reqStream = request.GetRequestStream())
{
reqStream.Write(byteData, 0, byteData.Length);
}
//Response应答流获取数据
string strResponse = "";
using (HttpWebResponse res = (HttpWebResponse)request.GetResponse())
{
objCookieContainer = request.CookieContainer;
using (Stream resStream = res.GetResponseStream())
{
using (StreamReader sr = new StreamReader(resStream, Encoding.UTF8)) //UTF8
{
strResponse = sr.ReadToEnd();
}
}
// res.Close();
}
return strResponse;
}
catch (Exception e)
{
Console.WriteLine(e.ToString());
Console.Read();
}
return null;
}
#endregion
运行程序,点击"浏览"授权成果后在点击"授权"的运行结果如下图所示:
四.使用access_token
通过前面的步骤我们已经获取了access_token数据,那么怎样使用它呢?当我看到豆瓣给出的使用官方文档如下时:
curl "https://api.douban.com/v2/user/~me"
-H "Authorization: Bearer a14afef0f66fcffce3e0fcd2e34f6ff4"
我很头痛啊!那么,怎样通过C#实现使用access_token访问要授权的数据呢? GET https://api.douban.com/v2/user/~me
该URL是获取当前授权用户信息的,需要必须先进行API认证授权,返回的是当前授权用户信息.因此,使用它完全可以验证授权成果后的操作.具体代码如下:
#region 第三步 豆瓣访问授权数据
private void button3_Click(object sender, EventArgs e)
{
try
{
//获取当前授权用户信息 需要必须先进行API认证授权,返回当前授权的UserInfo
string Input = "https://api.douban.com/v2/user/~me";
textBox1.Text = Input;
//HttpWebRequest对象实例:该类用于获取和操作HTTP请求
var request = (HttpWebRequest)WebRequest.Create(Input); //Create:创建WebRequest对象
//设置请求方法为GET
request.Headers.Add("Authorization", "Bearer " + accessToken);
request.Method = "GET";
//HttpWebResponse对象实例:该类用于获取和操作HTTP应答
var response = (HttpWebResponse)request.GetResponse(); //GetResponse:获取答复
//构造数据流对象实例
Stream stream = response.GetResponseStream(); //GetResponseStream:获取应答流
StreamReader sr = new StreamReader(stream); //从字节流中读取字符
//从流当前位置读取到末尾并显示在WebBrower控件中
string content = sr.ReadToEnd();
webBrowser1.DocumentText = content;
//关闭响应流
stream.Close();
sr.Close();
response.Close();
}
catch (Exception msg) //异常处理
{
MessageBox.Show(msg.Message);
}
}
#endregion
运行结果如下图所示:
不要以为这简单的几句代码就很容易实现了访问数据,其实你需要注意一下几点: 1.你可能会遇到1000错误 需要权限need_permission?
那时你需要在访问时设置header,添加-H "Authorization: Bearer a14afef0f66fcffce3e0fcd2e34f6ff4".采用C#设置标题头的具体代码如下:
request.Headers.Add("Authorization", "Bearer " + accessToken);
其中你需要注意Bearer后面的空格.而且有同学说他在使用网盘认证时,获取的方法有两种,一种是设置header,一种是在URL后面加上?access_token=值即可.但我测试了下豆瓣只有设置header可以.
2.认证过程已经详细叙述了,增删改查其他数据影片信息、用户信息、评论、收藏、关注等方法都类似,感兴趣的可以自己完成.它的具体设置参数参照豆瓣文档:http://developers.douban.com/wiki/?title=api_v2
五.总结
最后总结下它的具体步骤,其实它就是按照豆瓣的文档完成的,三个步骤:获取authorization_code、获取access_token和使用access_token.你也不能说豆瓣文档讲得不好,其实实质东西它都讲述清除了.需要的只是你自己的探索,可能刚接触比较新鲜和难,但其实完成后就发现它很简单. 希望该文章对大家有所帮助,尤其是想做豆瓣API开发的并且使用C#的,这方面资料比较少,基本都是php和java的.更希望同学能从该文章中学到一下几个东西:
1.如何通过官方文档完成一个东西,可能遇到的问题都需要自己解决,而不是复制粘贴.
2.如何使用C#网络编程GET和POST两种方法HTTP请求并获取应答.
3.如何使用C#解析Json格式的数据.
4.如何使用OAuth认证API开发,这方面腾讯、新浪等比较完善.
最后也感谢豆瓣网带给我很多知识,推荐程序员也看看文学书籍,如《文学回忆录》和《季羡林 清华园日记》,生活不只有编程啊.如果有错误或不足之处,还请海涵. 源代码免费下载地址:http://download.csdn.net/detail/eastmount/7399075
(By:Eastmount 2014-5-25 下午6点 原创CSDNhttp://blog.csdn.net/eastmount/)
微信小程序豆瓣电影项目的改造过程经验分享
在学习微信小程序开发过程中,一部分的难点是前端逻辑的处理,也就是对前端JS的代码编辑;一部分的难点是前端界面的设计展示;本篇随笔基于一个豆瓣电影接口的小程序开源项目进行重新调整,把其中遇到的相关难点和改进的地方进行讨论介绍,希望给大家提供一个参考的思路,本篇随笔是基于前人小程序的项目基础上进行的改进,因此在开篇之前首先对原作者的辛劳致敬及感谢。
1、豆瓣电影接口的小程序项目情况
豆瓣电影接口提供了很多相关的接口给我们使用,豆瓣电影接口的API地址如下所示:https://developers.douban.com/wiki/?title=movie_v2
在GitHub的开源库里面,可以搜索到很多关于豆瓣电影接口的小程序,我本篇随笔是基于 weapp-douban-movie 这个小程序进行的改造处理,后来找到了原作者的项目地址:wechat-weapp-movie,原作者对版本做了一次升级,后来我对照我的调整和作者最新版本的源码,发现有些地方改造的思路有些类似,如对于URL地址外放到统一的配置文件中的处理,不过还是有很多地方改造不同。
本篇随笔的改造方案是基于小程序项目 weapp-douban-movie 的,因此对比的代码也是和这个进行比较,不知道这个版本是不是原作者的旧版本,不过这个版本对文件目录的区分已经显得非常干净利落了,对电影信息的展示也统一到了模板里面,进行多次的重复利用,整体的布局和代码都做的比较好,看得出是花了不少功夫进行整理优化的了。
小程序主界面效果如下所示:
小程序源码目录结构如下所示:
不过每个人都有不同的经验和看法,对于开发小程序来说,我侧重于使用配置文件减少硬编码的常量,使用Promise来优化JS代码的使用,将获取和提交JSON数据的方法封装到辅助类,以及使用地理位置接口动态获取当前城市名称和坐标等等。
本篇随笔下面的部分就是介绍使用这些内容进行代码优化的处理过程。
1、使用配置文件定义常量内容
我们在使用任何代码开发程序的时候,我们都是非常注意一些变量或常量的使用,如果能够统一定义那就统一定义好了,这种在小程序的JS代码里面也是一样,我们尽可能抽取一些如URL,固定参数等信息到独立的配置文件中,这样在JS代码引入文件,使用变量来代替
例如原来的config.js文件里面,只是定义了一个地址和页面数量的大小常量,如下所示
module.exports = {
city: '杭州',
count: 20
}
原来的小程序代码在获取待映的电影内容时候,部分源码如下所示
其他页面JS代码也和这个类似,头部依旧有很多类似这样URL地址,这个是我希望统一到config.js文件的地方,另外这个调用的函数是使用回调函数的处理方式,如下所示。
douban.fetchFilms.call(that, url, config.city, that.data.start, config.count)
其实我认为这里面既然是定义的外部函数,那么这里面的url, city, config.city, config.cout都不需要这里,在封装函数内部使用这些常量即可,因此可以对他们进行改造,如下我们统一抽取各个文件里面的URL,以及一些常见变量到config.js里面。
下面代码是我优化整理后的配置参数信息。
module.exports = {
city: '',
location:'0,0',
count: 20,
coming_soon_url: 'https://api.douban.com/v2/movie/coming_soon',
in_theaters_url: 'https://api.douban.com/v2/movie/in_theaters',
top_url: 'https://api.douban.com/v2/movie/top250',
search_url: 'https://api.douban.com/v2/movie/search?tag=',
detail_url: 'https://api.douban.com/v2/movie/subject/', //?id=
celebrity_url: 'https://api.douban.com/v2/movie/celebrity/',
baidu_ak:'6473aa8cbc349933ed841467bf45e46b',
baidu_movie:'https://api.map.baidu.com/telematics/v3/movie',
hotKeyword: ['功夫熊猫', '烈日灼心', '摆渡人', '长城', '我不是潘金莲', '这个杀手不太冷', '驴得水', '海贼王之黄金城', '西游伏妖片', '我在故宫修文物', '你的名字'],
hotTag: ['动作', '喜剧', '爱情', '悬疑'],
}
上面的配置文件config.js里面,我统一抽取了各个页面的URL地址、关键词和标签(hotKeyword和hotTag)、城市及地址(city和location后面动态获取)、页面数量count等参数信息。
另外由于部分参数统一通过config.js获取,就不需要再次在调用的时候传入了,因此简化调用代码的参数传入,如下代码所示。
douban.fetchComming(that, that.data.start)
对于原先的代码
douban.fetchFilms.call(that, url, config.city, that.data.start, config.count)
简化的虽然不多,但是尽可能的保持干净简单的接口是我们的目标,而且这里把常规的URL等参数提取到函数里面,更加符合我们编码的习惯。
这里定义的douban.fetchComming(that, that.data.start) 使用了Promise来简化代码,传入的that参数是因为需要在函数体里面设置该页面里面的Data等处理。
关于Promise的相关处理,我们在下面进行介绍。
2、使用Promise来优化JS代码
关于Promise的好处和如何使用Promise插件介绍,我在随笔《在微信小程序的JS脚本中使用Promise来优化函数处理》中已有介绍,我很喜欢使用这种Promise的风格代码,而且可以定义一些常用的辅助类来提高代码的重用。在我参考的这个豆瓣电影小程序还是使用常规回调的函数,对比原作者最新版本的 wechat-weapp-movie 小程序,也依旧使用回调函数模式来处理,有点奇怪为什么不引入Promise插件来开发。
原来的小程序,电影接口的相关处理,统一在fetch.js里面进行处理,这里封装对各种豆瓣API接口的调用。
这里我们来看看原来程序没有采用Promise的回调函数处理代码
var config = require('./config.js')
var message = require('../../component/message/message')
module.exports = {
fetchFilms: function(url, city, start, count, cb) {
var that = this
if (that.data.hasMore) {
wx.request({
url: url,
data: {
city: config.city,
start: start,
count: count
},
method: 'GET',
header: {
"Content-Type": "application/json,application/json"
},
success: function(res){
if(res.data.subjects.length === 0){
that.setData({
hasMore: false,
})
}else{
that.setData({
films: that.data.films.concat(res.data.subjects),
start: that.data.start + res.data.subjects.length,
showLoading: false
})
}
wx.stopPullDownRefresh()
typeof cb == 'function' && cb(res.data)
},
fail: function() {
that.setData({
showLoading: false
})
message.show.call(that,{
content: '网络开小差了',
icon: 'warning',
duration: 3000
})
}
})
}
},
这个函数是一个通用的函数,用来获取待映、热映、top250口碑的记录信息,不过它把参数抛给调用者传入,因此显得调用比较复杂一些,我们经过使用Promise优化代码处理,并对接口的参数进行简化,代码改造如下所示。
var config = require('./config.js')
var message = require('../../component/message/message')
var app = getApp()//获取应用实例
module.exports = {
//待映
fetchComming : function(page, start) {
return this.fetchFilms(page, config.coming_soon_url, config.city, start, config.count);
},
//热映
fetchPopular : function(page, start) {
return this.fetchFilms(page, config.in_theaters_url, config.city, start, config.count);
},
//top250口碑
fetchTop : function(page, start) {
return this.fetchFilms(page, config.top_url, config.city, start, config.count);
},
//通用的热映、待映的获取方式
fetchFilms: function(page, url, city, start, count) {
return new Promise((resolve, reject) => {
var that = page;
var json = {city: city, start: start, count: count };
var type = "json";//特殊设置,默认是application/json
if (that.data.hasMore) {
app.utils.get(url, json, type).then(res => {
if(res.subjects.length === 0){
that.setData({
hasMore: false,
})
}else{
that.setData({
films: that.data.films.concat(res.subjects),
start: that.data.start + res.subjects.length,
showLoading: false
})
}
wx.stopPullDownRefresh();
resolve(res);
})
}
})
},
最终的请求接口参数只有两个,一个是页面对象,一个是请求的起始位置,如下代码所示
function(page, start)
另外我们使用了代码
app.utils.get(url, json, type)
来对wx.request方法的统一封装,直接使用工具类里面的方法即可获取结果,不需要反复的、臃肿的处理代码。这就是我们使用Promise来优化JS,并抽取常用代码到工具类里面的做法。
我们再来对比一下获取电影详细信息的接口函数封装,原来代码如下所示。
fetchFilmDetail: function(url, id, cb) {
var that = this;
wx.request({
url: url + id,
method: 'GET',
header: {
"Content-Type": "application/json,application/json"
},
success: function(res){
that.setData({
filmDetail: res.data,
showLoading: false,
showContent: true
})
wx.setNavigationBarTitle({
title: res.data.title
})
wx.stopPullDownRefresh()
typeof cb == 'function' && cb(res.data)
},
fail: function() {
that.setData({
showLoading: false
})
message.show.call(that,{
content: '网络开小差了',
icon: 'warning',
duration: 3000
})
}
})
},
我改造后的函数代码如下所示。
//获取电影详细信息
fetchFilmDetail: function(page, id) {
return new Promise((resolve, reject) => {
var that = page;
var url = config.detail_url + id;
var type = "json";//特殊设置,默认是application/json
app.utils.get(url, {}, type).then(res => {
that.setData({
filmDetail: res,
showLoading: false,
showContent: true
});
wx.setNavigationBarTitle({
title: res.title
});
wx.stopPullDownRefresh();
resolve(res);
});
})
},
通过对fetch.js函数代码的改造处理,可以看到调用的JS代码参数减少了很多,而且页面也不用保留那么多连接等参数常量信息了。
onLoad: function() {
var that = this
douban.fetchComming(that, that.data.start)
},
3、使用地理位置接口动态获取当前城市名称和坐标
原来程序使用硬编码的方式设置当前城市,如下脚本所示
module.exports = {
city: '杭州',
count: 20
}
不过我们不同地方的人员使用的时候,这个城市名称肯定需要变化的,因此可以使用微信的地理位置接口动态获取当前位置信息,然后写入到配置文件里面即可。
//获取当前位置信息
function getLocation (type) {
return new Promise((resolve, reject) => {
wx.getLocation({ type: type, success: resolve, fail: reject })
})
}
//根据坐标获取城市名称
function getCityName (latitude = 39.90403, longitude = 116.407526) {
var data = { location: `${latitude},${longitude}`, output: 'json', ak: '6473aa8cbc349933ed841467bf45e46b' };
var url = 'https://api.map.baidu.com/' + 'geocoder/v2/';
var type = 'json';
return this.get(url, data, type).then(res => res.result.addressComponent.city);
}
然后我们在app.js里面编写代码,在app启动的时候,动态获取城市名称、坐标信息然后写入配置文件即可,这里使用的还是Promise的函数调用实现。
const utils = require('./comm/script/util.js')
const config = require('./comm/script/config.js')
App({
onLaunch: function() {
utils.getLocation()
.then(res=>{
const { latitude, longitude } = res;
config.location = `${longitude},${latitude}`;//当前坐标
console.log(`currentLocation : ${config.location}`);
return utils.getCityName(latitude, longitude)
})
.then(name=>{
config.city = name.replace('市', ''); //当前城市名称
console.log(`currentCity : ${config.city}`)
})
.catch(err => {
config.city = '广州'
console.error(err)
})
},
...
最后呈上改造过代码的运行界面,还是保留原来的功能正常使用。
本文转自博客园伍华聪的博客,原文链接:微信小程序豆瓣电影项目的改造过程经验分享,如需转载请自行联系原博主。
小程序开发-第二章第四节小程序http请求与请求本地json文件-全栈工程师之路-中级篇
上一节课,我们已经基本完成了,首页的界面编写。逻辑暂时不理会。但是我们用于展示页面绑定的是我们本地的假数据。接下来我们就来获取真正的数据来展示页面。请跳过划线部分,我只是不想删除而已。绑定假数据编写页面算是前段最早做的一件事情吧。特别是在前后端同步开发,联合测试的时候。服务端会先输出接口文档,然后前后端根据接口文档同步开发,最后联合测试。这就需要前段本地编写大量的假数据。如果我们每个页面的假数据的单独编写的话,后续和服务端联调,我们就需要更改大量的文件。且修改完链接到服务器,然后又加了一个需求或者服务器挂了,这时候你要本地调试,就会变得很麻烦,你要把你修改的地方再改回来。这样的操作繁琐而且容易出错。这时候我们就可以思考一下,是不是有什么办法,构建一个本地的假数据服务。这里只说一个我最常用的吧,使用http请求本地的json文件,来获取数据。本地的json文件,根据接口文档创建同样的层级目录,联调的时候只要在请求地址的最前端加入服务器地址就可以了。有两个缺点:一、这样的请求方式,不支持携带参数的请求,就是不管你写的是什么条件,最终获得的都是本地的json文件,原样输出。二、访问本地的文件需要些文件后缀名,如data/data.json.有些服务器请求地址是data/data。后续删除也是一个麻烦的问题。根据上面的思路,我们要做几件事情。1、获取接口文档2、统一管理服务器地址3、创建本地假数据文件4、编写http请求本地json文件5、修改服务器地址访问真正的服务器6、调试修改。wx.request不支持请求本地json(写了半天发现微信不支持,这才叫瞎比比),这部分就不讲了,直接讲真实数据了。软件开发基本上就是这么一个流程,发现问题,归纳问题,提出解决方案,编写程序,最后测试验证方案准确。1、首先我在很早的时候就说过了,我们这次使用豆瓣电影的公开API文档https://developers.douban.com/wiki/?title=api_v2真是的请求地址是如https://api.douban.com/v2/movie/in_theaters?count=3(直接浏览器访问就能请求到数据哦!)2、我们在util.js文件中,增加服务器的统一地址变量。其实更好的做法是,编写一个url处理方法。这样如果后续需要统一的追加参数,或者其他的统一操作,可以直接在这里修改。3、编写http请求。我们先请求正在上映。在小程序中我们使用wx.request发送http请求。我们在index.js中编写url:请求地址,这里我们调用factory方法处理了method:请求方式header:有时候服务端要求带请求头data:请求参数success:成功回调fail:失败回调这里要注意的是要在头部引用util文件var util = require('../../utils/util.js');这里我们在成功和失败的时候都打印了日志,所以运行程序打开控制台。这个问题是微信为了数据安全,要在后台配置合法域名。后台配置在“设置”-“开发设置”中填写request合法域名。由于我们没有自己的服务器,又只是在开发环境测试,所以我们可以使用以下方法访问非合法域名。在开发工具左侧“项目”里面,勾选“开发环境不校验请求域名、TLS版本以及HTTPS证书”刷新项目,还是查看控制台这里我们打印了从豆瓣获取的数据,控制台也明确声明了工具未验证。4、回调函数中绑定数据我们将服务器请求的数据展开,标记我们需要的数据,我们会发现有一些我们不需要的数据,由于我们调用的是公共的API所以会有很多数据提供给其他用户的其他需求,如果我们调用私有的API的时候,这种情况比较少,但是也不排斥服务器懒直接对一个大对象给你的。你跟他说流量啥的,都说服不了他的“方便”。所以我们这里写一个方法,把我们需要的数据取出来。还有一点是因为我们最开始写这个页面的时候,是没有借口文档的,所以所有的变量名都是我们自己定义的。但是拿到借口文档之后,建议根据接口文档,把变量名都改过来,对于后续的维护和调试有很大的好处。修改index.wxml修改movielist模板修改moviecard模板里绑定的变量名运行程序我们发现,中间这个电影的名字太长了,我们界面上不需要这么长,所以我们在moviecard模板的样式文件里面加入强制不换行,超出省列号显示的属性。(截图的时候少了一个width:200rpx;)有些接口需要许可,所以我们先挑选标志,不需要许可的接口Required Scopemovie_premium_r同样的方法,我们编写获取即将上映和Top250即将上映Resources URI/v2/movie/coming_soonTop250Resources URI/v2/movie/top250通过观察我们发现这几个接口数据格式相同。所以我们统一修改一下请求方法。在index中使用运行效果到此我们的数据基本就绑定正确了。但是如上图中标记的还有好几个效果不是很理想。这里我们在starts模板里面增加wx:if当分数为0是显示暂无评分然后请求的时候传递自定义栏目标题。修改一点点细节。不改也无所谓。这节课主要的内容是http请求。细心的朋友会在控制台看到这样一条警告。这是因为我们多次调用setData函数。那么预计下一节课我们就讲解怎么解决这个问题。这节课的内容就到这里结束了。感谢您的阅读。
通过nodejs实现网易云音乐批量下载或单曲下载
从豆瓣转到网易云后,发现了不少好听的歌曲,然鹅..当我想把这些歌拿下来扔车上听的时候发现竟然不允许下载..能听不能下?这不科学,作为一名程序猿,必然要迎难而上啊.2019年3月14日补充:在经过了多次使用下载后,发现其实老是要找到源码,还要配置什么的,也是很麻烦的,最终,把这个做成了一个cli 命令行工具,只需要在某个文件夹内打开cmd,输入十几个字符就搞定了。(大写的注意:使用的时候一定要有安装nodejs环境哈,安装超级简单。)npm install music-163-load -g //一定要全局安装哦,不然每次使用都很麻烦。
//然后在你要保存音乐文件的文件夹内,右键打开命令行,输入以下回车即可,
// -i 后面的是歌单的ID,如果不知道的话,可以在web上打开连接,后面地址上就有写的,
//例如:https://music.163.com/#/playlist?id=2613099530
download -i 2613099530 单曲下载如果只是想下载某一首歌曲的话,其实也不需要写代码的,毕竟浏览器有很多功能已经可以拿到文件了,但是如果想大量的下载,这种重复劳动就需要扔给程序去做了。如果是客户端的话,可以右键拿到单曲链接,然后在浏览器打开。如下:![打开浏览器按F12打开控制台]](https://ucc.alicdn.com/pic/developer-ecology/aaf5bc18eaa44ce69775a72f5c682bf4.png)在浏览器中输入链接后,进入页面,然后按下F12后,打开控制台,然后点击network进入该TAB页面。然后点击播放按钮,查看network中就会有该音乐的播放地址。点击该链接,打开后,复制下mp3的播放地址,另开一个窗口输入后即可播放,右键即可另存为本地。以上操作即可将单曲音乐的文件下载下来,如果不怕麻烦,也可以按照如此的操作,将整个歌单的歌曲文件全部下载下来。当然,作为一名程序员,决不允许这样的劳动浪费行为。通过程序进行批量下载以下为专业知识,需要至少了解js nodejs 相关的知识,以及少量代码编写操作。准备一个后台API服务一个客户端,下载音乐nodejs 环境(如何安装这里就不细说了,网上还是很多的。)步骤1 - 搭建后台APIgithub 上有一个网易云音乐的API接口服务,地址如下:https://github.com/chrunlee/netmusic-node通过git下载到本地后,启动,然后作为API服务器用来请求调用获取数据使用的。我这里提供一个我用的,各位在使用的时候,请节制,我也不希望被封IP以后用不了,希望不要用在自动爬取下载的程序上,只是自己使用下载歌单用下就行。http://music.byyui.com/步骤2 - 搭建本地客户端下载工具由于这个工具只是自己平时使用,也没有放在github上,所以,这里只是贴下几个核心代码,大家可以参考下。// api.js// var api = 'http://localhost:3000';
var api = 'http://music.byyui.com';
//获得歌单
var superagent = require('superagent');
function MusicLoad ( opt ){
var _default = {
isSingle : false,
getListUrl : api+'/v1/playlist/detail',
getSingleInfo : api+'/v1//music/detail',
getUrl : api+'/v1/music/url',
fs : require('fs'),
url : require('url'),
http : require('http'),
async : require('async')
};
this.opt = Object.assign(_default,opt);
this.init();
}
MusicLoad.prototype.init = function(){
var that = this,opt = that.opt;
if(opt.isSingle){
that.getUrl(that.opt.id,null,function(){
console.log('下载完毕.');
});
}else{
that.getList();
}
}
MusicLoad.prototype.getList = function( ){
var that = this;
superagent.get(that.opt.getListUrl+'?id='+that.opt.id+'&limit=300').end(function(err,res){
if(err){
console.log('无法获取歌单')
return;
}
var txt = res.text;
var data = JSON.parse(txt);
var list = data.playlist.tracks;
console.log(list.length);
console.log('获得'+data.playlist.creator.nickname+'的歌单,共计歌曲:'+list.length+'首');
if(list.length > 0){
//循环,获得一首,下载一首
var data = list.map(function(item){
return {
id : item.id,
name : item.name
};
});
//开始判断
if(that.opt.start !== 0){
data = data.slice(that.opt.start,data.length);
console.log('从第'+that.opt.start+'处开始下载,共计'+(data.length - that.opt.start));
}
that.list = data;
that.startLoad();
}else{
console.log('对不起,这个歌单没有歌曲下载。');
}
});
}
MusicLoad.prototype.startLoad = function(){
var that = this;
var list = that.list,async = that.opt.async;
async.mapLimit(list,1,function(item,cb){
that.getUrl(item.id,item.name,cb);
},function(){
console.log('全部下载完成;');
});
}
MusicLoad.prototype.getUrl = function(id,name,cb){
var that = this;
var target= that.opt.getUrl+'?id='+id+'&br=320000';
console.log(target);
superagent.get(target).end(function(err,res){
if(err){
console.log(err);
cb(err,null);
return;
}
var data =JSON.parse(res.text);
var url = data.data[0].url;
if(typeof url == 'string'){
that.download({id : id,name : name,url : url},cb);
}else{
console.log('没有获得该歌曲的URL');
cb(null,null);
}
});
}
MusicLoad.prototype.download = function( item,callback ){
var download = this.opt.download,fs = this.opt.fs,url = this.opt.url,http = this.opt.http;
var href = item.url,
myHref = url.parse(href);
var host = myHref.host,pathname = myHref.pathname;
var http_client = http.request({
hostname: host,
method: 'GET',
path: pathname,
headers: {
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8',
'Accept-Language': 'zh-CN,zh;q=0.8,gl;q=0.6,zh-TW;q=0.4',
'Connection': 'keep-alive',
// 'Content-Type': 'application/x-www-form-urlencoded',
// 'Referer': 'http://music.163.com',
'Pragma':'no-cache',
'Host': host,
'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 9_1 like Mac OS X) AppleWebKit/601.1.46 (KHTML, like Gecko) Version/9.0 Mobile/13B143 Safari/601.1'
}
}, function(res) {
res.on('error', function(err) {
//回调,报错
callback(err,null);
});
var fileBuffer = [];
res.on('data',function(chunk){
fileBuffer.push(new Buffer(chunk));
});
res.on('end',function(){
var total = Buffer.concat(fileBuffer);
fs.appendFile(download+'/'+(item.name || item.id) +'.mp3',total,function(err){
console.log('歌曲下载完成:'+item.name);
callback(null,'over');
});
});
});
http_client.end();
}
var Down = function( opt ){
new MusicLoad(opt);
}
module.exports = Down;以上api.js 为调用API服务的工具类,包括下载以及获取数据,下面的是调用api.js的入口函数://app.js//引入api.js文件
let api = require('./api');
//调用
api({
isSingle : false,//是否是单曲
id : '87950133',//单曲ID或者歌单ID
start : 0,//从第几个开始下载
download : 'f:/redmusic/'//歌曲保存位置
});
console.log('开始下载歌曲..成功率不高');
保存好后,执行node app 即可。以上,仅作为研究学习使用。
LoongSSO 大中型WEB系统单点登陆(SSO)整合利器
作者:七夜来源:http://blog.chinaunix.net/space.php?uid=1760882&do=blog&id=93117
我们都知道网易、搜狐等大型门户都有“通行证”的概念,这个通行证系统就是今天讨论的“单点登录系统”。其主要特征是多个站点一个用户中心,一点登陆后其他也自动登录,注销也是。比如我们在126登录了邮箱,再去163.com就是登陆状态。就好比要建一个摩天大楼,打好地基是重点之重.看到SSO的重要性了吧.
下面我简单介绍一下国际一些名气比较大的SSO解决方案:
一. SAML
SAML,鸟语全名为Security Assertion Markup Language,他是由SUN、BEA、IBM、RSA、AOL、Boeing等大公司,制定技术规范相当专业有水准,系统分层合理,抽象了几个概念把整个系统描述得很清楚,使用流行技术XML Schema来描述协议,使用到了XML-Sign和XML Encrypt等较为前缘XML安全技术. 一看就会让你感觉望而生畏。用个形象性的比喻,SAML协议跟java一样,把每个层都分的很细.跟裹脚布一样.所以SAML技术在java领域用的比较多,在非java领域比较稀少了.sun的 open sso就是开源的SAML一种实现. 要想把opensso 搞定,那得要深厚的功力.自个修炼去吧
二. OpenID
OpenID实际上不属于SSO, 只是一种身份的认证而已。OpenID挺NB的,拥有众多大腕粉丝, 例如GOOGLE、YAHOO、Facebook,希望别人的系统使用它们的帐号登陆。他们希望一种足够简单的WEB SSO规范,于是选择一种草根网络协议OpenID。OpenID,名字取得好,顾名思义,一看就知道它是干嘛的。国内也有它的Fans,例如豆瓣网。openID的确足够简单,但是协议本身是不完善,可能需要一些补充协议才能够满足业务需求。例如GOOGLE采用OpenID + OAuth。目前支持OpenID有Yahoo、Google、Windows Live,还有号称要支持OpenID的Facebook。目前Yahoo和Google宣称对OpenID的支持,但是其实是有限制的,Yahoo的OpenID只有少数合作伙伴才能获得其属性,Google也只有在其Google Apps中才能获得账号的Attribute。用户账号毕竟是一个互联网公司的最宝贵资源,希望他们完全分享账号是不可能的。OpenId 作为一个所谓的“开源项目”,仿佛是人人都在为他服务,但是又好象人人都不给他服务。没有一个机构能够真正的去帮助别人熟悉和使用他
三. Oauth
OAUTH跟OpenID差不多实际不属于SSO范围.是用户身份权限制认证。OAuth是由Blaine Cook、Chris Messina、Larry Halff 及David Recordon共同发起的,目的在于为API访问授权提供一个开放的标准。OAuth规范的1.0版于2007年12月4日发布。目前在微博上应用比较多.
oAuth的典型应用场景(senario)
以前,用户在 拥有资源 的的网站A有一大堆东西;现在用户发现了一个新的网站B,比较好玩,但是这个新的网站B想调用 拥有资源的网站A的数据。
用户在 求资源的网站B 上,点击一个URL,跳转到 拥有 资源的网站A。
拥有资源的网站A提示:你需要把资源分享给B网站吗?Yes/No。
用户点击 Yes,拥有资源的网站A 给 求资源的网站B 临时/永久 开一个通道,然后 求资源的网站 就可以来 拥有资源的网站 抓取所需的信息了。
oAuth更像是一种资源更像是一种网站资源的共享,而并不是用户数据的共享和一站登陆全站都登陆的机制.
四.CAS
CAS(Central Authentication Service) 是 Yale 大学发起的一个开源项目,据统计,大概每 10 个采用开源构建 Web SSO 的 Java 项目,就有 8 个使用 CAS 。Cas是java应用最广泛的开源单点登陆实现了。
国内的一些门户网站包括吃都是基于cookie SSO方案。浏览器直接请求SSO server进行身份验证,验证成功返回一段回调的JS代码。然后浏览器用js src逐个隐式的去访问sso client , sso client用P3P技术,把各个域名的cookie种在用户浏览器,这样就实现了整个SSO过程
这种方式显而易见就是非常简单,比刚才介绍的任何一款SSO开源项目都简单方便. 但是这种方式的缺点,本人认为主要是两点:1. 子站点过多时,回调接口相应增多,这个在分布子站的量的限制上,如何控制来使登录效率不会太低,不好把握; 2. 当某个子站回调接口出现问题时,默认的登录过程会卡住(可以限制登录程序的执行时间,但相应出现问题子站后面的子站的回调接口就调不到了。
以下是大致的流程图
LoongSSO是2008年就开始发布的一款整体SSO开源解决方案。项目包括sso server(单点登陆)、session pool server.
Loongsso开源网站 http://www.loongsso.com
使用文档: http://www.loongsso.com/doc.html论坛讨论: http://www.loongsso.com/bbs/
loongsso 作者 七夜(李锦星)mail lijinxing@gmail.comQQ 531020471MSN lijinxing20@hotmail.com
LoongSSO2.1 下载地址http://code.google.com/p/loongsso/downloads/detail?name=loongsso2.1.tar.bz2&can=2&q=
loongsso for Discuz api 下载地址http://code.google.com/p/loongsso/downloads/detail?name=Discuz7.2.tar.bz2&can=2&q=
loongsso2.1 for phpwind API 下载地址http://code.google.com/p/loongsso/downloads/detail?name=phpwind8.3.tar.bz2&can=2&q=
Loongsso server 大致介绍
1. 采用C开发,能稳定高效的运行在linux、freebsd等类*NIX系统下2. 使用master-worker多进程工作模型再配合epoll、kqueue事件触发机制3. 采用MySQL作为用户数据库,通过Handler Socket来进行读写mysql,既保证了用户数据的安全稳定,又提高了读写数据的效率4. 采用简单易配置的xml配置文件5. 使用HTTP协议交互,MD5数字签名,保证数据交互的方便性、安全性、高效性6. 有保留关键注册名的功能7. 对SSO client的编辑删除的权限控制
session pool server 大致介绍
1. 采用C开发,能稳定高效的运行在linux、freebsd等类*NIX系统下2. 使用线程池工作模型再配合epoll、kqueue事件触发机制3. 持久session保存数据,内部采用高效的hashtable来存储session数据4. 采用haproxy作为入口网关,使用haproxy的一致性hash工作模式把请求分发给集群中的session server
loongSSO分为两种SSO工作模式1. JS回调机制.Javascript回调各个SSO client,然后利用P3P协议种各自的cookie2. 统一cookie机制.就是session id统一种植在SSO server所运行的域名cookie下.各个SSO client需要session id。都通过loongsso去查询
总结
第一种模式的优点和缺点上文已经大概的介绍过了,个人觉的缺点大于利,无法在几十个或者几百个域名下实现单点登陆。
第二种模式,用户登录后是不用把用户登录的信息逐个通知给各个SSO client。当用户访问哪个SSO client,那个SSO client就会去loongsso server去读取。这样就保证了资源最大化的利用,就算成千上万个sso client都不成问题。当然loongsso server是可以分布式的运行的来支持更多的请求
DEMO 站点
http://sso2.weigame.com/ Discuz 论坛
http://sso3.dlapk.com/index.php?m=bbs phpwind论坛
测试用户名: demo123 密码: 123456
在任何一端登陆,到另一个网站就无须登陆
1. 用户浏览器请求www.aaa.com的login.html2. Web server返回 login.html3. 用户直接把表单POST到SSO server4. SSO server根据用户名去mysql的用户库去验证5. 数据库验证成功,生成session id,把session数据写到session pool server6. Sso server 把session id种在sso server本域名下cookie7. 用户访问www.bbb.com8. www.bbb.com返回页面给用户浏览器.用户浏览器请求SSO server查询session id9. SSO server返回session id给用户浏览器.10. www.aaa.com用session id去session pool server查询session 用户数据11. 根据session用户数据,生成www.aaa.com的cookie
本文来自CSDN博客,转载请标明出处:http://blog.csdn.net/heiyeshuwu/archive/2011/01/19/6152599.aspx
用 Node.js 把玩一番 Alfred Workflow
插件地址(集成Github、掘金、知乎、淘宝等搜索)
作为 Mac 上常年位居神器榜第一位的软件来说,Alfred 给我们带来的便利是不言而喻的,其中 workflow(工作流) 功不可没,在它上面可以轻松地查找任何 api;可以快速在豆瓣上搜到自己喜欢的电影、图书、音乐;可以快速把图片上传到图床 等等。
一些安利
附上一张个人装着的插件的截图。Caffeinate 插件能在指定时间使电脑不黑屏;在 Dash 插件上能轻松查任何文档;Youdao Translate 插件比系统自带的翻译方便许多。插件也是因人而异,大家可以在 Workflow List 上逛逛,各取所需。
在用了别人的插件感觉高大上后,便萌发了也写一个插件的想法,计划把自己常逛的网站集合成一个插件,使用特定的缩略词便可快速进行搜索数据,又看了官方称可以使用 bash, zsh, PHP, Ruby, Python, Perl, Apple Script 开发 Alfred Workflow。于是我选择了 Node.js 作为开发语言,开发了一款 commonSearch, 开发完效果如下(集成了Github、掘金、知乎、淘宝等搜索)。
开发阶段
在开发前,得先对一些特定的操作步骤和知识点有一定的认知,这样开发时就基本上没有大碍了。
前置步骤
可以先参考 如何去写一个第三方的 workflow 的开始部分, 完成基本工作流的搭建,如下图是我搭建好的基本工作流连线。
在 Script 中,可以看到 /usr/local/bin/node common_search.js 相当于就是在调用该插件的时候起了一个 node 服务,后面的 1 是为了区分当前调用的是哪个搜索手动传入 common_search.js 的,{query} 则是用户查询的名称。
使用 Node.js 调用 JSON API
最初开发参考了 知乎搜索 这个项目,它是基于 cheerio 这个模块对请求到的网页数据进行分析爬取,但是引入了 cheerio 后,插件体积多了 2M 多,这对于一个插件来说太不友好了,所以这可能是 python 之类的语言更适合开发类似插件的原因吧(猜想:python 不需要引人第三方库就能进行爬虫),于是我开始选择提供 JSON API 的接口,比如找寻掘金返回数据的接口。首先打开 chrome 控制台,这可能对前端工程师比较熟悉了。
从而找到了掘金返回搜索数据的接口是 https://search-merger-ms.juejin.im/v1/search?query={query}&page=0&raw_result=false&src=web
接着愉快地使用 node 提供的 https 模块,这里有一个注意点,http.get() 回调中的 res 参数不是正文,而是 http.ClientResponse 对象,所以我们需要组装内容。
var options = {
host: 'search-merger-ms.juejin.im',
path: '/v1/search?query=' + encodeURI(keyword) + '&page=0&raw_result=false&src=web'
}
https.get(options, function (res) {
res.on('data', (chunk) => {
var content += chunk
}).on('end', function () {
var jsonContent = JSON.parse(content) && JSON.parse(content).d
var result_array = []
for (var i = 0; i < jsonContent.length; i++) {
if (jsonContent[i].user.jobTitle === '') {
result_array.push({
title:
subtitle:
arg:
icon: {
path: join(__dirname, 'xx.png'),
},
mods: {
cmd: {}
}
})
}
}
content = ''
console.log(JSON.stringify({
items: result_array
}))
})
})
这种方法应该是最直接的调用 JSON API 的方案了,当然也可以引人第三方模块 request 后解析 JSON,示例如下:
var request = require('request')
var url = 'search-merger-ms.juejin.im/v1/search?query=' + encodeURI(keyword) + '&page=0&raw_result=false&src=web'
request.get({
url: url,
json: true,
headers: {'User-Agent': 'request'}
}, (err, res, data) => {
if (err) {
console.log('Error:', err);
} else if (res.statusCode !== 200) {
console.log('Status:', res.statusCode);
} else {
// data is already parsed as JSON:
console.log(data.html_url);
}
});
还有一点要注意的是返回值的字段是固定的,具体可以参考它的官方解释,琢磨了好久才把 JS 中的 Icon 自定义的格式找出来。
title: 主标题
subtitle: 内容行
arg: 跳转链接
icons: 图标
mods:定制键盘按键的方法
对于 Github、掘金、知乎、淘宝的搜索都是基于以上思路进行开发的,就是对于具体返回的 JSON 数据进行了不同处理,虽然粗糙,但也算完成了第一个 Alfred Workflow 插件的开发。
尾声
本文的知识点写的不是特别丰满,一是就是对开发这个插件的小结,另外就是抛砖引玉了,能让更多的小伙伴了解开发一个插件并不是难事,同时让更多的朋友开发出更多有意义,有趣的 alfred-workflow 插件也算是本文分享的一个初衷了。
作者:牧云云
出处:http://www.cnblogs.com/MuYunyun/"
本文版权归作者和博客园所有,欢迎转载,转载请标明出处。
如果您觉得本篇博文对您有所收获,请点击右下角的 [推荐],谢谢!
iOS - Share 分享/第三方登录
1、系统方式创建分享
按照下图在 Info.plist 文件中将 Localization native development region 的值改为 China。如果不设置此项弹出的分享页面中显示的按钮为英文说明。
UIActivityViewController 方式创建
// 设置分享的内容
NSString *textToShare = @"请大家登录《iOS云端与网络通讯》服务网站。";
UIImage *imageToShare = [UIImage imageNamed:@"swift"];
NSURL *urlToShare = [NSURL URLWithString:@"http://m.baidu.com"];
// 创建分享视图控制器
/*
activityItems: 分享的内容
applicationActivities: 分享的类型,默认(nil)时为 UIActivity
*/
NSArray *items = @[textToShare, imageToShare, urlToShare];
UIActivityViewController *activityVC = [[UIActivityViewController alloc] initWithActivityItems:items
applicationActivities:nil];
// 设置不出现的分享按钮
/*
Activity 类型又分为 “操作” 和 “分享” 两大类:
UIActivityCategoryAction 操作:
UIActivityTypeAirDrop AirDrop AirDrop
UIActivityTypePrint 打印 Print
UIActivityTypeSaveToCameraRoll 保存到相册 Save Image
UIActivityTypeAssignToContact 添加到联系人 AssignToContact
UIActivityTypeAddToReadingList 添加到 Safari 阅读列表 AddToReadingList
UIActivityTypeCopyToPasteboard 复制到剪贴板 Copy
UIActivityTypeOpenInIBooks 在 iBook 中打开
UIActivityCategoryShare 分享:
UIActivityTypeMail 邮箱 Mail
UIActivityTypeMessage 短信 Message
UIActivityTypePostToTwitter 分享到 Twitter
UIActivityTypePostToFacebook 分享到 Facebook
UIActivityTypePostToVimeo 分享到 Vimeo(视频媒体)
UIActivityTypePostToFlickr 分享到 Flickr(网络相簿)
UIActivityTypePostToWeibo 分享到 新浪微博
UIActivityTypePostToTencentWeibo 分享到 腾讯微博
*/
// 添加到此数组中的系统分享按钮项将不会出现在分享视图控制器中
activityVC.excludedActivityTypes = @[UIActivityTypeAssignToContact, UIActivityTypePrint];
// 显示分享视图控制器
[self presentViewController:activityVC animated:YES completion:nil];
// 分享完成
activityVC.completionWithItemsHandler = ^(NSString *activityType,
BOOL completed,
NSArray *returnedItems,
NSError *activityError) {
// 分享完成或退出分享时调用该方法
if (completed) {
NSLog(@"分享完成");
} else {
NSLog(@"取消分享");
}
};
效果
2、系统方式自定义分享
按照下图在 Info.plist 文件中将 Localization native development region 的值改为 China。如果不设置此项弹出的分享页面中显示的按钮为英文说明。
自定义按钮
myUIActivity.h
#import <UIKit/UIKit.h>
@interface myUIActivity : UIActivity <UINavigationControllerDelegate>
@end
myUIActivity.m
#import "myUIActivity.h"
/*
自定义分享按钮
*/
@implementation myUIActivity
// 设置分享按钮的类型
- (NSString *)activityType {
// 在 completionWithItemsHandler 回调里可以用于判断,一般取当前类名
return NSStringFromClass([myUIActivity class]);
}
// 设置分享按钮的标题
- (NSString *)activityTitle {
// 设置显示在分享框里的名称
return @"自定义分享按钮";
}
// 设置分享按钮的图片
- (UIImage *)activityImage {
// 图片自定变为黑白色,默认尺寸为 56 * 56 像素
return [UIImage imageNamed:@"JHQ0228"];
}
// 设置是否显示分享按钮
- (BOOL)canPerformWithActivityItems:(NSArray *)activityItems {
// 这里一般根据用户是否授权等来决定是否要隐藏分享按钮
return YES;
}
// 预处理分享数据
- (void)prepareWithActivityItems:(NSArray *)activityItems {
// 解析分享数据时调用,可以进行一定的处理
NSLog(@"prepareWithActivityItems");
// 手动执行分享操作,保存到相册
UIImageWriteToSavedPhotosAlbum(activityItems[1], nil, nil, nil);
}
// 执行分享
- (UIViewController *)activityViewController {
// 点击自定义分享按钮时调用,跳转到自定义的视图控制器
NSLog(@"activityViewController");
return nil;
}
// 执行分享
- (void)performActivity {
// 点击自定义分享按钮时调用
NSLog(@"performActivity");
}
// 完成分享
- (void)activityDidFinish:(BOOL)completed {
// 分享视图控制器退出时调用
NSLog(@"activityDidFinish");
}
@end
使用自定义按钮
ViewController.m
#import "myUIActivity.h"
// 设置分享的内容
NSString *textToShare = @"请大家登录《iOS云端与网络通讯》服务网站。";
UIImage *imageToShare = [UIImage imageNamed:@"swift"];
NSURL *urlToShare = [NSURL URLWithString:@"http://m.baidu.com"];
// 设置分享的类型
myUIActivity *myActivity = [[myUIActivity alloc] init];
// 创建分享视图控制器
NSArray *items = @[textToShare, imageToShare, urlToShare];
NSArray *activities = @[myActivity];
UIActivityViewController *activityVC = [[UIActivityViewController alloc] initWithActivityItems:items
applicationActivities:activities];
// 设置不出现的分享按钮
activityVC.excludedActivityTypes = @[UIActivityTypeAirDrop];
// 显示分享视图控制器
[self presentViewController:activityVC animated:YES completion:nil];
// 分享完成
activityVC.completionWithItemsHandler = ^(NSString *activityType,
BOOL completed,
NSArray *returnedItems,
NSError *activityError) {
// 分享完成或退出分享时调用该方法
if (completed) {
NSLog(@"分享完成");
} else {
NSLog(@"取消分享");
}
};
效果
3、友盟 登录/分享 集成
U-Share 快速集成多平台分享、登录功能。帮助应用或游戏快速具备国内外多平台分享、第三方登录功能,SDK 包最小,集成成本最低,平台覆盖最全,并基于友盟+大数据,提供最为权威、实时的用户画像、分享回流等数据分析,助力产品开发与推广。
覆盖国内外近 30 家社交平台,支持文本、图片、音乐、视频、链接等多种内容类型的分享,并提供了主流游戏平台的 SDK。
国内平台:微信、朋友圈、QQ、Qzone、新浪微博、腾讯微博、人人、豆瓣、易信、短信、邮件等。
国外平台:Facebook、Twitter、Instagram、Google+、LINE、WhatsApp、Pinterest、Evernote、Pocket、LinkedIn、KakaoTalk 等。
U-Share 集成流程
U-Share 第三方账号申请及绑定
U-Share 快速集成文档
U-Share API 说明
U-Share SDK 下载
集成友盟社会化组件流程
1.1 注册友盟账号
登陆友盟官网,在我的产品页面添加新应用,然后获取到 Appkey。
1.2 申请第三方账号
参照文档:申请第三方账号
1.3 绑定第三方账号
参照文档:绑定第三方账号到友盟后台
1.4 下载 SDK
进入下载 SDK 页面,勾选自己需要的功能进行下载。
1.5 技术支持
官方微博: umengsocial
开发者社区:http://bbs.umeng.com/thread-5908-1-1.html?from=qianming
技术支持:联系客服
3.1 第三方账号申请及绑定
3.1.1 申请第三方账号
进行分享、授权操作需要在第三方平台创建应用并提交审核,友盟默认提供了大多数平台的测试账号,但如果需要将分享、授权来源、分享到 QQ、Qzone 的 icon 更改为自己 APP 的应用,就需要自己申请第三方账号。
新浪微博
登录新浪微博开放平台,填写相关应用信息并上传 icon 图片。注意修改安全域名为 sns.whalecloud.com 同时设置授权回调页为 http://sns.whalecloud.com/sina2/callback 安全域名设置在应用信息 --> 基本信息,具体位置参考下
授权回调页、取消授权回调页设置在应用信息 --> 高级信息,具体位置参考下图
安全域名的修改需要二次审核通过才生效,授权回调页修改即时生效
微信
登录微信开放平台,填写相关应用信息,审核通过后获取到微信 AppID 及 AppSecret,如果需要微信登录功能,需要申请微信登录权限。
QQ 及 Qzone
QQ 及 Qzone 使用同一个 AppID 及 Appkey,登录腾讯开放平台,选择移动应用,填写相关应用信息并提交审核,未审核前通过只能使用测试账号,添加测试账号方法如下:选择用户能力 --> 进阶社交能力 --> 应用调试者,添加测试账号必须在申请者好友列表中,如下图
人人网
登录人人开放平台,填写相关应用信息,同时填写应用根域名为 sns.whalecloud.com 具体位置: 基本信息 --> 应用根域名 如图
豆瓣
登录豆瓣开放平台,创建应用并填写相关应用信息,注意权限必须选择广播,同时填写回调地址为 http://sns.whalecloud.com/douban/callback
3.1.2 绑定第三方账号到友盟后台
目前需要在友盟后台绑定的第三方账号为:新浪微博、腾讯微博、人人网、豆瓣、Qzone,其余平台如微信、QQ 直接在代码中设置。
绑定地址:http://umeng.com/apps,登录友盟网站 -> 左上角选择你们的产品 -> 组件 -> 社会化组件 -> 设置
短链接开关
短链接开关只对新浪微博、腾讯微博、人人网、豆瓣四个平台有效,开启短链接开关,分享文案中附加的链接会被转码,同时可以统计到分享回流率(点击链接的次数),关闭短链接开关则无法统计,短链接开关默认为关闭状态。
文字截断开关
文字截断开关只对新浪微博、腾讯微博、人人网、豆瓣四个平台有效,同时只对使用自定义分享编辑页或没有分享编辑页用户有效,当分享文案超出字数限制时自动截断,开关状态默认关闭。
3.2 U-Share SDK 集成
3.2.1 下载 U-Share SDK
通过 iOS 社会化组件下载页面选择所需的社交平台后进行下载。
3.2.2 加入 U-Share SDK
将 U-Share SDK 添加到工程
添加项目配置,在 Other Linker Flags 加入 -ObjC
加入依赖系统库 libsqlite3.tbd
添加平台相应的依赖库,根据集成的不同平台加入相关的依赖库,未列出平台则不用添加。添加方式:选中项目 Target -> Linked Frameworks and Libraries 列表中添加
注:Twitter 平台加入后需添加 TwitterKit.framework/Resources/TwitterKitResources.bundle。
3.3 U-Share SDK 平台配置
从这一步骤就开始需要第三方 appKey 和 appSecret 等信息,可参考第三方账号申请及绑定申请所需的平台账号。
3.3.1 配置各平台 URL Scheme
添加 URL Types
URL Scheme 是通过系统找到并跳转对应 app 的一类设置,通过向项目中的 info.plist 文件中加入 URL types 可使用第三方平台所注册的 appkey 信息向系统注册你的 app,当跳转到第三方应用授权或分享后,可直接跳转回你的 app。
添加 URL Types 有如下几处,都可进行设置
1、通过工程设置面板
2、通过 info.plist 文件编辑
3、直接编辑 info.plist 中 XML 代码
配置第三方平台 URL Scheme
未列出则不需设置
3.3.2 适配 iOS 9/10 系统
iOS9 系统后 Apple 对 HTTP 请求及访问外部应用做了更加严格的要求,包括 HTTP 白名单、跳转第三方应用白名单等,具体设置第三方平台参数请参照适配 iOS9/10 系统。
HTTPS 传输安全
Apple 将从 2017 年开始执行 ATS(App Transport Security),所有进行审核的应用中网络请求全部支持 HTTPS,届时以下配置将会失效,请提前做好准备。
以 iOS10 SDK 编译的工程会默认以 SSL 安全协议进行网络传输,即 HTTPS,如果依然使用 HTTP 协议请求网络会报系统异常并中断请求。目前可用如下两种方式保持用 HTTP 进行网络连接:
在 info.plist 中加入安全域名白名单(右键 info.plist 用 source code 打开)
<key>NSAppTransportSecurity</key>
<dict>
<!-- 配置允许 http 的任意网络End-->
<key>NSExceptionDomains</key>
<dict>
<!-- 集成新浪微博对应的HTTP白名单-->
<key>sina.com.cn</key>
<dict>
<key>NSIncludesSubdomains</key>
<true/>
<key>NSThirdPartyExceptionAllowsInsecureHTTPLoads</key>
<true/>
<key>NSThirdPartyExceptionRequiresForwardSecrecy</key>
<false/>
</dict>
<key>sinaimg.cn</key>
<dict>
<key>NSIncludesSubdomains</key>
<true/>
<key>NSThirdPartyExceptionAllowsInsecureHTTPLoads</key>
<true/>
<key>NSThirdPartyExceptionRequiresForwardSecrecy</key>
<false/>
</dict>
<key>sinajs.cn</key>
<dict>
<key>NSIncludesSubdomains</key>
<true/>
<key>NSThirdPartyExceptionAllowsInsecureHTTPLoads</key>
<true/>
<key>NSThirdPartyExceptionRequiresForwardSecrecy</key>
<false/>
</dict>
<key>sina.cn</key>
<dict>
<!-- 适配iOS10 -->
<key>NSExceptionMinimumTLSVersion</key>
<string>TLSv1.0</string>
<key>NSIncludesSubdomains</key>
<true/>
<key>NSThirdPartyExceptionRequiresForwardSecrecy</key>
<false/>
</dict>
<key>weibo.cn</key>
<dict>
<!-- 适配iOS10 -->
<key>NSExceptionMinimumTLSVersion</key>
<string>TLSv1.0</string>
<key>NSIncludesSubdomains</key>
<true/>
<key>NSThirdPartyExceptionRequiresForwardSecrecy</key>
<false/>
</dict>
<key>weibo.com</key>
<dict>
<!-- 适配iOS10 -->
<key>NSExceptionMinimumTLSVersion</key>
<string>TLSv1.0</string>
<key>NSIncludesSubdomains</key>
<true/>
<key>NSThirdPartyExceptionAllowsInsecureHTTPLoads</key>
<true/>
<key>NSThirdPartyExceptionRequiresForwardSecrecy</key>
<false/>
</dict>
<!-- 新浪微博-->
<!-- 集成人人授权对应的HTTP白名单-->
<key>renren.com</key>
<dict>
<key>NSIncludesSubdomains</key>
<true/>
<key>NSThirdPartyExceptionAllowsInsecureHTTPLoads</key>
<true/>
<key>NSThirdPartyExceptionRequiresForwardSecrecy</key>
<false/>
</dict>
<!-- 人人授权-->
</dict>
</dict>
若新版 Xcode 控制台输出 “[] tcp_connection_xxx“ 等内容,可以在运行按钮旁的选择 target 选项内的 Edit Scheme - Run - Arguments - Enviroment variables 中增加 OS_ACTIVITY_MODE=disable,可将相关日志关闭。
配置 ApplicationQueriesSchemes(应用间跳转)
如果你的应用使用了如 SSO 授权登录或跳转到第三方分享功能,在 iOS9/10 下就需要增加一个可跳转的白名单,即 LSApplicationQueriesSchemes,否则将在 SDK 判断是否跳转时用到的 canOpenURL 时返回 NO,进而只进行 webview 授权或授权/分享失败。 在项目中的 info.plist 中加入应用白名单,右键 info.plist 选择 source code 打开(具体设置在 Build Setting -> Packaging -> Info.plist File 可获取 plist 路径)。
<key>LSApplicationQueriesSchemes</key>
<array>
<!-- 微信 URL Scheme 白名单-->
<string>wechat</string>
<string>weixin</string>
<!-- 新浪微博 URL Scheme 白名单-->
<string>sinaweibohd</string>
<string>sinaweibo</string>
<string>sinaweibosso</string>
<string>weibosdk</string>
<string>weibosdk2.5</string>
<!-- QQ、Qzone URL Scheme 白名单-->
<string>mqqapi</string>
<string>mqq</string>
<string>mqqOpensdkSSoLogin</string>
<string>mqqconnect</string>
<string>mqqopensdkdataline</string>
<string>mqqopensdkgrouptribeshare</string>
<string>mqqopensdkfriend</string>
<string>mqqopensdkapi</string>
<string>mqqopensdkapiV2</string>
<string>mqqopensdkapiV3</string>
<string>mqqopensdkapiV4</string>
<string>mqzoneopensdk</string>
<string>wtloginmqq</string>
<string>wtloginmqq2</string>
<string>mqqwpa</string>
<string>mqzone</string>
<string>mqzonev2</string>
<string>mqzoneshare</string>
<string>wtloginqzone</string>
<string>mqzonewx</string>
<string>mqzoneopensdkapiV2</string>
<string>mqzoneopensdkapi19</string>
<string>mqzoneopensdkapi</string>
<string>mqqbrowser</string>
<string>mttbrowser</string>
<!-- 支付宝 URL Scheme 白名单-->
<string>alipay</string>
<string>alipayshare</string>
<!-- 人人 URL Scheme 白名单-->
<string>renrenios</string>
<string>renrenapi</string>
<string>renren</string>
<string>renreniphone</string>
<!-- 来往 URL Scheme 白名单-->
<string>laiwangsso</string>
<!-- 易信 URL Scheme 白名单-->
<string>yixin</string>
<string>yixinopenapi</string>
<!-- instagram URL Scheme 白名单-->
<string>instagram</string>
<!-- whatsapp URL Scheme 白名单-->
<string>whatsapp</string>
<!-- line URL Scheme 白名单-->
<string>line</string>
<!-- Facebook URL Scheme 白名单-->
<string>fbapi</string>
<string>fb-messenger-api</string>
<string>fbauth2</string>
<string>fbshareextension</string>
<!-- Kakao URL Scheme 白名单-->
<!-- 注:以下第一个参数需替换为自己的kakao appkey-->
<!-- 格式为 kakao + "kakao appkey"-->
<string>kakaofa63a0b2356e923f3edd6512d531f546</string>
<string>kakaokompassauth</string>
<string>storykompassauth</string>
<string>kakaolink</string>
<string>kakaotalk-4.5.0</string>
<string>kakaostory-2.9.0</string>
<!-- pinterest URL Scheme 白名单-->
<string>pinterestsdk.v1</string>
</array>
3.4 调用 U-Share SDK
3.4.1 初始化设置
初始化 U-Share 及第三方平台
app 启动后进行 U-Share 和第三方平台的初始化工作,以下代码将所有平台初始化示例放出,开发者根据平台需要选取相应代码,并替换为所属注册的 appKey 和 appSecret。
在 AppDelegate.m 中设置如下代码
#import <UMSocialCore/UMSocialCore.h>
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// 打开调试日志
[[UMSocialManager defaultManager] openLog:YES];
// 设置友盟 appkey
[[UMSocialManager defaultManager] setUmSocialAppkey:@"57b432afe0f55a9832001a0a"];
// 获取友盟 social 版本号
// NSLog(@"UMeng social version: %@", [UMSocialGlobal umSocialSDKVersion]);
// 设置微信的 appKey 和 appSecret
[[UMSocialManager defaultManager] setPlaform:UMSocialPlatformType_WechatSession
appKey:@"wxdc1e388c3822c80b"
appSecret:@"3baf1193c85774b3fd9d18447d76cab0"
redirectURL:@"http://mobile.umeng.com/social"];
// 设置分享到 QQ 互联的 appKey 和 appSecret
// U-Share SDK 为了兼容大部分平台命名,统一用 appKey 和 appSecret 进行参数设置,
// 而 QQ 平台仅需将 appID 作为 U-Share 的 appKey 参数传进即可。
[[UMSocialManager defaultManager] setPlaform:UMSocialPlatformType_QQ
appKey:@"100424468"
appSecret:nil
redirectURL:@"http://mobile.umeng.com/social"];
// 设置新浪的 appKey 和 appSecret
[[UMSocialManager defaultManager] setPlaform:UMSocialPlatformType_Sina
appKey:@"3921700954"
appSecret:@"04b48b094faeb16683c32669824ebdad"
redirectURL:@"http://sns.whalecloud.com/sina2/callback"];
// 支付宝的 appKey
[[UMSocialManager defaultManager] setPlaform:UMSocialPlatformType_AlipaySession
appKey:@"2015111700822536"
appSecret:nil
redirectURL:@"http://mobile.umeng.com/social"];
// 设置易信的 appKey
[[UMSocialManager defaultManager] setPlaform:UMSocialPlatformType_YixinSession
appKey:@"yx35664bdff4db42c2b7be1e29390c1a06"
appSecret:nil
redirectURL:@"http://mobile.umeng.com/social"];
// 设置点点虫(原来往)的 appKey 和 appSecret
[[UMSocialManager defaultManager] setPlaform:UMSocialPlatformType_LaiWangSession
appKey:@"8112117817424282305"
appSecret:@"9996ed5039e641658de7b83345fee6c9"
redirectURL:@"http://mobile.umeng.com/social"];
// 设置领英的 appKey 和 appSecret
[[UMSocialManager defaultManager] setPlaform:UMSocialPlatformType_Linkedin
appKey:@"81t5eiem37d2sc"
appSecret:@"7dgUXPLH8kA8WHMV"
redirectURL:@"https://api.linkedin.com/v1/people"];
// 设置 Twitter 的 appKey 和 appSecret
[[UMSocialManager defaultManager] setPlaform:UMSocialPlatformType_Twitter
appKey:@"fB5tvRpna1CKK97xZUslbxiet"
appSecret:@"YcbSvseLIwZ4hZg9YmgJPP5uWzd4zr6BpBKGZhf07zzh3oj62K"
redirectURL:nil];
// 如果不想显示平台下的某些类型,可用以下接口设置
// [[UMSocialManager defaultManager] removePlatformProviderWithPlatformTypes:@[@(UMSocialPlatformType_WechatFavorite),
@(UMSocialPlatformType_YixinTimeLine),
@(UMSocialPlatformType_LaiWangTimeLine),
@(UMSocialPlatformType_Qzone)]];
...
return YES;
}
设置系统回调
// 支持所有 iOS 系统
- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url
sourceApplication:(NSString *)sourceApplication annotation:(id)annotation {
BOOL result = [[UMSocialManager defaultManager] handleOpenURL:url];
if (!result) {
// 其他如支付等 SDK 的回调
}
return result;
}
注:以上为建议使用的系统 openURL 回调,且 新浪 平台仅支持以上回调。还有以下两种回调方式,如果开发者选取以下回调,也请补充相应的函数调用。
1、仅支持 iOS9 以上系统,iOS8 及以下系统不会回调
- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url
options:(NSDictionary<UIApplicationOpenURLOptionsKey, id> *)options {
BOOL result = [[UMSocialManager defaultManager] handleOpenURL:url];
if (!result) {
// 其他如支付等 SDK 的回调
}
return result;
}
2、支持目前所有 iOS 系统
- (BOOL)application:(UIApplication *)application handleOpenURL:(NSURL *)url {
BOOL result = [[UMSocialManager defaultManager] handleOpenURL:url];
if (!result) {
// 其他如支付等 SDK 的回调
}
return result;
}
3.4.2 第三方平台登录
支持登录的平台:微信、QQ、新浪微博、腾讯微博、人人网、豆瓣、Facebook、Twitter、Linkedin 领英、Kakao。
支持登录并获取用户信息的平台:微信、QQ、新浪微博、Facebook、Twitter、Linkedin 领英、Kakao。
授权并获取用户信息
// 在需要进行获取登录信息的 UIViewController 中加入如下代码
#import <UMSocialCore/UMSocialCore.h>
- (void)getUserInfoForPlatform:(UMSocialPlatformType)platformType {
[[UMSocialManager defaultManager] getUserInfoWithPlatform:platformType
currentViewController:self
completion:^(id result, NSError *error) {
UMSocialUserInfoResponse *resp = result;
// 第三方登录数据(为空表示平台未提供)
// 授权数据
NSLog(@" uid: %@", resp.uid);
NSLog(@" openid: %@", resp.openid);
NSLog(@" accessToken: %@", resp.accessToken);
NSLog(@" refreshToken: %@", resp.refreshToken);
NSLog(@" expiration: %@", resp.expiration);
// 用户数据
NSLog(@" name: %@", resp.name);
NSLog(@" iconurl: %@", resp.iconurl);
NSLog(@" gender: %@", resp.gender);
// 第三方平台 SDK 原始数据
NSLog(@" originalResponse: %@", resp.originalResponse);
}];
}
注:若在 4.x 及 5.x 版本中使用微信登录,升级后需参考说明:4.x/5.x版本升级(授权信息变化)。
3.4.3 第三方平台分享
调用分享面板
在分享按钮绑定如下触发代码
#import <UShareUI/UShareUI.h>
// 显示分享面板
[UMSocialUIManager showShareMenuViewInWindowWithPlatformSelectionBlock:^(UMSocialPlatformType platformType, NSDictionary *userInfo) {
// 根据获取的 platformType 确定所选平台进行下一步操作
}];
定制自己的分享面板预定义平台
以下方法可设置平台顺序
#import <UShareUI/UShareUI.h>
[UMSocialUIManager setPreDefinePlatforms:@[@(UMSocialPlatformType_Sina),@(UMSocialPlatformType_QQ),@(UMSocialPlatformType_WechatSession)]];
[UMSocialUIManager showShareMenuViewInWindowWithPlatformSelectionBlock:^(UMSocialPlatformType platformType, NSDictionary *userInfo) {
// 根据获取的 platformType 确定所选平台进行下一步操作
}];
为避免应用审核被拒,仅会对有效的平台进行显示,如平台应用未安装,或平台应用不支持等会进行隐藏。由于以上原因,在模拟器上部分平台会隐藏。
如果遇到分享面板未显示,请参考分享面板无法弹出。
设置分享内容
分享文本
- (void)shareTextToPlatformType:(UMSocialPlatformType)platformType {
// 创建分享消息对象
UMSocialMessageObject *messageObject = [UMSocialMessageObject messageObject];
// 设置文本
messageObject.text = @"社会化组件 UShare 将各大社交平台接入您的应用,快速武装 App。";
// 调用分享接口
[[UMSocialManager defaultManager] shareToPlatform:platformType
messageObject:messageObject
currentViewController:self
completion:^(id data, NSError *error) {
if (error) {
NSLog(@"************ Share fail with error %@ *********", error);
}else{
NSLog(@"response data is %@", data);
}
}];
}
其他分享类型示例请参考 U-Share API 文档。
3.5 技术支持
访问:友盟开发者社区
发邮件至 social-support@umeng.com。
为了能够尽快响应您的反馈,请提供您的 appkey 及 log 中的详细出错日志,您所提供的内容越详细越有助于我们帮您解决问题。
开启友盟分享调试 log 方法:
#import <UMSocialCore/UMSocialCore.h>
[[UMSocialManager defaultManager] openLog:YES];
在 console 中查看日志。
效果
3.6 简单使用
AppDelegate.m
#import <UMSocialCore/UMSocialCore.h>
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// 打开日志
[[UMSocialManager defaultManager] openLog:YES];
// 打开图片水印
//[UMSocialGlobal shareInstance].isUsingWaterMark = YES;
// 获取友盟 social 版本号
UMSocialLogInfo(@"UMeng social version: %@", [UMSocialGlobal umSocialSDKVersion]);
// 设置友盟 appkey
[[UMSocialManager defaultManager] setUmSocialAppkey:@"57b432afe0f55a9832001a0a"];
// 设置微信的 appKey 和 appSecret
[[UMSocialManager defaultManager] setPlaform:UMSocialPlatformType_WechatSession
appKey:@"wxdc1e388c3822c80b"
appSecret:@"3baf1193c85774b3fd9d18447d76cab0"
redirectURL:@"http://mobile.umeng.com/social"];
/*
* 添加某一平台会加入平台下所有分享渠道,如微信:好友、朋友圈、收藏,QQ:QQ 和 QQ 空间
* 以下接口可移除相应平台类型的分享,如微信收藏,对应类型可在枚举中查找
*/
//[[UMSocialManager defaultManager] removePlatformProviderWithPlatformTypes:@[@(UMSocialPlatformType_WechatFavorite)]];
// 设置分享到 QQ 互联的 appID
// U-Share SDK为了兼容大部分平台命名,统一用 appKey 和 appSecret 进行参数设置,
// 而 QQ 平台仅需将 appID 作为 U-Share 的 appKey 参数传进即可。
[[UMSocialManager defaultManager] setPlaform:UMSocialPlatformType_QQ
appKey:@"1105821097" /*设置 QQ 平台的 appID*/
appSecret:nil
redirectURL:@"http://mobile.umeng.com/social"];
// 设置新浪的 appKey 和 appSecret
[[UMSocialManager defaultManager] setPlaform:UMSocialPlatformType_Sina
appKey:@"3921700954"
appSecret:@"04b48b094faeb16683c32669824ebdad"
redirectURL:@"https://sns.whalecloud.com/sina2/callback"];
// 钉钉的 appKey
[[UMSocialManager defaultManager] setPlaform:UMSocialPlatformType_DingDing
appKey:@"dingoalmlnohc0wggfedpk"
appSecret:nil
redirectURL:nil];
// 支付宝的 appKey
[[UMSocialManager defaultManager] setPlaform:UMSocialPlatformType_AlipaySession
appKey:@"2015111700822536"
appSecret:nil
redirectURL:@"http://mobile.umeng.com/social"];
// 设置易信的 appKey
[[UMSocialManager defaultManager] setPlaform:UMSocialPlatformType_YixinSession
appKey:@"yx35664bdff4db42c2b7be1e29390c1a06"
appSecret:nil
redirectURL:@"http://mobile.umeng.com/social"];
// 设置点点虫(原来往)的 appKey 和 appSecret
[[UMSocialManager defaultManager] setPlaform:UMSocialPlatformType_LaiWangSession
appKey:@"8112117817424282305"
appSecret:@"9996ed5039e641658de7b83345fee6c9"
redirectURL:@"http://mobile.umeng.com/social"];
// 设置领英的 appKey 和 appSecret
[[UMSocialManager defaultManager] setPlaform:UMSocialPlatformType_Linkedin
appKey:@"81t5eiem37d2sc"
appSecret:@"7dgUXPLH8kA8WHMV"
redirectURL:@"https://api.linkedin.com/v1/people"];
// 设置 Facebook 的 appKey 和 UrlString
[[UMSocialManager defaultManager] setPlaform:UMSocialPlatformType_Facebook
appKey:@"506027402887373"
appSecret:nil
redirectURL:@"http://www.umeng.com/social"];
// 设置 Pinterest 的 appKey
[[UMSocialManager defaultManager] setPlaform:UMSocialPlatformType_Pinterest
appKey:@"4864546872699668063"
appSecret:nil
redirectURL:nil];
// dropbox 的 appKey
[[UMSocialManager defaultManager] setPlaform: UMSocialPlatformType_DropBox
appKey:@"k4pn9gdwygpy4av"
appSecret:@"td28zkbyb9p49xu"
redirectURL:@"https://mobile.umeng.com/social"];
// vk 的 appkey
[[UMSocialManager defaultManager] setPlaform:UMSocialPlatformType_VKontakte
appKey:@"5786123"
appSecret:nil
redirectURL:nil];
return YES;
}
//#define __IPHONE_10_0 100000
#if __IPHONE_OS_VERSION_MAX_ALLOWED > 100000
- (BOOL)application:(UIApplication *)app openURL:(NSURL *)url options:(NSDictionary<UIApplicationOpenURLOptionsKey, id> *)options {
BOOL result = [[UMSocialManager defaultManager] handleOpenURL:url];
if (!result) {
// 其他如支付等SDK的回调
}
return result;
}
#endif
- (BOOL)application:(UIApplication *)application openURL:(NSURL *)url sourceApplication:(NSString *)sourceApplication annotation:(id)annotation {
BOOL result = [[UMSocialManager defaultManager] handleOpenURL:url];
if (!result) {
// 其他如支付等SDK的回调
}
return result;
}
- (BOOL)application:(UIApplication *)application handleOpenURL:(NSURL *)url {
BOOL result = [[UMSocialManager defaultManager] handleOpenURL:url];
if (!result) {
// 其他如支付等SDK的回调
}
return result;
}
ViewController.m
#import <UMSocialCore/UMSocialCore.h>
#import <UShareUI/UShareUI.h>
#pragma mark - 第三方平台登录
- (IBAction)loginButtonClick:(UIButton *)sender {
[self getUserInfoForPlatform:UMSocialPlatformType_WechatSession];
}
- (void)getUserInfoForPlatform:(UMSocialPlatformType)platformType {
[[UMSocialManager defaultManager] getUserInfoWithPlatform:platformType
currentViewController:self
completion:^(id result, NSError *error) {
UMSocialUserInfoResponse *resp = result;
// 第三方登录数据(为空表示平台未提供)
// 授权数据
NSLog(@" uid: %@", resp.uid);
NSLog(@" openid: %@", resp.openid);
NSLog(@" accessToken: %@", resp.accessToken);
NSLog(@" refreshToken: %@", resp.refreshToken);
NSLog(@" expiration: %@", resp.expiration);
// 用户数据
NSLog(@" name: %@", resp.name);
NSLog(@" iconurl: %@", resp.iconurl);
NSLog(@" gender: %@", resp.gender);
// 第三方平台 SDK 原始数据
NSLog(@" originalResponse: %@", resp.originalResponse);
}];
}
#pragma mark - 第三方平台分享
- (IBAction)shareButtonClick:(UIButton *)sender {
[self showShareMenuView];
}
- (void)showShareMenuView {
// 设置平台顺序,只显示设置列表中的应用
[UMSocialUIManager setPreDefinePlatforms:@[@(UMSocialPlatformType_Sina),
@(UMSocialPlatformType_QQ),
@(UMSocialPlatformType_WechatSession),
@(UMSocialPlatformType_AlipaySession)]];
// 设置分享面板位置,底部 默认
[UMSocialShareUIConfig shareInstance].sharePageGroupViewConfig.sharePageGroupViewPostionType =
UMSocialSharePageGroupViewPositionType_Bottom;
// 设置分享按钮背景形状,有图片 没有圆背景
[UMSocialShareUIConfig shareInstance].sharePageScrollViewConfig.shareScrollViewPageItemStyleType =
UMSocialPlatformItemViewBackgroudType_None;
// 添加自定义分享按钮
[UMSocialUIManager addCustomPlatformWithoutFilted:UMSocialPlatformType_UserDefine_Begin+2
withPlatformIcon:[UIImage imageNamed:@"icon_circle"]
withPlatformName:@"演示 icon"];
// 显示分享面板
[UMSocialUIManager showShareMenuViewInWindowWithPlatformSelectionBlock:^(UMSocialPlatformType platformType,
NSDictionary *userInfo) {
// 根据获取的 platformType 确定所选平台进行下一步操作
[self shareTextToPlatformType:platformType];
// 在回调里面获得点击的
if (platformType == UMSocialPlatformType_UserDefine_Begin+2) {
NSLog(@"点击演示添加 Icon 后该做的操作");
dispatch_async(dispatch_get_main_queue(), ^{
UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"添加自定义 icon"
message:@"具体操作方法请参考 UShareUI 内接口文档"
delegate:nil
cancelButtonTitle:NSLocalizedString(@"确定", nil)
otherButtonTitles:nil];
[alert show];
});
} else {
}
}];
}
// 设置分享内容
- (void)shareTextToPlatformType:(UMSocialPlatformType)platformType {
// 创建分享消息对象
UMSocialMessageObject *messageObject = [UMSocialMessageObject messageObject];
// 设置文本
messageObject.text = @"社会化组件 UShare 将各大社交平台接入您的应用,快速武装 App。";
// 调用分享接口,进行分享
[[UMSocialManager defaultManager] shareToPlatform:platformType
messageObject:messageObject
currentViewController:self
completion:^(id data, NSError *error) {
if (error) {
NSLog(@"************ Share fail with error %@ *********",error);
}else{
NSLog(@"response data is %@",data);
}
}];
}
从实现iPhone的OAuth封装看国内互联网和开放平台
由于工作需要,我最近接触了现在开放平台基本都会使用的OAuth协议。我相信OAuth是很强大的,但是,终于我还是切身的领教了中国互联网的的强大之处。
我的目的是制作一个iphone平台比较通用的OAuth库封装,通过简单的配置URL和key等参数,就可以快速的支持其开放平台,包括界面也是由我来完成。原先以为都是有开源封库的,只要简单的在做一层wrapper,开放几个接口应该就可以了。可惜的是想法是美好的,现实是残酷,道路也肯定是曲折的。
我看到的OAuth
说实话,我没有比较好的研究过OAuth这个协议,大致上来说,OAuth协议的目的是让第三方在不接触到用户的账号和密码情况下可以访问第一方的资源。这想起来真是太强大了。
OAUTH认证授权就三个步骤,三句话可以概括:
1. 获取未授权的Request Token
2. 获取用户授权的Request Token
3. 用授权的Request Token换取Access Token
当应用拿到Access Token后,就可以有权访问用户授权的资源了。大家肯能看出来了,这三个步骤不就是对应OAUTH的三个URL服务地址嘛。一点没错,上面的三个步骤中,每个步骤分别请求一个URL,并且收到相关信息,并且拿到上步的相关信息去请求接下来的URL直到拿到Access Token。
开源项目
很幸运,网络上开源项目有Objective-C版本的,我好不客气的checkout了,没有很深入的研究内部的代码,不过看到代码主要还是对数据结构的封装,对URLRequest的封装,还有一些加密的处理。小做修改,就可以将其中桌面版的移植到手机版本的。不过我马上发现新浪的代码也是使用OAuthConsumer这份代码。我毫不犹豫的投入到新浪的怀抱中。
新浪微博
l 新浪的判断机制
新浪微博被认为是行业翘楚,其代码和api应该不会差到哪。例子的程序跑的挺好,可是后来看到其中一段代码和注释,我真的凌乱了。
/*********************************************************************************************************
I am fully aware that this code is chock full 'o flunk. That said:
- first we check, using standard DOM-diving, for the pin, looking at both the old and new tags for it.
- if not found, we try a regex for it. This did not work for me (though it did work in test web pages).
- if STILL not found, we iterate the entire HTML and look for an all-numeric 'word', 7 characters in length
Ugly. I apologize for its inelegance. Bleah.
*********************************************************************************************************/
- (NSString *) locateAuthPinInWebView: (UIWebView *) webView {
NSString *pin;
NSString *html = [webView stringByEvaluatingJavaScriptFromString: @"document.body.innerText"];
NSLog(@"html:%@", [webView stringByEvaluatingJavaScriptFromString: @"document.body.innerHTML"]);
if (html.length == 0) return nil;
const char *rawHTML = (const char *) [html UTF8String];
int length = strlen(rawHTML), chunkLength = 0;
for (int i = 0; i < length; i++) {
if (rawHTML[i] < '0' || rawHTML[i] > '9') {
if (chunkLength == 6) {
char *buffer = (char *) malloc(chunkLength + 1);
memmove(buffer, &rawHTML[i - chunkLength], chunkLength);
buffer[chunkLength] = 0;
pin = [NSString stringWithUTF8String: buffer];
free(buffer);
return pin;
}
chunkLength = 0;
} else
chunkLength++;
}
return nil;
}
这段代码我也没细品,但是基本可以看出是在网页的html代码中找到一串数字,找到了就认为是授权pin码,我的天啊,算了,人家也道歉了。可是我真不知道怎么来做这个协议了。
后来才发现,其实只要取到verifier就可以了,这是直接在url里面就有,不用去html里面扣。
l 新浪支持的XAuth协议
什么是XAuth呢? 我没有去考据究竟XAuth是谁的创造, 它几乎搬了大部分OAuth的内容, 不过解决了一个什么问题呢? 解决了一个 "OAuth认证必须跳转到第一方去输入密码" 的问题(美其名曰提升用户体验)。我真不知道说什么了, 第三方是可以得到用户密码的, 那它是不是直接把OAuth的第一大feature给咔嚓掉了——那basic auth不能满足你么?
l OAuth提供两种认证方式
在新浪的文档中,我还注意到了这一段话:OAuth提供两种认证方式:query-string和http headers。我们推荐使用http header进行认证。
也许是这段话,腾讯微博做出了选择,让我悲剧了好长一段时间,终于向企鹅大哥妥协了。
腾讯微博
很快,调通了新浪微博后,我就开始腾讯的历程了。可是怎么都是不能授权成功,第一步获取token就失败了。后来我在FAQ中找到一段话:
请带上所有OAuth需要的参数,并按照规范传输(暂不支持Header方式传参,请通过GET,POST方式),腾讯微博授权协议在OAuth 【RFC 5849】 标准基础上开发。第一步需带上回调URL(oauth_callback参数),不是在第二步(非同于基他微博OAuth授权)。回调url需根据[RFC3986] 所定义的百分号机制进行URL转义。
我真的怀疑腾讯为了和新浪不兼容才不支持header方式传参的。就因为这一条,我最后还是选择了腾讯的封装,而弃用了新浪的封装。
另一个问题是:在请求request_token时传递oauth_callback参数, 因为写测试没用到callback, 所以肯定库会帮我填上oob(out-of-bound), 不过让我很崩溃的是, 给我跳转到了一个这样一个地方:
https://open.t.qq.com/oauth_html/oob?oauth_token=xxxxx&oauth_verifier=xxxxxx
结果是NOT FOUND了。
然后我又看到了下一条说明:
桌面应用,手机应用,因为没有callback_url(oauth_callback参数),所以在第一步传CALLBACK时请用 oauth_callback=null代替,不然得不到 PIN码。“null”串不区分大小写。
还有腾讯的不同之处还有是拦截url中的oauth_verifier=xxxxxx的部分来继续操作。相比新浪的,也是靠谱一点,可是也就是因为这个,后面豆瓣和网易我都没能都支持。
搜狐微博
搜狐微博的支持似乎比较弱,但是中规中规中矩的,除了界面比较丑陋,没有针对手机适配,其他的很容易就走通了流程了。
网易微博
开始我尝试了网易微博的授权,没能成功。
首先我发现的明显的一个不按照标准的地方是authorization步骤, 它提供了两个接口, 一个是/oauth/authorize, 一个是/oauth/authenticate, 首先我先申明这两个单词我一直搞不清, 不过在网易给OAuth的分工中, 它们的工程师是这么定义的: authorize不处理callback, 直接展示verifier的页面; 另一个, authenticate则处理callback. (至于给第二个传oob会怎么样, 有没有返回oauth_callback_confirmed, 我都没有细看了)。
另外小提一下, 它在回调你的callback地址的时候, 传的参数名是oauth_token, 不过相信我, 我觉得那是oauth_verifier。
后来仔细研究了网易的协议后,我才发现,其实并不是每一个都需要verifier。那怎么来确定是否同意授权,只要重定向了,就可以认为用户已经授权,直接去换取accesstoeken就可以了。因为这样豆瓣的问题也同样的解决了。
还有,网易需要把重定向url加到在请求授权url时一起加上,他才不管你在header里已经传了。
最后还需要加上client_type=mobile,真是麻烦。
还有,网易的demo代码真是不知道说什么了。不过算了,毕竟人家也是贡献出来的。整个网易的objective-C代码就是把腾讯的demo改改,然后就拿出来了。不过出现了和我一样的问题,但是可能不知道重定向url的问题,出现了手动输入pin的界面,这是我做库所不能接受的,所以没有采纳。
豆瓣
开始豆瓣我也没有能够成功,他总是不会重定向到我的URL,转而跳转到其授权的界面。这大概是我使用腾讯封装导致的结果。可是我还真没好好的确认这个流程了。
后来才发现,只要在请求网页地址的时候,需要吧重定向地址加到url,这样才会正确重定向,并且和网易一样,没有verifier,直接请求就可以成功了。
豆瓣没有Objective-C的代码,没有参考,很久都没有解决,最后是解决网易的时候也一起解决的。
人人网
人人网使用的OAuth2.0,2.0的协议我没有好好的看过,因为现在只有人人支持2.0所以我偷懒的直接使用了人人的代码,所以,结果是2.0现在只支持人人授权。
人人的代码是直接冲facebooksdk改改就拿来用的,也是有时候看看也觉得真是的,一点都不专业。
一点感触
已经不记得是在什么地方看到一篇文章, 作者一上来第一句话就是 "从OAuth可以看出一个互联网企业的技术力", 虽然稍微有些偏激, 但多多少少有值得认同的地方。
OAuth的应用越来越广泛, 互联网企业拿着互联网开发的新年开发着自己的平台, 这是一件很好的事情.从开发角度来说, 不得不承认OAuth并不简单, 我也并不精通OAuth, 偷懒了用了别人的库或多或少出现了些问题.。我依然比较郁闷的是, OAuthConsumer本身自己就支持一大堆国外的OAuth, 并且在我看来代码都是没有什么问题的, 到了国内, 标准就不是标准了, 标准只是参考, 各家各户按照自己的理解在做。
国内的互联网环境真的是相当有特色的,对此,面对需求和计划,我只能说,先实现功能吧,代码写成什么样先不说了。
国内的开发环境也是比较混乱,因为基本都是代码抄来抄去,有时候并没有真正的去实现,可以想象,服务端的协议制作应该也是一样的,做协议的总是不能够静下心来做事。
由于时间比较紧,我承认我偷懒使用了很多现有的代码。也因为这样,让这个库的功能还不是那么完整,还有很多不是能够比较好的支持。中间百般折腾,给我造成了很多的困扰,自此,也就是支持新浪,腾讯,搜狐,网易,豆瓣和人人的授权。其他的还需要改进。
本文转自 arthurchen 51CTO博客,原文链接:http://blog.51cto.com/arthurchen/596195,如需转载请自行联系原作者
【Spring专题】「开发指南」OAuth2的技术体系架构和开发概览
背景介绍主要实现 OAuth2的三种授权模式:密码模式、客户端模式和授权码模式,包括展示授权服务器、资源服务器、客户端等几种角色的交互,以及JWT的整合。并且每个实例都提供两个代码版本:一个是基于旧的 Spring Security OAuth2 组件;一个是基于新的 Spring Authorization Server 组件。注意的是 password 模式由于 OAuth2.1 不推荐使用所以只能提供旧的组件代码版本,具体请参见 datatracker.ietf.org/doc/html/dr…OAuth 授权体系设计之初主要是为了解决第三方应用登录和授权的问题,但由于其严格规范的流程定义,广泛的授权通用性,且与具体技术平台无关等诸多优点,逐渐发展成为认证和授权领域的主流技术规范。但其实 OAuth2 规范归纳起来并不复杂,就四种主要的授权模式和五种角色。OAuth2 体系结构OAuth授权体系设计之初主要是为了解决第三方应用登录和授权的问题,但由于其严格规范的流程定义,广泛的授权通用性,且与具体技术平台无关等诸多优点,逐渐发展成为认证和授权领域的主流技术规范。但其实 OAuth2 规范归纳起来并不复杂,就四种主要的授权模式和五种角色。OAuth2.0 的四种授权模式授权码模式(authorization code)简化模式(implicit)密码模式(resource owner password credentials)客户端模式(client credentials)密码模式常用于外部服务的认证、授权和鉴权,客户端模式常用于内部服务的认证、授权和鉴权和开放平台应用的授权,授权码模式常用于社会化登录和SSO,因此OAuth2.0 可作为完整的统一身份认证和授权方案。四种模式都有其特定的使用场景,但是在落地过程中,也可以根据实际情况自行取舍。为了方便接下来的介绍,基于图像的物品分类系统(IBCS,Image-Based Classification System)、相册预览系统(PAPS,Photo Album Preview System)。授权码模式 vs 密码模式 vs 客户端模式授权码模式先来看授权码模式和密码模式之间的比较,授权码模式是OAuth2体系安全性最高的模式,密码模式与其相比,主要差别是少了一层用户确认授权的动作,缺乏这一动作就导致在授权阶段,用户需要把用户名密码告知客户端,造成潜在的密码泄露风险。比较项授权码模式密码模式备注适用场景不可信/第三方认证和授权、可信/内部服务认证和授权可信/内部服务认证和授权开发难度较为复杂相对简单安全性最高只要不运用于不可信/第三方场景则同样安全使用场景分析可信/内部服务场景同一套OAuth2体系的应用和服务,且这些应用和服务是由相同的或者相互信任的团队开发,也可以称作第一方应用。微服务架构下的 B2C 商城系统,基本组成有前端 H5、无线端 APP、API 网关、认证授权服务、订单服务、商品服务等,由于上述所有组成部分都同属于一套OAuth2 体系,且都由相同团队开发,那么他们全都归属于可信/内部服务场景。第三方/不可信应用网关属于安全边界,网关以内的认证授权服务、订单服务、商品服务属于内部服务,而前端H5、无线端APP则属于外部应用,如果这些外部应用是其他团队开发,我们也可以定义它们为第三方应用。这样就以网关为边界,划分出了内部服务和外部服务。电商系统的前端H5、无线端APP在认证和授权阶段,采用授权码模式和采用密码模式,授权码模式有一层用户确认授权的动作,从而避免泄露用户名和密码给第三方应用,除此之外,两者之间几乎提供了相同的安全流程。以此来看,在上述条件的限定下,两种模式的安全性打平了。遵循“简化原则”,采用密码模式即可。授权码模式授权码模式主要的应用场景,是在第三方/不可信应用的登录和授权,主要解决在不泄露用户密码的情况下如何安全授权某个应用向另一个应用提供用户资源的问题,举例来说,某第三方应用(客户端)需要获得用户(资源所有者)在另一个不可信应用(资源服务器)上的该用户的用户数据(资源)的场景就特别适合采用授权码模式。综上,选择授权码模式还是密码模式,具体要根据业务场景来确定,其中的关键决策点是应用或服务之间是否互相信任。客户端模式采用何种授权模式?首先要明确资源是什么,其次该资源是受保护的,最后资源归谁所有,谁就是资源所有者。明确资源所有者的含义后,再根据前文的分析,毫无疑问:如果PAPS的demo应用是第三方的不可信应用,则应该采用授权码模式;如果是第一方可信应用,则可以采用密码模式,当然不怕麻烦也可以用授权码模式。是密码模式的最精简架构层次,在实际开发中可以此作为基础进行扩展。密码模式涉及到五种主要角色,另外还有一个用户代理/浏览器角色。OAuth2.0 的五种主要的角色:用户代理/浏览器 User Agent客户端 Client:一般指第三方应用程序,例如用QQ登录豆瓣网站,这里的豆瓣网就是 Client;但在微服务体系里,Client通常是服务本身,如 APP 端的注册登录服务;资源所有者 Resource Owner:一般指用户,例如用 QQ 登录豆瓣网站,这里的所有者便是用户;但在微服务体系里,资源所有者的指向不是一成不变的,要具体分析;资源服务器 Resource Server (受保护资源):一般指资源所有者授权存放用户资源的服务器,例如用 QQ 登录豆瓣网站,这里的 QQ 就是资源服务器;但在微服务体系里,服务提供者本身便是资源服务器;授权服务器 Authorization Server:一般是指服务提供商的授权服务,例如用 QQ 登录豆瓣网站,这里的 QQ 便是授权服务器;类似地,在微服务体系里,IDP 服务便是授权服务器。整个流程分为两个阶段:第一阶段:认证授权阶段前端页面将用户输入的用户名和密码,发送给客户端(process-service);客户端(process-service)将用户输入的用户名和密码,连同 client_id + client_secret (由idp分配)一起发送到idp以请求令牌,如果 idp 约定了 scope 则还需要带上 scope 参数;idp 首先验证 client_id + client_secret 的合法性,再检查 scope 是否无误,最后验证用户名和密码是否正确,正确则生成 token。这一步也叫“认证”;idp 返回认证结果给客户端,认证通过返回 token,认证失败返回 401。如果认证成功则此步骤也叫“授权”;客户端收到 token 后进行暂存,并创建对应的 session;客户端颁发 cookie 给用户代理/浏览器。至此,认证授权阶段完成。其中步骤 5-6 也有其他会话方案,比如REST型应用可能会将 token 存储在浏览器端,但 session/cookie 方案无疑是最稳妥的选择。在Web 应用中\用户登录过程。第二阶段:授权后请求资源阶段用户通过用户代理,访问“我的资源”页面,用户代理携带 cookie 向客户端(process—service)发起请求;客户端通过session找到对应的 token,携带此 token 向资源服务器(resource-service)发起请求;资源服务器(resource-service)向 idp 请求验证 token 有效性;idp 校验 token 有效性,再根据 scope 判断客户端(process-service)是否有权限调用此 API,最后返回校验结果给资源服务器。这一步也叫“鉴权”;资源服务器根据 idp 检验结果(true/false 或其他等效手段)决定是否返回用户相册数据给客户端。如果 token 校验失败则返回 401 给客户端,如果 scope 检查不通过则返回 403。这一步也叫“权限控制”。至此,授权后请求资源阶段完成。scope 参数是用来约束客户端的权限的,跟用户权限(authorities)是不同的。比如可以在 idp 中利用scope参数约束某客户端只能发起读(GET)型请求,或只能调用指定的几个 API 等,具体业务逻辑自行编写。密码模式的微服务架构层次和主要流程微服务场景下,增加了一个网关,网关实际上是一个反向代理,将用户的请求转发到内部服务器。类似地,微服务场景下也分为两个阶段,而且第一阶段没什么变化,主要不同在于第二阶段:用户通过用户代理访问“我的资源”页面,用户代理携带cookie向客户端(process—service)发起请求;客户端通过session找到对应的token,携带此token向网关发起对资源服务器(resource-service)的请求;网关截取token连同本次请求的细节,一并向idp请求校验;idp 校验 token 有效性,再根据 scope 和请求细节判断客户端(process-service)是否有权限调用此 API,最后返回校验结果给网关。如果校验全部通过,idp 生成 JWT 并返回给网关;如果 token 校验失败返回 401;如果 scope 检查不通过则返回 403;如果校验通过,网关将得到 JWT,携带此 JWT 转发请求到资源服务器;资源服务器解析 JWT 得到用户信息,查询用户相册数据后返回给网关;网关将用户数据返回给客户端。网关截取 token 后向 idp 请求校验;idp 校验 token 有效性,通过校验则根据 token 查询用户信息和 scope,生成 JWT 返回给网关;如果不通过则返回 401;网关得到 JWT,解析后根据 scope 判断客户端是否有权限调用此 API,如有则携带 JWT 转发请求到资源服务器,否则返回 403 给客户端。客户端权限检查放到网关,则网关要维护 scope 和客户端权限的逻辑。授权码模式的微服务架构层次和主要流程授权码模式在微服务场景下的架构层次和主要流程:整个流程分为两个阶段:第一阶段:认证授权阶段用户在用户代理处点击登录按钮,或请求授权登录按钮,此操作将访问客户端的某个 URI;客户端(process-service)将用户导向 idp 提供的认证授权页面,并在页面 ULR 参数中携带 client_id,response_type=code,redirect_uri(可选),scope(可选),state(可选);用户通过用户代理,在 idp 的认证授权页面选择是否给予授权,如用户未登录,则需要先登录后再操作;用户给予授权,idp 将用户导向 redirect_uri 指定的页面,并附加授权码(code);如果未指定 redirect_uri,则导向发起该请求时的 URI,同时附加授权码(code);客户端收到授权码(code),向 idp 发起令牌申请,同时附上 client_id(必填) + client_secret(必填) + state(如有) + scope(如有)。注意这一步是客户端在后台发起的,用户层面无法感知;idp 收到请求后,先核对 client_id + client_secret + scope(如有)是否无误,然后校验授权码(code),全部正确后颁发 token 给客户端。第二阶段:授权后请求资源阶段用户在用户代理处点击登录按钮或请求授权登录按钮后,通知客户端(process-service),客户端收到通知后返回重定向的指示,以及 scope(可选),state(可选)等;收到响应后,直接将用户导向 idp 提供的认证授权页面,并在页面 URL 参数中携带客户端返回的参数(除了 state 参数外,其他参数可以在页面中写死)client_id,response_type=code,redirect_uri(必选),scope(可选),state(可选),其中 redirect_uri 建议是必选的,而且必须是客户端提供的 URI(回调地址)。用户在用户代理处点击登录按钮或请求授权登录按钮后,直接将用户导向 idp 提供的认证授权页面,并在页面 URL 参数中携带 client_id,response_type=code,redirect_uri(必选),scope(可选),但是不需要携带 state 参数,因为客户端不知道此参数的存在,其中 redirect_uri 建议是必选的,而且必须是客户端提供的 URI(回调地址)。至此,授权码模式的认证授权全流程完毕。
Scott 带你掌握 Nodejs 打通全栈
本文约 7000 字,通读需要花费 5 分钟
大家好,我是 Scott,本文通过提供给大家学习的方法,以及我个人录制的一系列视频,帮助你更快更好的学习 Nodejs,了解前后端的 HTTP 知识,以及配置和使用阿里云 ECS 来部署你的 Nodejs 项目,成为那个具有争议的全栈开发工程师。
先简述下被很多新入行同学问到的问题,我从业 7 年,前 4 年在阿里巴巴做前端开发工程师,开发全网的通用创意模板,近 3 年在创业公司担任技术负责人,也就是听上去高大上的 CTO,我职业的前 6 年月工资不到 1 万 5,最近一年才调薪过 2 万,我选择了一条荆棘的跨专业自学入行之路,之后又创业,弯路多多踩坑无数,希望从我的亲身经历中,大家可以对自己有更准确的定位和投资,掌握更好的技术/职业上升的方法,我走过的弯路不再是束缚你的套路。
要不要学习 Nodejs
如果你是前端开发工程师,你本地电脑上不可避免的要安装 Nodejs,作为工具也好,作为服务器也好,要帮助你做掉很多又脏又累的事情,比如 less/scss 的编译,ES6/7 到 ES5 的转换,Javascript 代码的压缩合并,切页面调试样式的热更新,无论是通过社区迅速更新换代的 Grunt/Gulp/Webpack,还是通过自己集成或者定制到本地的其他模块,Nodejs 的这个运行环境都是你得力的助手。
如果你是后端开发工程师,比如之前是开发 PHP,Java,最近转行做 Nodejs 或者想要增加一个语言技能才来学习 Nodejs,你也不可避免的要去了解 ES5/6/7 或者说 Javascipt 的整个语法概念,去浏览 Nodejs 的各个 API,然后基于 Nodejs 之上的一些流行框架,比如 Express/Koa,甚至是阿里开源的 Egg,用你既有的后端开发经验,对于 Web 服务层交互的知识,再套上这些框架的 API,来玩票性质的搭建一些子项目运行一些产品业务。
如果你是运营或者产品经理,但是你已具备一些基本的开发技能,比如 HTML/CSS/Javascript,甚至是对 Linux 主机的系统使用,域名解析也有一些经验,你可能也更愿意在不去学习另外一种全新的语言下,来借助 Nodejs 搭建你的 Web 服务,帮你的小点子小创意快速上线测试,获取一些用户的反馈或者价值验证,事实上我认识的不少产品经理对 Express/React/Vue 这些很喜欢,能高效的帮他们实现一些产品原型的测试。
以上举例,是为了说明影响你涨薪也就是职业发展的其中一个因素,就是某项技能的深度,或者是某些技能的广度,这些技能包含但不限于前端后端或者产品,不要给自己设限,从事某个工种不代表你只可以钻研这个工种,就拿前端举例,既然本地有了 Nodejs 的运行环境,那么适度的往下扩展技能树是顺水推舟的事情,而对 Nodejs 很感兴趣的无论任何职业的人来说,什么时候学习它都不晚,因为整个互联网经过几十年的发展目前的现状就是, Javascript 成为了 Web 层最容易入门使用且最被工业标准和厂商推广的语言,掌握了这个语言,就掌握了 Nodejs 的 1/3,剩下的 2/3 分别是 HTTP 知识和 Nodejs 本身的运行机制和系统能力。
我在 2016 年被杭州芋头哥邀请去大搜车做了个小分享,当时现场氛围特别好,整个杭州甚至苏州的许多做 Nodejs 的同学,其中超过 2/3 都是前端工程师,都跑来一起嗨皮,我之后又模拟录制了一遍,对于 Nodejs 不熟悉的同学依然有参考意义,大家可以听一下:
创业公司的 Nodejs 工程师
Nodejs 与 Javascript 先学哪个
Java 和 Javascript 是雷锋和雷峰塔的关系,而 Nodejs 和 javascript 可以简单的看做是雷锋塔和雷峰塔上的砖这样的关系,虽然这样比喻不够准确。
掌握 Javascript 和掌握 Nodejs 并不冲突也并没有绝对的先后顺序,因为往往我们开始使用 Nodejs,是从 Web 的层面,而这个层面,我们有很多简易入手的框架使用,比如 Express,即便不了解它的原理,以模仿性质的方式我们都可以通过阅读文档来编写一个简单的网站程序,然后基于这种迅速可见可得的体验进一步激发我们学习的兴趣,花更多的时间去从多维度反复敲打自己对于 Nodejs 的知识结构,这样一天两天慢慢就理解它的知识点了,通过实际的手写代码,运行服务器,看预览效果,再去查文档,写更复杂的代码,有些关键字不懂再去查查,这样其实就是一个不断反复不断倒带学习的过程,说是学习,其实并没有刻意的去啃 Javascript 或者 Nodejs 的语法和 API,而是自然的从工具使用走向理论构建的过程。
上面这段是面向非常初级的 Nodejs 学习者,如果已经有了其他语言的编程能力,那么入门 Javascript 也不会有太大障碍,可以先去了解 Javascript 这门语言的特点,再去尝试使用 Nodejs,在我看来,只要你对于 Javascript 和 Nodejs 是陌生的,那么学习这两个就是交替并行来回穿梭的过程,在去熟悉 Nodejs 的过程中,也就逐步的了解到了 Javascript 的语法规则,在研究 Javascript 的过程中,也就更容易领会 Nodejs 暴露出来的框架封装出来的方法函数变量他们的使用姿势和运行特点。
先看书看文档还是先找项目练手
对于学习习惯不同记忆能力不同的人来说,看文档和做项目哪个更优先,应该不会有标准答案,而且这两个在中期以后往往是交叉的,我个人的学习习惯一般是,先做项目,如果项目太难,我就把项目中难的功能都去掉,梳理出一个最简版本,以写代码为为主,以看文档作为补充,比如白天或者核心时间写代码,午间或者公交车上这些零碎时间翻文档,可以有目的的看,也可以随便挑几个感兴趣的看,这样做的缺点是,在项目一开始启动的时候,会因为不熟悉文档,走一些弯路而且比较耗脑力,它的优点是,一开始就写代码有作品雏形出来,容易激发我征服它的斗志,更有欲望把它做出来,所以这样开始的方式,往往我都不会中途放弃,而看文档的方式,我会觉得枯燥,或者不知所云,可能看看就困了放弃了,这种不太适合我,但是它的优点也很明显,如果充分的看了文档,基于原作者或者原始团队的角度理解技术点,更容易一开始就走在正确的道路上,无论是名词还是原理或者 API,心中会很有方寸,做项目的时候也容易推测出来出问题的环节去哪里找答案。
对于新人来说,先把项目做起来会比较实用,走弯路和踩的坑必然会多一些,但是这样更容易产生兴趣把硬骨头啃下来,也会提高解决问题的即时反应能力,所谓临阵不惧,运筹有度。
哪里找练手项目
练手项目从观看类型上分两种,一种是纯文字形式的,比如连载的博客,一种是视频,或者是直播或者是录播的视频,连载的博客好处是都形成了文字,并且有代码示例,看的时候可以用脑力集中消化某一个技术点,容易看懂,这同时也是它的一个缺点,因为博客中不同的技术点都用大脑消化后,就会产生一种已经理解了它的直觉,这种直觉会促使你快速的通读全完直到最后,所有的编程环节却没有动手实施,导致过了三五天后再来回想这些文字和当初所理解的概念,脑海中就会印象模糊甚至一片空白,用行话说就是没有把消化后的知识持久化。
直播的视频对个人的时间要求比较严格,同时直播中不能暂停甚至不能提问,会导致自己被动的 Push 往前走,容易遗留下一些关键的问题点没有及时消化,不过现在有的直播平台会自动存储有备份,所以也可以重播。
我个人倾向于看录播的视频,可以暂停,可以快进,可以重播,跟着屏幕敲代码大概是目前最好的一种跟随实战的方式了,录播的视频这方面有很多的学习平台,我自己平时是泡在慕课网,虽然我也是讲师,但是我也经常听别的讲师讲的课程,收获还是挺大的。
如果你对 Nodejs 已经有一定的掌握了,那么其实可以跳开一些简单的博客啊视频啊,直接上 Github 上搜一些别人开源的 Github 项目,Clone 到本地,跑起来,再去读他们的源码,这是最直接也最有效的学习方式了,但是这个对于初学者往往比较难,因为阅读源代码的前提是要有一定的编程量,上来就一通读不仅会增加理解项目的难度,更会因此失去阅读代码的兴趣。
我下面会罗列一些,我自己录制过的一些项目,或者其他的我觉得可以按照这个路线继续深入学习的项目资源。
Express 实现一个电影网站
在我入行的那个年代,一般编程的第一个练手项目,往往是一个增删改查的博客系统,或者是一个聊天室,这方面资源很多,但是我个人觉得起点略低而且提不起兴趣,于是就录制了一个开发电影网站的免费视频课程,课程地址如下:
Node+MongoDB 建站攻略(一期)
Node+MongoDB 建站攻略(二期)
这两个课程,是采用 Nodejs+MongoDB 外加一些必要的中间件,比如 Mongoose 建模,body 解析,Jade 模板等等,有前端的页面也有后端的页面,有注册登录也有密码的加盐设计,后端这里也有管理员的角色,请求豆瓣数据写入数据库包括评论的数据结构和评论流程的实现,算是一个五脏俱全的小小麻雀。
课程中用到的 Express 框架,需要理解 req 和 res 两个跟业务密切相关的知识点,一个是数据流进,一个是数据流出,然后就是 Express 的中间件机制基本是基于回调或者是基于事件的,这些都是在跟着把代码全部实现后就能理解的东西,一开始不用太纠结细节,先关注流程就行,同时因为数据存储用到了 MongoDB,项目中也使用了 Mongoose 来配置和链接数据库,来设计每一个数据模型的 Schema,可以理解为表的概念,另外由于课程录制的比较早,大家要特别留意版本,在 2 期课程中有一个补录的升级课程,可以在一期的学习中,跳过去看升级再回过头看。
或者如果不喜欢 Scott 早期课程的声音或者讲解的方式,也可以直接看下面的这个课程,里面有许多小例子,跟着敲代码不累。
Nodejs 的基础概念和 API 讲解
这个是纯面向新手的课程,地址如下:
进击Node.js基础(一)
进击Node.js基础(二)
在这两个课程中,我尝试了新的录制风格,但同时也导致了录制的时候,描述过于逗比不够严谨,还好很多看过的童鞋们还都表示很有收获,看来激起学习兴趣的确是所有市面的博客也好,视频也好都要去提升的一个点。
用 Nodejs 开发微信公众号
在微信公众号推出的那个风口,满大街都在找能开发微信公众号的工程师,而且由于公众号本身与微信打通的链路和易传播性,H5 工程师成了各个类媒体公司和创业公司的标配,但是微信公众号由于它的特殊性,需要前后端协作完成,并且需要通过微信的官方服务器进行消息的分发和中转接收,给很多公司是前端后端同学带去困扰,增加了协作的成本,于是具备 Nodejs 开发能力的前端工程师在市面上备受欢迎,因为 Java/PHP 这种传统后端可以赋予公众号纯数据的接口调用权限,而多变的业务逻辑则可以由前端自己通过 Nodejs 来自由控制,同时微信公众号里面的消息和跳转的页面自己都能完全控制,于是 Nodejs 微信开发工程师往往通过一两个人的配合,就能极高效率的完成公司在微信端的的业务实现,涉及到复杂数据或者大体积数据存储的部分,仍然可以交还给 Java/PHP 工程师来负责,从公司层面,产品做的更轻更快而且人员成本上也更省钱,从个人层面,对于产品研发的流程也更有控制权,同时个人也更有竞争力更值钱。
在现在,公众号依然是很多传统公司走向互联网化所优先考虑的方向,加上小程序的推出和一系列附加能力的扩展,这两个可以很好的协作来拉新和转化,现在具备 Nodejs 在微信的使用层面,依然是有很大的成本优势。
于是响应慕课网的需求,推出了这个收费的视频课程:
Node.js 七天搞定微信公众号
在这个课程里面,把上面的电影网站,进行了大刀阔斧的升级改造,基于原来的登录注册后台上传这些功能外
把原来的 Express 框架也切换到 Koa 框架
利用 yield 来做异步操作的执行
从零开始封装微信中间件
深入到最底层的请求逻辑
微信第三方接口调用过程
ES2015 新特性使用(Promise, Generator)
充分理解 http 请求和实现二跳换 token 请求
...
让这个微信公众号,本身可以响应用户的消息,比如上传图片/视频,文字自动回复,电影的检索和数组消息推送,还可以在公众号里面跳到电影网站的手机版本,里面可以调用语音 SDK 进行搜寻,可以基于微信的 openid 进行用户注册和评论等等,这时候原来那个电影网站的小麻雀就变成了一只犀利的老鹰,能对微信用户提供更丰富的功能和体验,而这一切的背后,仅仅是 Nodejs+MongoDB 外加 Koa 框架的支撑。
React Native App 开发
React 问世以来,特别是 React Native 问世以来,整个前端世界再一次陷入了变革狂潮,用声音或者行动宣示要搞事要运动,因循守旧的工程师被动主动的被淘汰,因时制宜的工程师和大量的前端新人,踩在老人的尸体上大步前行,无论承认与否,互联网世界一天天变得更丰富多彩,技术的圈子亦然,变革的速度快慢不重要,基于变革而能及时上车的勇气很重要,无论是 Microsoft,Facebook,Google,Instagram,还是 Alibaba, 腾讯,地球上的一线大公司的一线牛人,在过往的几年中,开源了巨量的框架,尤其在 Nodejs 和 React Native 方面,也都有了许多实践和改造,给许多的中小公司带去更多的信心,有了更多的选择。
开发 App 就是这样,世界不再是过去的非 Object-C(Swift) 即 Java,也不再是牺牲性能追求效率的 Cordova 和 PhoneGap,而是有了介于二者直接的 React Native,更多的养不起两个不同语种工程师的小公司和创业公司,开始眷顾能适应两个平台,且能满足自己业务和展现需求的 React Native,而开发一个 App 并不简单,除了肉眼可见的前台,还有作为纯 API 接口的后台,前后之间依然有种种的门槛,这时候 Nodejs 的轻便就又派上用场,具备 Nodejs 开发能力的 React Native 工程师,到今天为止依然是香饽饽。
还是顺应潮流,结合慕课网推出了这个课程,同时慕课网的官方设计师负责给 App 出插图润色,不过这个课程是收费的:
React Native贯穿全栈开发App
在这个课程中,先撇开 React Native 这个纯 App 端展现的部分,整个后端是用 Nodejs 开发的,框架依然是 Koa,涉及到了用户 token 生成获取,短信验证码,无后台的 Mock 数据,以及基于 MongoDB 的 Restful API 服务,对于音频视频资源的转码和合并都是在后端完成,里面涉及到大量的异步操作,对于一个初级和中级工程师,有非常好的实战借鉴意义。
上面发了都是我的个人作品,下面提供下其他的我认为学习 Nodejs 需要投入时间研究的知识或者方向;
培养英文读写提问的能力
有句话怎么说,编程三分靠资历,7 分靠 Google,大部分你在业务场景中遇到的技术问题,在网上都有现成的答案,问题是你不知道用什么关键词去搜出来,这个还是比较考察英文的表述能力,这些最前沿的技术,都是老外们实践出来的,我们的脑回路也应该在编程之外,多适应一下英文的思考方式,多逼自己阅读英文技术文档,随着时间慢慢就能习惯,只要解锁了英文的这把枷锁,就打开了新世界的大门,无论是从检索和阅读角度,还是从提问参与的角度。
关注 Github 仓库 Wiki 和产品榜单
我们上 Github 的时候,往往会很关注热门的框架库,那么这些框架库里往往蕴含着更有参考价值的宝藏,那就是 Wiki,比如 Express,在它的 Wiki 里面,除了有 Express 的升级指南,中间件列表之外,还有一个 Frameworks built with Express,什么意思呢,就是很多团队或者个人对 Express 不够满意,或者自己的场景更特殊,Express 不能满足,他们都基于 Express 这个框架基础或者思想来构建了一个新的框架,这些新的框架,都的基于他们的需求或者他们认为合理的方向来实现的,这些框架的源代码包括文档,大家在学习 Nodejs 的时候,可以好好研究一下,能学习到更多他人在设计框架时候考虑的方向,实现的方式,比如这些:
Feathers -- 一个迷你的数据驱动的框架
Monorail.js -- 一个极轻量级的 MVC 框架
Locomotive -- 受 Ruby on Rails 启发而开发的强大的 MVC 框架
CompoundJS -- 同样是受 Ruby on Rails 启发的高水平 MVC 框架
Calipso -- 基于 Express/Mongoose 的内容管理系统
Derby -- 一个实时的协作应用框架
Bones -- 基于 Express/Backbone 实现的 CS 应用框架
Matador -- Twitter 工程师开发的 MVC 框架
Lemmy -- 一个不可描述(爆粗口)的神秘框架
Mojito -- Yahoo 开发的 MVC 框架
综合比较一下,就能找出更适合自己用的框架,等到对框架足够熟悉的时候,就可以去阅读他们的 issues 甚至是源代码,看的多了想的多了,就越来越上道了。
全栈的最后一公里
全栈这个词很敏感,有人不认同有人无所谓,对我而言,它就像是一个 H5 工程师一样,只是表述了一种印象或者概念,具体严谨与否不重要,因为跟人沟通,心领神会即可不必强究细节。
那么在学习 Nodejs 的过程中,有一道门槛是无论如何也逃不过去的,那就是 服务器的选购配置,域名的解析指向,Nodejs 项目的部署等这些偏运维方向,这些让很多新人甚至是已经工作的工程师困扰,自己去琢磨太花费时间,不去琢磨,始终没有能力凭自己双手,让一个产品上线或者让自己的一个想法实现到互联网上,不论是成为独立站长,还是只是让朋友同学访问,于是我把这些总结了一下,以以上几个项目为例,大概分为如下几个步骤:
部署方案规划
待部署项目分析
选购及备案域名
厂商对比与选配阿里云服务器
初步 SSH 无密码登录连接和配置
搭建 Nodejs/MongoDB/Nginx 环境
配置 IPTables/Fail2Ban 防火墙及主动防御
域名 DNS 转移及 A 记录/CNAME 解析配置
MongoDB 角色配置与安全规则设置
线上 MongoDB 单表单库导入导出与跨机迁移备份上传
PM2 发布环境配置
服务器与 Git 仓库读写配置
PM2 一键部署线上 Nodejs 项目
电影网站/ReactNative App 后台/微信公众号/微信小程序后台等项目实战部署
SSL 证书申请及 Nginx 证书集成提供 HTTPS 协议
上线方案复盘总结
以上这些应该会在 4 月 10 日在 慕课网实战 板块上线,感兴趣的同学们可以关注一下,同时我也会再准备一片文章来简述整个部署过程。
编程路漫漫,与大家共勉!