蛙蛙推荐:用.net 4.0构建新浪微博开发平台应用
摘要:现在很多互联网应用都推出了开放平台,开心,人人,新浪,淘宝,豆瓣,腾讯,还有飞信也即将推出开放平台,大多数开发平台都会用OAuth认证,并提供返回json数据的Rest接口,用.NET 4.0的新特性来开发这些平台的应用有着天然的优势,一起来看看。
认证
要想使用开发平台的数据和接口,第一步肯定是登录和认证,OAuth被越来越多的开放平台所认可和使用,TerryLee已经介绍过在.NET里如何实现OAuth认证,不过是针对豆瓣平台的一些示例代码,新浪微博的OAuth认证有一些不同的地方,一会儿我会标识出来。sarlmolapple为新浪提供了新浪微博开放平台的c#版本SDK,但没有完全对新浪微博的所有接口进行支持。因此我借鉴了两位的代码写了个针对新浪微博的精简版的OAuth认证,因为简单,如果大家想开发其它开放平台的应用,也可以很快比猫画虎做出来。
首先还是要下载别人写好的OAuthBase.cs文件,然后继承该类写一些自定义的逻辑,后来发现只通过子类来实现自定义逻辑还不好使,所以迫不得已还是修改了OAuthBase.cs类,因为新浪微博开放平台要求生成OAuth签名时,多了一个Verifier参数,所以在生成basestring时(GenerateSignatureBase方法)要加上这个参数。
//NOTE:add by onlytiancai@gmail.com 兼容sina登录,参考了http://code.google.com/p/opensinaapi/
if (!string.IsNullOrEmpty(Verifier) && httpMethod == "GET")
{
parameters.Add(new QueryParameter(oAauthVerifier, Verifier));
}
准备一下OAuth认证的配置项,这些配置项大家可以在数据库里配置或者写个自定义配置节在Web.config里配置,为了简单,我就先写死了,如下
public class OAuthConfig
{
public static OAuthConfig Instance = new OAuthConfig();
public OAuthConfig()
{
RequestTokenUri = "http://api.t.sina.com.cn/oauth/request_token";
AuthorizeUri = "http://api.t.sina.com.cn/oauth/authorize";
AccessTokenUri = "http://api.t.sina.com.cn/oauth/access_token";
ApiKey = "xxxx"; //在新浪微博开放平台申请
ApiKeySecret = "xxxxxxxxxxxxxxxxxx";//在新浪微博开放平台申请
}
public string RequestTokenUri { get; set; }
public string ApiKey { get; set; }
public string ApiKeySecret { get; set; }
public string AuthorizeUri { get; set; }
public string AccessTokenUri { get; set; }
}
请求open api接口时一般要拼写PostData格式的数据,而PostData的格式是类似"?p1=a&p2=b&p3=c",这种格式,要手动拼写这样格式的字符串很麻烦,所以我们想要一种优雅的方式,比如
get_request(new {p1="a", p2="b", p3="c"})
这个参数是一个c# 3.5的匿名类,然后在get_request方法里可以通过反射获取到属性名和值,构建成PostData格式,但这样做性能会稍差一些,我们可以用另外一种相近的格式,如下
get_request(new Pairs { { "p1", "a" }, {"p2", "b"}, "p3", "c" } )
这里的参数是一个Pairs类,后面的大括号套着大括号是.net 3.5的集合初始化语法,这个Pairs是我在(Using C# 3.0 Anonymous Types as Dictionaries )这篇博客的评论里看到的,我略微修改了一下,如下
public class Pairs : List<KeyValuePair<String, String>>
{
public void Add(String key, String value)
{
Add(new KeyValuePair<string, string>(key, value));
}
public string ToPostData(Func<string, string> valueHandler = null)
{
StringBuilder sb = new StringBuilder();
foreach (var pair in this)
{
if (sb.Length > 0)
sb.Append("&");
sb.AppendFormat("{0}={1}", pair.Key, valueHandler == null
? pair.Value : valueHandler(pair.Value));
}
return sb.ToString();
}
}
我只增加了一个ToPostData的方法,其中有一个可选的参数用来在拼接PostData时对参数值进行处理,可选参宿是.net 4.0提供的机制,如果不提供这个参数,我就按原文拼写参数值
我继承OAuthBase写了一个OAuthHelper类,其中OAuthRequest方法用来发送一个OAuth请求,如下
public string OAuthRequest(string uri, Pairs data = null, string method = "GET")
{
//1、准备获取签名所需参数
string nonce = GenerateNonce();
string timeStamp = GenerateTimeStamp();
if (data == null)
data = new Pairs();
data.Add("source", OAuthConfig.Instance.ApiKey); //添加默认的source参数
uri += uri.IndexOf("?") > 0 ? "&" : "?";
uri += data.ToPostData(str => UrlEncode(str)); //防止参数里有",%,="等字符
//2、获取签名
string normalizeUrl, normalizedRequestParameters;
string sig = GenerateSignature(
new Uri(uri),
OAuthConfig.Instance.ApiKey,
OAuthConfig.Instance.ApiKeySecret,
Token,
TokenSecret,
method,
timeStamp,
nonce,
OAuthBase.SignatureTypes.HMACSHA1,
out normalizeUrl,
out normalizedRequestParameters);
normalizedRequestParameters = normalizedRequestParameters + "&"
+ OAuthSignatureKey + "=" + HttpUtility.UrlEncode(sig);
//3、发送WEB请求并接受数据
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(
method == "POST" ? normalizeUrl : normalizeUrl + "?" + normalizedRequestParameters);
request.Method = method;
if (method == "POST")
{
using (StreamWriter sw = new StreamWriter(request.GetRequestStream()))
{
sw.Write(normalizedRequestParameters);
}
}
using (HttpWebResponse response = (HttpWebResponse)request.GetResponse())
{
using (StreamReader reader = new StreamReader(request.GetResponse()
.GetResponseStream(), System.Text.Encoding.UTF8))
{
return reader.ReadToEnd();
}
}
}
具体步骤我都做了注释,其中GenerateNonce,GenerateTimeStamp,UrlEncode,GenerateSignature都是父类提供的方法,直接调用即可,根据名字也很容易想出它的功能,如果你做的不是新浪微博开放平台的应用,这里可能要相应的做一些小改动。
然后是进行OAuth的三步认证,第一步获取RequestToken,第二步让用户进行授权,一般情况下会提示用户登录新浪微博或者提示用户授权或拒绝授权你的应用访问用户的新浪微博数据,第三步就是拿到AccessToken去访问受限的资源,具体过程大家可以看这里。有了上面我们写的方法,这三步就很简单了。
/// <summary>
/// 登录第一步:获取RequestToken
/// </summary>
public void GetRequestToken()
{
string response = OAuthRequest(OAuthConfig.Instance.RequestTokenUri);
if (response.Length > 0)
{
NameValueCollection qs = HttpUtility.ParseQueryString(response);
if (qs[OAuthTokenKey] != null)
{
this.Token = qs[OAuthTokenKey];
this.TokenSecret = qs[OAuthTokenSecretKey];
}
}
}
/// <summary>
/// 登录第二部:Response.Redirect到这个页面,提示用户授权
/// </summary>
/// <param name="callbackUri">用户认证成功后返回的地址</param>
/// <returns></returns>
public string GetAuthorizeUri(string callbackUri)
{
return string.Format("{0}?oauth_token={1}&&oauth_callback={2}",
OAuthConfig.Instance.AuthorizeUri,
this.Token, callbackUri);
}
/// <summary>
/// 登录第三部:获取AccessToken,成功登录
/// </summary>
public void GetAccessToken()
{
string response = OAuthRequest(OAuthConfig.Instance.AccessTokenUri);
if (response.Length > 0)
{
NameValueCollection qs = HttpUtility.ParseQueryString(response);
if (qs[OAuthTokenKey] != null)
{
this.Token = qs[OAuthTokenKey];
this.TokenSecret = qs[OAuthTokenSecretKey];
}
}
}
其中Token和TokenSecret是OAuthHelper的两个string类型成员。
获取JSON数据
一般开放平台的接口可以返回xml格式或JSON格式的数据,如果解析XML数据会很不通用,每一个接口需要写一个方法,而返回json的话.NET 4.0提供的JavaScriptSerializer可以对json数据进行序列化和反序列化,json格式的数据可以反序列化成Dictionary<string, object>类型或其类型的数组,其中object又是一个Dictionary<string, object>,但我们对Dictionary的操作会很麻烦,我们想办法吧它转换成一个c# 4.0的dynamic类型,这里有篇帖子(Turning JSON into a ExpandoObject )介绍了如何去做,我在此基础上改动了一下,并提供了一个直接把open api返回的数据转换成ExpandoObject的方法,如下。
/// <summary>
/// 发送OAuth请求,并把返回的json结果转换成dynamic类型
/// </summary>
/// <param name="uri">请求地址,请确保返回json数据</param>
/// <param name="data">请求数据</param>
/// <param name="method">HttpMethod</param>
/// <returns></returns>
public ExpandoObject OAuthRequestDynamic(string uri, Pairs data = null, string method = "GET")
{
string result = OAuthRequest(uri, data, method);
if (result == null)
return null;
return Expando(result);
}
public static ExpandoObject Expando(string json)
{
JavaScriptSerializer serializer = new JavaScriptSerializer();
object obj = serializer.DeserializeObject(json);
if (obj is ICollection)
return Expando(new Dictionary<string, object>() { { "arr", obj } });
else
return Expando((Dictionary<string, object>)obj);
}
//http://coderjournal.com/2010/07/turning-json-into-a-expandoobject/
private static ExpandoObject Expando(IDictionary<string, object> dictionary)
{
var expando = new ExpandoObject();
var expandoDic = (IDictionary<string, object>)expando;
foreach (var item in dictionary)
{
bool alreadyProcessed = false;
if (item.Value is IDictionary<string, object>)
{
expandoDic.Add(item.Key, Expando((IDictionary<string, object>)item.Value));
alreadyProcessed = true;
}
else if (item.Value is ICollection)
{
var itemList = new List<object>();
foreach (var item2 in (ICollection)item.Value)
if (item2 is IDictionary<string, object>)
itemList.Add(Expando((IDictionary<string, object>)item2));
else
itemList.Add(Expando(new Dictionary<string, object> { { "Unknown", item2 } }));
if (itemList.Count > 0)
{
expandoDic.Add(item.Key, itemList);
alreadyProcessed = true;
}
}
if (!alreadyProcessed)
expandoDic.Add(item);
}
return expando;
}
这下可好了,访问opan api,直接返回的就是带类型的了,省了解析的工序了,和Python差不多,直接写出成员就行。
开发第一个应用
去新浪微博开放平台注册一个账户,得到app key和app key secret,然后就可以开发测试应用了,新建一个Default.aspx页,摆放如下控件。
<div>
<asp:Button ID="btnLogin" runat="server" Text="登录" OnClick="btnLogin_Click" />
<asp:Button ID="btnTimeline" runat="server" OnClick="btnTimeline_Click" Text="timeline" />
<p>
<asp:Label ID="lblMessage" runat="server" Text="未登录"></asp:Label>
</p>
<asp:Repeater ID="rpt1" runat="server">
<ItemTemplate>
<p>
<img src="<%# Eval("user.profile_image_url") %>" alt="" /><b><%# Eval("user.name") %>:</b><%# Eval("text")%>(<%# Eval("created_at")%>)
</p>
</ItemTemplate>
</asp:Repeater>
</div>
控件的功能根据名字可以推测出来,先看登录
protected void btnLogin_Click(object sender, EventArgs e)
{
OAuthHelper oauth = new OAuthHelper();
oauth.GetRequestToken();
Session["oauth_token"] = oauth.Token;
Session["oauth_token_secret"] = oauth.TokenSecret;
string url = oauth.GetAuthorizeUri("http://localhost:1812/");
Response.Redirect(url);
}
先获取RequestToken,然后获取用户认证页地址,重定向过去,提示用户登录,用户登录完了之后会返回到calback uri,这里填写是本页,新浪会在返回的页面上提供oauth_verifier参数,如果有这个参数就说明是用户登录成功返回的,所以我们要在Page_Load里做一些处理,困了,就不写那么多说明了,大家看看,大多都是借鉴了sarlmolapple的示例代码。
protected void Page_Load(object sender, EventArgs e)
{
Session[Guid.NewGuid().ToString()] = 1; //生成cookie里的sessionid,防止Redirect回来丢失Session
if (!Page.IsPostBack)
{
OAuthHelper oauth = new OAuthHelper();
if (Request["oauth_verifier"] != null)
{
oauth.Token = Session["oauth_token"].ToString();
oauth.TokenSecret = Session["oauth_token_secret"].ToString();
oauth.Verifier = Request["oauth_verifier"];
oauth.GetAccessToken();
Session["oauth_token"] = oauth.Token; ;
Session["oauth_token_secret"] = oauth.TokenSecret;
Response.Redirect("/Default.aspx");
}
if (Session["oauth_token"] != null)
{
this.lblMessage.Text = "登录成功";
this.btnLogin.Visible = false;
this.btnTimeline.Visible = true;
}
else
{
this.lblMessage.Text = "未登录";
this.btnLogin.Visible = true;
this.btnTimeline.Visible = false;
}
}
}
登录成功后,我们获取public_timeline,并直接绑定在Repeater上,不做任何解析,你猜能不能用。
protected void btnTimeline_Click(object sender, EventArgs e)
{
OAuthHelper oauth = new OAuthHelper();
oauth.Token = Session["oauth_token"].ToString();
oauth.TokenSecret = Session["oauth_token_secret"].ToString();
dynamic json = oauth.OAuthRequestDynamic("http://api.t.sina.com.cn/statuses/public_timeline.json",
new Pairs { { "count", "5" } });
rpt1.DataSource = json.arr;
rpt1.DataBind();
}
猜对了,不能用,但是加上如下代码就能用了
protected new object Eval(string expression)
{
string[] arr = expression.Split('.');
object obj = this.Page.GetDataItem();
IDictionary<string, object> items = obj as IDictionary<string, object>;
if (items != null)
{
if (arr.Length == 1)
return items[arr[0]];
else if (arr.Length == 2)
return ((IDictionary<string, object>)items[arr[0]])[arr[1]];
else
throw new NotSupportedException("哥,表达式too长了,不支持呀。");
}
else return DataBinder.Eval(this.Page.GetDataItem(), expression);
}
以上代码是在网上找到的一段,出处忘了,我在它的基础上修改了一下,支持了json的二级数据的绑定,可以直接在Eval里写“user.name”这样的表达式。
public_timeline返回的json样子大概是如下
[
{
"created_at" : "Tue Nov 30 14:34:35 +0800 2010",
"text" : "吃力不讨好的事情我是坚决不会再做了,RI你个仙人!发飙~~~~我只想说档次和素质在那里去了,你也就只能在这种地方混!",
"truncated" : false,
"in_reply_to_status_id" : "",
"annotations" :
[
],
"in_reply_to_screen_name" : "",
"geo" : null,
"user" :
{
"name" : "习惯寂寞吗",
"domain" : "",
"geo_enabled" : true,
"followers_count" : 5,
"statuses_count" : 61,
"favourites_count" : 0,
"city" : "1",
"description" : "",
"verified" : false,
"id" : 1676792942,
"gender" : "f",
"friends_count" : 26,
"screen_name" : "习惯寂寞吗",
"allow_all_act_msg" : false,
"following" : false,
"url" : "http://1",
"profile_image_url" : "http://tp3.sinaimg.cn/1676792942/50/1284648784",
"created_at" : "Wed Dec 30 00:00:00 +0800 2009",
"province" : "51",
"location" : "四川 成都"
},
"favorited" : false,
"in_reply_to_user_id" : "",
"id" : 3978753419,
"source" : "<a href=\"http://t.sina.com.cn\" rel=\"nofollow\">新浪微博</a>"
},
...
]
最后来张图吧