本文主要介绍OAuth的用处、OAuth的流程、腾讯微博OAuth认证示例(新浪、人人类似)以及一些认证的异常。
1、OAuth介绍
目前很多主流的用户权限认证都是用OAuth,像google、microsoft、yahoo、人人、新浪微博、腾讯微博。只不过各自使用的OAuth版本可能略有不同。
使用OAuth的一个好处就是在用户向服务器数据请求时,避免了每次都需要传输用户名和密码,通过access token和secret使得用户在正常访问数据的同时保证了用户帐号的安全性。
OAuth比较适合的web应用程序和提供服务器端api或者两者混合的场景,OAuth支持目前大部分的主流语言。
更多关于OAuth见:http://www.oauth.net
2、OAuth流程
OAuth的流程最终的结果是为了得到可以访问数据的access token和ccess secret(可能没有),以后就通过此access token和access secret和服务器进行交互。
大致的流程分为三步(OAuth1.0和2.0可能有点差异):
a 先获得一个未授权的request token,或者叫request code
b 以上步的未授权的token换取授权的request token和request secret(可能没有),这一步之后一般会提示输入用户名、密码
c 使用上步授权后的request token换取access token和access secret(可能没有)
现在就得到了access token和ccess secret(可能没有),使用它们就可以同服务器交互访问数据,而不用每次传递用户名和密码
3、腾讯微博OAuth api介绍
目前腾讯微博使用的是OAuth1.0、新浪微博使用的是OAuth2.0、人人网使用的是OAuth2.0,这里只介绍腾讯微博,关于人人和新浪类似,大家可以自己修改。
因自己写的腾讯微博sdk中默认不带oauth认证过程,不少朋友问到如何进行认证,这里就大致贴代码介绍下,有点长,可看下大概明白意思,自己再根据需要精简。主要分为三个部分:
第一部分:调用认证函数,跳转到认证页面
认证函数如下
private static QqTSdkService qqTSdkService = new QqTSdkServiceImpl(); /** * OAuth部分参见http://wiki.open.t.qq.com/index.php/API%E6%96%87%E6%A1%A3#category_1 */ @Override public Intent auth(Context context, String callBackUrl) { Intent intent = new Intent(); Bundle bundle = new Bundle(); QqTAppAndToken qqTAppAndToken = new QqTAppAndToken(); qqTAppAndToken.setAppKey(APP_KEY); qqTAppAndToken.setAppSecret(APP_SECRET); qqTSdkService.setQqTAppAndToken(qqTAppAndToken); Map<String, String> requestTokenMap = qqTSdkService.getUnAuthorizedRequestToken(callBackUrl); if (!MapUtils.isEmpty(requestTokenMap) && requestTokenMap.containsKey(QqTConstant.PARA_OAUTH_TOKEN)) { Map<String, String> parasMap = new HashMap<String, String>(); parasMap.put(QqTConstant.PARA_OAUTH_TOKEN, requestTokenMap.get(QqTConstant.PARA_OAUTH_TOKEN)); bundle.putString(SnsConstant.OAUTH_URL, HttpUtils.getUrlWithParas(QqTConstant.GET_AUTHORIZATION_URL, parasMap)); bundle.putString(SnsConstant.CALL_BACK_URL, callBackUrl); bundle.putString(SnsConstant.REQUEST_TOKEN_SECRET, requestTokenMap.get(SnsConstant.REQUEST_TOKEN_SECRET)); intent.putExtras(bundle); intent.setClass(context, OAuthWebViewActivity.class); } return intent; }
a. 两个参数第一个为activity中getApplicationContext();得到的context
第二个为认证成功返回的url,对于android的activity格式为"appName://activityClassName",其中appname
为应用名,activityClassName为activity的类名。为了认证后能正确跳转到activity,需要在AndroidManifest.xml中添加相应的activity的intent-filter如下,相当于host配置
<intent-filter> <data android:scheme="appName" android:host="activityClassName" /> </intent-filter>
b. QqTSdkService、MapUtils、QqTConstant、HttpUtils的引用见腾讯微博java(android) api
c. SnsContant 中的一些常量定义如下
/** 程序中用到的一些字符串常量 **/ public static final String WEBSITE_TYPE = "websiteType"; public static final String OAUTH_URL = "oAuthUrl"; public static final String CALL_BACK_URL = "callBackUrl"; public static final String REQUEST_TOKEN_SECRET = "oauth_token_secret"; public static final String STATUS_ID = "statusId"; public static final String COMMENT_TYPE = "commentType"; public static final String COMMENT_ID = "commentId";
d. OAuthWebViewActivity的就是认证页面,代码见第二部分
activity中调用认证函数
Intent intent = auth(context, "appName://activityClassName"); if (intent == null || intent.getExtras() == null || !intent.getExtras().containsKey(SnsConstant.CALL_BACK_URL)) { // Toast.makeText(this, "进入认证页面失败", Toast.LENGTH_SHORT).show(); return; } else { startActivity(intent); }
第二部分:进入认证页面
OAuthWebViewActivity的代码如下,就是一个webview加载授权页面
package com.trinea.sns.activity; import android.app.Activity; import android.content.Intent; import android.graphics.Bitmap; import android.net.Uri; import android.net.http.SslError; import android.os.Bundle; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; import android.view.View.OnTouchListener; import android.view.Window; import android.webkit.SslErrorHandler; import android.webkit.WebChromeClient; import android.webkit.WebSettings; import android.webkit.WebView; import android.webkit.WebViewClient; import com.trinea.sns.util.CodeRules; import com.trinea.sns.util.SnsConstant; /** * 认证的webView * * @author Trinea 2012-3-20 下午08:42:41 */ public class OAuthWebViewActivity extends Activity { private WebView authWebView = null; private Intent intent = null; private String callBackUrl; private String requestTokenSecret; public static OAuthWebViewActivity webInstance = null; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); requestWindowFeature(Window.FEATURE_PROGRESS); setContentView(R.layout.web_view); setTitle("腾讯微博授权认证"); webInstance = this; authWebView = (WebView)findViewById(R.id.authWebView); WebSettings webSettings = authWebView.getSettings(); webSettings.setJavaScriptEnabled(true); webSettings.setSaveFormData(true); webSettings.setSavePassword(true); webSettings.setSupportZoom(true); webSettings.setBuiltInZoomControls(true); webSettings.setCacheMode(WebSettings.LOAD_NO_CACHE); authWebView.setOnTouchListener(new OnTouchListener() { @Override public boolean onTouch(View v, MotionEvent event) { authWebView.requestFocus(); return false; } }); // 根据传递过来的信息,打开相应的授权页面 intent = this.getIntent(); if (!intent.equals(null)) { Bundle bundle = intent.getExtras(); if (bundle != null && bundle.containsKey(SnsConstant.OAUTH_URL)) { authWebView.loadUrl(bundle.getString(SnsConstant.OAUTH_URL)); if (bundle.getString(SnsConstant.CALL_BACK_URL) != null) { callBackUrl = bundle.getString(SnsConstant.CALL_BACK_URL); } if (bundle.getString(SnsConstant.REQUEST_TOKEN_SECRET) != null) { requestTokenSecret = bundle.getString(SnsConstant.REQUEST_TOKEN_SECRET); } authWebView.setWebChromeClient(new WebChromeClient() { public void onProgressChanged(WebView view, int progress) { setTitle("腾讯微博授权页面加载中,请稍候..." + progress + "%"); setProgress(progress * 100); if (progress == 100) { setTitle(R.string.app_name); } } }); authWebView.setWebViewClient(new WebViewClient() { @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { view.loadUrl(url); return true; } @Override public void onPageStarted(WebView webView, String url, Bitmap favicon) { if (url != null && url.startsWith(callBackUrl)) { Class backClass = CodeRules.getActivityClass(CodeRules.getActivityNameFromUrl(callBackUrl)); if (backClass != null) { Intent intent = new Intent(OAuthWebViewActivity.this, backClass); Bundle backBundle = new Bundle(); backBundle.putString(SnsConstant.REQUEST_TOKEN_SECRET, requestTokenSecret); intent.putExtras(backBundle); Uri uri = Uri.parse(url); intent.setData(uri); startActivity(intent); } } } }); } } } @Override protected void onPause() { super.onPause(); } @Override protected void onResume() { super.onResume(); } @Override protected void onStop() { super.onStop(); } @Override protected void onDestroy() { super.onDestroy(); } /** * 监听BACK键 * * @param keyCode * @param event * @return */ @Override public boolean onKeyDown(int keyCode, KeyEvent event) { if (event.getKeyCode() == KeyEvent.KEYCODE_BACK && event.getRepeatCount() == 0) { if (authWebView.canGoBack()) { authWebView.goBack(); } else { // OAuthActivity.webInstance.finish(); finish(); } return true; } return super.onKeyDown(keyCode, event); } }
a. R.layout.web_view为
<?xml version="1.0" encoding="utf-8"?> <ScrollView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <WebView android:layout_height="wrap_content" android:layout_width="wrap_content" android:id="@+id/authWebView"> </WebView> </ScrollView>
b. CodeRules.getActivityClass(CodeRules.getActivityNameFromUrl(callBackUrl));作用是从url中获得到activity对应的类,方便在跳转回activity,即根据"appName://activityClassName"得到activityClassName的Class
c. public void onPageStarted(WebView webView, String url, Bitmap favicon)表示监听webView页面开始加载事件
if (url != null && url.startsWith(callBackUrl)) 表示认证已经成功,开始加载callBackUrl("appName://activityClassName"),这个时候我们让它跳转到对应的activity,这个时候的url中已经包含了accessToken和accessSecret
在第一部分startActivity后跳转到认证页面,填入帐号和密码并点击授权便可进入上面c的onPageStarted,这个时候我们已经得到了accessToken和accessSecret
第三部分 认证返回处理
在返回的activity中添加OnNewIntent函数,需要在AndroidManifest.xml中添加相应的activity的属性android:launchMode="singleTask"
@Override protected void onNewIntent(Intent intent) { super.onNewIntent(intent); Bundle bundle = intent.getExtras(); if (bundle != null) { UserInfo userInfo = authBack(intent.getData(), bundle.getString(SnsConstant.REQUEST_TOKEN_SECRET)); if (userInfo != null) { Toast.makeText(this, "获取用户信息失败,请重新验证", Toast.LENGTH_SHORT).show(); OAuthWebViewActivity.webInstance.finish(); } else { Toast.makeText(this, "获取用户信息失败,请重新验证", Toast.LENGTH_SHORT).show(); } } }
其中authBack函数如下
@Override public UserInfo authBack(Uri uri, String requestTokenSecret) { if (uri == null) { return null; } QqTAppAndToken qqTAppAndToken = new QqTAppAndToken(); qqTAppAndToken.setAppKey(SnsConstant.QQT_APP_KEY); qqTAppAndToken.setAppSecret(SnsConstant.QQT_APP_SECRET); qqTSdkService.setQqTAppAndToken(qqTAppAndToken); Map<String, String> requestTokenMap = qqTSdkService.getAuthorizedRequestToken(uri.getQuery()); if (MapUtils.isEmpty(requestTokenMap) || !requestTokenMap.containsKey(QqTConstant.PARA_OAUTH_TOKEN) || !requestTokenMap.containsKey(QqTConstant.PARA_OAUTH_VERIFIER)) { return null; } Map<String, String> accessTokenMap = qqTSdkService.getAccessToken(requestTokenMap.get(QqTConstant.PARA_OAUTH_TOKEN), requestTokenMap.get(QqTConstant.PARA_OAUTH_VERIFIER), requestTokenSecret); if (!MapUtils.isEmpty(accessTokenMap) || accessTokenMap.containsKey(QqTConstant.PARA_OAUTH_TOKEN) || accessTokenMap.containsKey(QqTConstant.PARA_OAUTH_TOKEN_SECRET)) { return UserInfoUtils.createUserInfo(websiteType, null, accessTokenMap.get(QqTConstant.PARA_OAUTH_TOKEN), accessTokenMap.get(QqTConstant.PARA_OAUTH_TOKEN_SECRET)); } return null; }
a. UserInfo类代码如下
package com.trinea.sns.entity; import java.io.Serializable; /** * 验证后存储在数据库中的用户信息类 * * @author Trinea 2012-3-13 上午01:08:30 */ public class UserInfo implements Serializable { private static final long serialVersionUID = -2402890084981532871L; /** 用户id,可能对于某些网站类型为空 **/ private String userId; /** access token **/ private String accessToken; /** access secret **/ private String accessSecret; /** 网站类型 **/ private String websiteType; /** 用户是否已经被选中 **/ private boolean isSelected; /** * 得到用户id,可能对于某些网站类型为空 * * @return the userId */ public String getUserId() { return userId; } /** * 设置用户id * * @param userId */ public void setUserId(String userId) { this.userId = userId; } /** * 得到accessToken * * @return the accessToken */ public String getAccessToken() { return accessToken; } /** * 设置accessToken * * @param accessToken */ public void setAccessToken(String accessToken) { this.accessToken = accessToken; } /** * 得到accessSecret * * @return the accessSecret */ public String getAccessSecret() { return accessSecret; } /** * 设置accessSecret * * @param accessSecret */ public void setAccessSecret(String accessSecret) { this.accessSecret = accessSecret; } /** * 得到网站类型 * * @return the websiteType */ public String getWebsiteType() { return websiteType; } /** * 设置网站类型 * * @param websiteType */ public void setWebsiteType(String websiteType) { this.websiteType = websiteType; } /** * 设置用户是否已经被选中 * * @param isSelected */ public void setSelected(boolean isSelected) { this.isSelected = isSelected; } /** * 得到用户是否已经被选中 * * @return the isSelected */ public boolean isSelected() { return isSelected; } }
b. createUserInfo代码如下
public static UserInfo createUserInfo(String websiteType, String... userInfo) { if (ArrayUtils.isEmpty(userInfo)) { return null; } UserInfo user = new UserInfo(); user.setUserId((userInfo.length > 0 && userInfo[0] != null) ? userInfo[0] : websiteType); user.setAccessToken(userInfo.length > 1 ? userInfo[1] : null); user.setAccessSecret((userInfo.length > 2 && userInfo[2] != null) ? userInfo[2] : websiteType); user.setWebsiteType(websiteType); return user; }
到此大功告成,如果想使用腾讯微博android sdk,请见http://trinea.iteye.com/blog/1299505
4、其他
腾讯微博认证异常
向https://open.t.qq.com/cgi-bin/request_token获取未授权的access token出现如下异常
java.lang.Exception: javax.net.ssl.SSLHandshakeException: org.bouncycastle.jce.exception.ExtCertPathValidatorException: Could not validate certificate signature.
原因应该是以上请求的ssl证书已经不可用,将https改为http即可,如http://open.t.qq.com/cgi-bin/request_token
已有 0 人发表留言,猛击->>这里<<-参与讨论
ITeye推荐