OAuth认证协议原理分析及同步消息到Twitter和Facebook使用方法
OAuth有什么用?为什么要使用OAuth?
twitter或豆瓣用户一定会发现,有时候,在别的网站,点登录后转到 twitter登录,之后转回原网站,你会发现你已经登录此网站了,这种网站就是这个效果。其实这都是拜 OAuth所赐。
OAuth(开放授权)是一个开放标准,允许用户让第三方应用访问该用户在某一网站上存储的私密的资源(如照片,视频,联系人列表),而无需将用户名和密码提供给第三方应用。
OAuth 协议为用户资源的授权提供了一个安全的、开放而又简易的标准。与以往的授权方式不同之处是OAUTH的授权不会使第三方触及到用户的帐号信息(如用户名与密 码),即第三方无需使用用户的用户名与密码就可以申请获得该用户资源的授权,因此OAUTH是安全的。同时,任何第三方都可以使用OAUTH认证服务,任 何服务提供商都可以实现自身的OAUTH认证服务,因而OAUTH是开放的。业界提供了OAUTH的多种实现如PHP,JavaScript,Java,Ruby等各种语言开发包,大大节约了程序员的时间,因而OAUTH是简易的。目前互联网很多服务如Open API,很多大头公司如Google,Yahoo,Microsoft等都提供了OAUTH认证服务,这些都足以说明OAUTH标准逐渐成为开放资源授权的标准。
认证和授权过程
在认证和授权的过程中涉及的三方包括:
- 服务提供方,用户使用服务提供方来存储受保护的资源,如照片,视频,联系人列表(如twitter角色)。
- 用户 ,存放在服务提供方的受保护的资源的拥有者。
- Consumer ,要访问服务提供方资源的第三方应用(中间商,类似上面的 twitterfeed 角色)。在认证过程之前,客户端要向服务提供者申请客户端标识。
注意到用户提交密码是在第四步 而第三方Consumer从头至尾没有获得账号信息
Oauth的流程 官方的说明图如下:
使用OAuth进行认证和授权的过程如下所示:
- 用户访问客户端的网站,想操作自己存放在服务提供方的资源。
- 客户端向服务提供方请求一个临时令牌。
- 服务提供方验证客户端的身份后,授予一个临时令牌。
- 客户端获得临时令牌后,将用户引导至服务提供方的授权页面请求用户授权。在这个过程中将临时令牌和客户端的回调连接发送给服务提供方。
- 用户在服务提供方的网页上输入用户名和密码,然后授权该客户端访问所请求的资源。
- 授权成功后,服务提供方引导用户返回客户端的网页。
- 客户端根据临时令牌从服务提供方那里获取访问令牌 。
- 服务提供方根据临时令牌和用户的授权情况授予客户端访问令牌。
- 客户端使用获取的访问令牌访问存放在服务提供方上的受保护的资源。
一,Consumer 向 服务提供商 申请接入权限
可得到:Consumer Key,Consumer Secret。twitter申请oauth的话,在 setting - connection - developer 里面申请。 同时给出三个访问网址:
- request_token_url = 'http://twitter.com/oauth/request_token'
- access_token_url = 'http://twitter.com/oauth/access_token'
- authorize_url = 'http://twitter.com/oauth/authorize'
二,当Consumer接到用户请求想要访问第三方资源(如twitter)的时候
Consumer需要先取得 请求另牌(Request Token)。网址为上面的 request_token_url,参数为:
- oauth_consumer_key:Consumer Key
- oauth_signature_method:签名加密方法
- oauth_signature:加密的签名 (这个下面细说)
- oauth_timestamp:UNIX时间戳
- oauth_nonce:一个随机的混淆字符串,随机生成一个。
- oauth_version:OAuth版本,可选,如果设置的话,一定设置为 1.0
- oauth_callback:返回网址链接。
- 及其它服务提供商定义的参数
这样 Consumer就取得了 请求另牌(包括另牌名 oauth_token,另牌密钥 oauth_token_secret。
三,浏览器自动转向服务提供商的网站:
网址为authorize_url?oauth_token=请求另牌名
四,用户同意 Consumer访问 服务提供商资源
那么会自动转回上面的 oauth_callback 里定义的网址。同时加上 oauth_token (就是请求另牌),及oauth_verifier(验证码)。
五,现在总可以开始请求资源了吧?
NO。现在还需要再向 服务提供商 请求 访问另牌(Access Token)。网址为上面的access_token_url,参数为:
- oauth_consumer_key:Consumer Key
- oauth_token:上面取得的 请求另牌的名
- oauth_signature_method:签名加密方法
- oauth_signature:加密的签名 (这个下面细说)
- oauth_timestamp:UNIX时间戳
- oauth_nonce:一个随机的混淆字符串,随机生成一个。
- oauth_version:OAuth版本,可选,如果设置的话,一定设置为 1.0
- oauth_verifier:上面返回的验证码。
- 请求 访问另牌的时候,不能加其它参数。
这样就可以取得 访问另牌(包括Access Token 及 Access Token Secret)。这个就是需要保存在 Consumer上面的信息(没有你的真实用户名,密码,安全吧!)
六,取得 访问另牌 后
Consumer就可以作为用户的身份访问 服务提供商上被保护的资源了。提交的参数如下:
- oauth_consumer_key:Consumer Key
- oauth_token:访问另牌
- oauth_signature_method:签名加密方法
- oauth_signature:加密的签名 (这个下面细说)
- oauth_timestamp:UNIX时间戳
- oauth_nonce:一个随机的混淆字符串,随机生成一个。
- oauth_version:OAuth版本,可选,如果设置的话,一定设置为 1.0
- 及其它服务提供商定义的参数
OAuth2.0
OAuth2.0和OAuth1.0的区别还是在于简化了认证过程,不需要从未授权的Request Token转化到授权Request Token,而是利用app key通过用户授权生成access token但是,与1.0的不同之处是access token有自身的有效期,且不同平台、不同级别的程序有着不同的有效期,在程序开发中一定记得判断access token是否过期,对于过期之后的处理方法主要是利用access token和refresh token重新生成access token或者重新利用app key向服务器发送请求生成access token。由于这个问题,与OAuth1.0基本一致不一样,各个平台OAuth2.0做了不一样的选择。
OAuth2.0服务支持以下获取Access Token的方式:
a. Authorization Code:Web Server Flow,适用于所有有Server端配合的应用。
b. Implicit Grant:User-Agent Flow,适用于所有无Server端配合的应用。
因为demo是无服务器的程式,所以我们采用Implicit Grant:User-Agent Flow的获取方式。
示意图(来自腾讯微博开发文档)
获取Access Token
为了获取Access Token,应用需要将用户浏览器(或手机/桌面应用中的浏览器组件)到OAuth2.0授权服务的“http://xxxxxxxxx/authorize”地址上,并带上以下参数:
参数名 | 必选 | 介绍 |
client_id | true | 申请组件时获得的API Key |
response_type | true | 此值固定为“token” |
redirect_uri | true | 授权后要回调的URI,即接受code的URI。对于无Web Server的应用,其值可以是“oob”。 |
scope | false | 以空格分隔的权限列表,若不传递此参数,代表请求默认的basic权限。如需调用扩展权限,必需传递此参数 |
state | false | 用于保持请求和回调的状态,授权服务器在回调时(重定向用户浏览器到“redirect_uri”时),会在Query Parameter中原样回传该参数 |
display | false | 登录和授权页面的展现样式,默认为“page”。手机访问时,此参数无效 |
client | false | 是否为手机访问。手机访问:client=1;不是手机,无需次参数 |
若用户登录并接受授权,授权服务将重定向用户浏览器到“redirect_uri”,并在Fragment中追加如下参数:
参数名 | 介绍 |
access_token | 要获取的Access Token |
expires_in | Access Token的有效期,以秒为单位 |
refresh_token |
用于刷新Access Token 的 Refresh Token,一些平台不返回这个参数,需要程序员进行判断处理 |
scope |
Access Token最终的访问范围,即用户实际授予的权限列表 |
state | 如果请求获取Access Token时带有state参数,则将该参数原样返回 |
1、首先是到登录地址 https://login.facebook.com/login.php?login_attempt=1 嘛,存储好 cookies 并且从页面解析出一个 lsd 的参数,然后再次向此地址提交登录参数,包括 charset_test=%E2%82%AC%2C%C2%B4%2C%E2%82%AC%2C%C2%B4%2C%E6%B0%B4%2C%D0%94%2C%D0%84、return_session=0、legacy_return=1、display=、session_key_only=0、trynum=1、email、pass、persistent=1、login=%E7%99%BB%E5%BD%95、lsd。
2、在登录成功之后保存 cookies 转向 http://www.facebook.com/home.php ,然后从此页面解析出 profile_id、 composer_id、post_form_id 、fb_dtsg 4个参数出来。
3、最后一步向 http://www.facebook.com/ajax/updatestatus.php?__a=1 提交状态信息,需要包括以下参数 action=HOME_UPDATE、home_tab_id=1、profile_id、status、target_id=0、app_id、privacy_data[value]=80、privacy_data[friends]=0、privacy_data[list_anon]=0、privacy_data[list_x_anon]=0、composer_id、composer_id、hey_kid_im_a_composer=true、display_context=home、post_form_id、fb_dtsg、lsd、_log_display_context=home、ajax_log=1、post_form_id_source=AsyncRequest 。
以下的程序段呢,如果出问题还请参照一下以前的几篇文章,比如使用代理地址,比如访问 https 需要注意的地方等。
private void update_facebook(string user, string password, string message)
{
string service = "Facebook";
this.BeginInvoke(new UpdateStatusDelegate(StateInfo), new object[] { "正在将消息发布到 " + service });
CookieContainer cc = new CookieContainer();
string lsd = "";
Uri uri = new Uri("https://login.facebook.com/login.php?login_attempt=1");
HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(uri);
request.UserAgent = "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)";
request.ContentType = "application/x-www-form-urlencoded";
request.Method = "GET";
request.CookieContainer = cc;
if (!string.IsNullOrEmpty(proxyserver))
{
request.Proxy = new WebProxy(proxyserver);
ServicePointManager.ServerCertificateValidationCallback = new System.Net.Security.RemoteCertificateValidationCallback(CheckValidationResult);
}
try
{
using (HttpWebResponse response = (HttpWebResponse)request.GetResponse())
{
foreach (Cookie cookie in response.Cookies)
{
cc.Add(cookie);
}
Stream responseStream = response.GetResponseStream();
StreamReader reader = new StreamReader(responseStream, Encoding.GetEncoding("UTF-8"));
string result = reader.ReadToEnd();
Match re = Regex.Match(result, @"name=\u0022lsd\u0022\svalue=\u0022([^\u0022]*)\u0022()");
lsd = re.Groups[].ToString();
}
}
catch (Exception ex)
{
ErrorMessage("获取登录到 "+ service +" 必要参数时出现意外:\r\n"+ex.Message);
return;
}
StringBuilder postData = new StringBuilder();
postData.Append("charset_test=%E2%82%AC%2C%C2%B4%2C%E2%82%AC%2C%C2%B4%2C%E6%B0%B4%2C%D0%94%2C%D0%84");
postData.Append("&lsd=" + lsd + "&return_session=0&legacy_return=1&display=&session_key_only=0&trynum=1");
postData.Append("&email=" + Utility.UrlEncode(user) + "&pass=" + Utility.UrlEncode(password) + "&persistent=1&login=%E7%99%BB%E5%BD%95");
byte[] bs = Encoding.UTF8.GetBytes(postData.ToString());
request = (HttpWebRequest)HttpWebRequest.Create(uri);
request.UserAgent = "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)";
request.ContentType = "application/x-www-form-urlencoded";
request.Method = "POST";
request.ContentLength = bs.Length;
request.CookieContainer = cc;
request.AllowAutoRedirect = true;
if (!string.IsNullOrEmpty(proxyserver))
{
request.Proxy = new WebProxy(proxyserver);
}
try
{
using (Stream requestStream = request.GetRequestStream())
{
requestStream.Write(bs, , bs.Length);
requestStream.Close();
}
using (HttpWebResponse response = (HttpWebResponse)request.GetResponse())
{
foreach (Cookie cookie in response.Cookies)
{
cc.Add(cookie);
}
response.Close();
}
}
catch (Exception ex)
{
ErrorMessage("登录到 " + service + " 时出现意外:\n"+ex.Message);
return;
}
uri = new Uri("http://www.facebook.com/home.php");
request = (HttpWebRequest)HttpWebRequest.Create(uri);
request.UserAgent = "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)";
request.ContentType = "application/x-www-form-urlencoded";
request.Method = "GET";
request.CookieContainer = cc;
if (!string.IsNullOrEmpty(proxyserver))
{
request.Proxy = new WebProxy(proxyserver);
}
string profile_id = "", composer_id = "", post_form_id = "", fb_dtsg = "";
try
{
using (HttpWebResponse response = (HttpWebResponse)request.GetResponse())
{
foreach (Cookie cookie in response.Cookies)
{
cc.Add(cookie);
}
Stream responseStream = response.GetResponseStream();
StreamReader reader = new StreamReader(responseStream, Encoding.GetEncoding("UTF-8"));
string result = reader.ReadToEnd();
Match re = Regex.Match(result, @"name=\u0022profile_id\u0022\svalue=\u0022([^\u0022]*)\u0022()");
profile_id = re.Groups[].ToString();
re = Regex.Match(result, @"name=\u0022composer_id\u0022\svalue=\u0022([^\u0022]*)\u0022()");
composer_id = re.Groups[].ToString();
re = Regex.Match(result, @"name=\u0022post_form_id\u0022\svalue=\u0022([^\u0022]*)\u0022()");
post_form_id = re.Groups[].ToString();
re = Regex.Match(result, @"name=\u0022fb_dtsg\u0022\svalue=\u0022([^\u0022]*)\u0022()");
fb_dtsg = re.Groups[].ToString();
response.Close();
}
}
catch (Exception ex)
{
ErrorMessage("重定向facebook页面并获取发布消息所需参数时出现意外:\n"+ex.Message);
return;
}
postData = new StringBuilder();
postData.Append("action=HOME_UPDATE&home_tab_id=1&profile_id=" + profile_id);
postData.Append("&status=" + Utility.UrlEncode(message) + "&target_id=0&app_id=");
postData.Append("&privacy_data[value]=80&privacy_data[friends]=0&&&&&privacy_data[list_anon]=0&&privacy_data[list_x_anon]=0");
postData.Append("&&composer_id=" + composer_id + "&hey_kid_im_a_composer=true&display_context=home");
postData.Append("&post_form_id=" + post_form_id + "&fb_dtsg=" + fb_dtsg+"&lsd="+lsd);
postData.Append("&_log_display_context=home&ajax_log=1&post_form_id_source=AsyncRequest");
bs = Encoding.UTF8.GetBytes(postData.ToString());
uri = new Uri("http://www.facebook.com/ajax/updatestatus.php?__a=1");
request = (HttpWebRequest)HttpWebRequest.Create(uri);
request.UserAgent = "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)";
request.ContentType = "application/x-www-form-urlencoded";
request.Method = "POST";
request.CookieContainer = cc;
request.ContentLength = bs.Length;
if (!string.IsNullOrEmpty(proxyserver))
{
request.Proxy = new WebProxy(proxyserver);
}
try
{
using (Stream requestStream = request.GetRequestStream())
{
requestStream.Write(bs, , bs.Length);
requestStream.Close();
}
}
catch (Exception ex)
{
ErrorMessage("发布消息到 facebook 时出现意外:\n"+ex.Message);
return;
}
}
Code
Twitter 更新消息时注意加入 postBody 参数就可以叻,更多细节可以对照 ”简版 OAuthr 认证 for C#“
/// <summary>、
/// OAuth 认证更新twitter消息
/// </summary>
/// <param name="consumer_key">应用的consumer_key</param>
/// <param name="consumer_secret">应用的consumer_secret</param>
/// <param name="oauth_token">应用的access_key</param>
/// <param name="oauth_token_secret">应用的access_secret</param>
/// <param name="message">发送的消息</param>
/// <param name="request_path">请求的API</param>
private void update_twitter(
string consumer_key,
string consumer_secret,
string oauth_token,
string oauth_token_secret,
string message,
string request_path)
{
string service = "推特";
System.Net.ServicePointManager.Expect100Continue = false;
this.BeginInvoke(new UpdateStatusDelegate(StateInfo), new object[] { "正在将消息发布到 " + service });
string postData = "status=" + Utility.UrlEncode(message);
byte[] bs = Encoding.UTF8.GetBytes(postData);
Dictionary<string, string> param = new Dictionary<string, string>();
param = OAuth.RequestParams(
consumer_key,
consumer_secret,
oauth_token,
oauth_token_secret,
request_path,
"POST",
postData,
null);
string head_string = OAuth.Dict2Header(param);
try
{
HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(request_path);
request.ContentType = "application/x-www-form-urlencoded";
request.Method = "POST";
request.ContentLength = bs.Length;
if (!string.IsNullOrEmpty(proxyserver))
{
request.Proxy = new WebProxy(proxyserver);
ServicePointManager.ServerCertificateValidationCallback = new System.Net.Security.RemoteCertificateValidationCallback(CheckValidationResult);
}
request.Headers.Add("Authorization", "OAuth realm=\"http://t.yunmengze.net\"," + head_string);
using (Stream reqStream = request.GetRequestStream())
{
reqStream.Write(bs, , bs.Length);
reqStream.Close();
}
}
catch (Exception e)
{
ErrorMessage("将消息发布到 " + service + " 时出现意外,建议暂时取消这一服务的同步:\r\n" + e.Message, );
return;
}
}
Code
如果使用代理,加上一段
public bool CheckValidationResult(object sender, X509Certificate certificate, X509Chain chain, SslPolicyErrors errors)
{
//直接确认,否则打不开
return true;
}
Code
OAuth安全机制是如何实现的?
OAuth 使用的签名加密方法有 HMAC-SHA1,RSA-SHA1 (可以自定义)。拿 HMAC-SHA1 来说吧,HMAC-SHA1这种加密码方法,可以使用 私钥 来加密 要在网络上传输的数据,而这个私钥只有 Consumer及服务提供商知道,试图攻击的人即使得到传输在网络上的字符串,没有 私钥 也是白搭。
私钥是:consumer secret&token secret (哈两个密码加一起)
要加密的字符串是:除 oauth_signature 外的其它要传输的数据。按参数名字符排列,如果一样,则按 内容排。如:domain=kejibo.com&oauth_consumer_key=XYZ& word=welcome………………….
前面提的加密里面都是固定的字符串,那么攻击者岂不是直接可以偷取使用吗?
不,oauth_timestamp,oauth_nonce。这两个是变化的。而且服务器会验证一个 nonce(混淆码)是否已经被使用。
那么这样攻击者就无法自已生成 签名,或者偷你的签名来使用了。
关于开发文档
- 新浪:http://open.weibo.com/wiki/%E9%A6%96%E9%A1%B5
- 空间:http://wiki.opensns.qq.com/wiki/%E3%80%90QQ%E7%99%BB%E5%BD%95%E3%80%91%E6%96%87%E6%A1%A3%E8%B5%84%E6%BA%90
- 腾讯:http://wiki.open.t.qq.com/index.php/%E9%A6%96%E9%A1%B5
- 人人:http://wiki.dev.renren.com/wiki/%E9%A6%96%E9%A1%B5
- 开心:http://open.kaixin001.com/document.php
- 豆瓣:http://www.douban.com/service/apidoc/
- 搜狐:http://open.t.sohu.com/en/%E9%A6%96%E9%A1%B5
- 网易:http://open.t.163.com/wiki/index.php?title=%E9%A6%96%E9%A1%B5
参考:
OAuth认证协议原理分析及同步消息到Twitter和Facebook使用方法的更多相关文章
- 深入理解HTTP协议、HTTP协议原理分析【转】
转自:http://blog.csdn.net/lmh12506/article/details/7794512 版权声明:本文为博主原创文章,未经博主允许不得转载. 目录(?)[-] 基础概念篇 ...
- 前端必须会的!!!关于对HTTP协议的理解、HTTP协议原理分析
http协议学习系列 1. 基础概念篇 1.1 介绍 HTTP是Hyper Text Transfer Protocol(超文本传输协议)的缩写.它的发展是万维网协会(World Wide Web C ...
- QUIC协议原理分析(转)
之前深入了解了一下HTTP1.1.2.0.SPDY等协议,发现HTTP层怎么优化,始终要面对TCP本身的问题.于是了解到了QUIC,这里分享一篇之前找到的有意义的文章. 原创地址:https://mp ...
- HTTP协议、HTTP协议原理分析
百度百科中说明: 超文本传输协议(HTTP,HyperText Transfer Protocol)是互联网上应用最为广泛的一种网络协议.所有的WWW文件都必须遵守这个标准.设计HTTP最初的目的是为 ...
- Oauth认证协议
原文地址腾讯QQ第三方登录的实现原理? Oauth当中的角色: 1.Service Provider(服务提供方): 服务提供方通常是网站,在这些网站当中存储着一些受限制的资源,如照片.视频.联系人列 ...
- 红外 IR 协议原理分析
1.概述:对多种红外遥控器的信号进行分析,其发出的红外指令中,引导码各不相同,而且后面的控制指令也有较大差别,甚至指令码的位数也不相同,原因是这些红外设计没有遵守相同的红外标准.但是其基本思想是相同的 ...
- 深入理解HTTP协议、HTTP协议原理分析
http://blog.csdn.net/g1036583997/article/details/50457441
- OAuth认证协议中的HMACSHA1加密算法
<?php function hmacsha1($key,$data) { $blocksize=64; $hashfunc='sha1'; if (strlen($key)>$block ...
- 公网API安全--OAuth认证
之前写过一个基于签名的公网API访问安全控制,另一种方式是基于OAuth认证协议做安全控制. 说明 用户访问A客户端,使用B的服务及资源.B只有征得用户的授权,才允许A客户端使用B上用户的资源和服务. ...
随机推荐
- 利用JqGrid结合ashx及EF分页显示列表之二
上一篇文章简单利用JqGrid及ashx进行一个数据列表的显示,要文的重点是利用EF的分页与JqGrid进行结合,EF本文只是简单运用所以没有很规范,重点还是JqGrid分页的实现;本实例把JqGri ...
- Python 对字符串切片
对字符串切片字符串 'xxx'和 Unicode字符串 u'xxx'也可以看成是一种list,每个元素就是一个字符.因此,字符串也可以用切片操作,只是操作结果仍是字符串:>>> 'A ...
- UI组件之TextView及其子类(五)计时器Chronometer
Chronometer直接继承了TextView组件,它会显示一段文本,显示从某个事实上时间開始.一共过了多长时间.我们看Chronometer的源代码: watermark/2/text/aHR0c ...
- OPENERP 构建动态视图
来自:http://shine-it.net/index.php/topic,16142.0.html 在openerp展示界面通常是通过定义class的view(xml文件)来实现的. 有时这种方法 ...
- SQL Server 性能调优(方法论)【转】
目录 确定思路 wait event的基本troubleshooting 虚拟文件信息(virtual file Statistics) 性能指标 执行计划缓冲的使用 总结 性能调优很难有一个固定的理 ...
- 一款基于SSM框架技术的全栈Java web项目(已部署可直接体验)
概述 此项目基于SSM框架技术的Java Web项目,是全栈项目,涉及前端.后端.插件.上线部署等各个板块,项目所有的代码都是自己编码所得,每一步.部分都有清晰的注释,完全不用担心代码混乱,可以轻松. ...
- mybatis WARN No appenders could be found for logger的解决方法
log4j:WARN No appenders could be found for logger (org.apache.ibatis.logging.LogFactory).log4j:WARN ...
- 温度测量【RTD】
原理 温度是表征物体冷热程度的物理量,它可以通过物体随温度变化的某些特性(如电阻.电压变化等特性)来间接测量,通过研究发现,金属铂(Pt) 的阻值跟温度的变化成正比,并且具有很好的重现性和稳定性,利用 ...
- screen常用命令
1. 背景 由于经常使用ssh登录实验室的服务器训练神经网络, 而一些复杂的神经网络模型需要长时间训练,在此期间,如果出现网络等原因出现链接中断的话,服务器的进程也会被杀死,之前的一切半途而废.利用s ...
- ngRoute 和 ui.router 的使用方法和区别
在单页面应用中要把各个分散的视图给组织起来是通过路由机制来实现的.本文主要对 AngularJS 原生的 ngRoute 路由模块和第三方路由模块 ui.router 的用法进行简单介绍,并做一个对比 ...