最近公司要做一个sdk,仿照微博开放平台。要写移动sdk,并且采用H5页面进行授权。看了几天微博SDK源码,终于理解了微博如何做到通过H5页面授权,并回调移动端的方法返回授权码,access Token等信息,在此做个记录。
对于用户认证采用OAuth2.0协议,以下是从微博copy过来的Oauth2授权机制。
OAuth2.0协议这里不作具体分析。主要通过微博sdk的demo代码(版本:3.1.4)分析如何通过h5方式授权。
demo的授权页面
这个页面对应 WBAuthActivity
微博授权按钮操作:
// SSO 授权, 仅Web
findViewById(R.id.obtain_token_via_web).setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
mSsoHandler.authorizeWeb(new AuthListener());
}
});
这里调用了SsoHandler的authorizeWeb方法,并创建了一个回调监听。最终就是在这个回调监听中获取授权通过后的token信息。
class AuthListener implements WeiboAuthListener {
@Override
public void onComplete(Bundle values) {
// 从 Bundle 中解析 Token
mAccessToken = Oauth2AccessToken.parseAccessToken(values);
// 省略其他代码
}
@Override
public void onCancel() {
Toast.makeText(WBAuthActivity.this,
R.string.weibosdk_demo_toast_auth_canceled, Toast.LENGTH_LONG).show();
}
@Override
public void onWeiboException(WeiboException e) {
Toast.makeText(WBAuthActivity.this,
"Auth exception : " + e.getMessage(), Toast.LENGTH_LONG).show();
}
}
这个回调是何时进行,怎么进行回调的呢?
先看SsoHandler的authorizeWeb方法
(这里是Android Studio 反编译的微博weibosdkcore_release.jar,有些显示不正确,但不影响阅读)
public void authorizeWeb(WeiboAuthListener listener) {
this.authorize('胍', listener, SsoHandler.AuthType.WebOnly);
WbAppActivator.getInstance(this.mAuthActivity, this.mAuthInfo.getAppKey()).activateApp();
}
首先会调用该类的authorize方法,并将类型设置为WebOnly。 authorise方法如下
private void authorize(int requestCode, WeiboAuthListener listener, SsoHandler.AuthType authType) { this.mSSOAuthRequestCode = requestCode; this.mAuthListener = listener; boolean onlyClientSso = false; if(authType == SsoHandler.AuthType.SsoOnly) { onlyClientSso = true; } // 会走到这个分支 if(authType == SsoHandler.AuthType.WebOnly) { if(listener != null) { this.mWebAuthHandler.anthorize(listener); } } else {
// 这里进行的sso方式授权,不做分析 boolean bindSucced = this.bindRemoteSSOService(this.mAuthActivity.getApplicationContext()); if(!bindSucced) { if(onlyClientSso) { if(this.mAuthListener != null) { this.mAuthListener.onWeiboException(new WeiboException("not install weibo client!!!!!")); } } else { this.mWebAuthHandler.anthorize(this.mAuthListener); } } } }
可以看到会调用WebAuthHandler的anthorize方法
public void anthorize(WeiboAuthListener listener) { this.authorize(listener, 1); } public void authorize(WeiboAuthListener listener, int type) { this.startDialog(listener, type); } private void startDialog(WeiboAuthListener listener, int type) { if(listener != null) { WeiboParameters requestParams = new WeiboParameters(this.mAuthInfo.getAppKey()); requestParams.put("client_id", this.mAuthInfo.getAppKey()); requestParams.put("redirect_uri", this.mAuthInfo.getRedirectUrl()); requestParams.put("scope", this.mAuthInfo.getScope()); requestParams.put("response_type", "code"); requestParams.put("version", "0031405000"); String aid = Utility.getAid(this.mContext, this.mAuthInfo.getAppKey()); if(!TextUtils.isEmpty(aid)) { requestParams.put("aid", aid); } if(1 == type) {
// 这里是增加应用的包名和签名,做验证用的 requestParams.put("packagename", this.mAuthInfo.getPackageName()); requestParams.put("key_hash", this.mAuthInfo.getKeyHash()); } String url = "https://open.weibo.cn/oauth2/authorize?" + requestParams.encodeUrl(); if(!NetworkHelper.hasInternetPermission(this.mContext)) {
// 网络权限判断,不管
UIUtils.showAlert(this.mContext, "Error", "Application requires permission to access the Internet"); } else {
// 正常会进入这个分支 AuthRequestParam req = new AuthRequestParam(this.mContext); req.setAuthInfo(this.mAuthInfo); req.setAuthListener(listener); // 把listener 设置到了AuthRequestParam中,并传递到WeiboSdkBrowser这个WebView页面 req.setUrl(url); req.setSpecifyTitle("微博登录"); Bundle data = req.createRequestParamBundle(); // 这里就是把req的成员转化为了bundle Intent intent = new Intent(this.mContext, WeiboSdkBrowser.class); intent.putExtras(data); this.mContext.startActivity(intent); } } }
// 贴一下AuthRequestParam的构造函数 主要是mLaucher 这个值会用到
public AuthRequestParam(Context context) { super(context); this.mLaucher = BrowserLauncher.AUTH; }
Bundle data = req.createRequestParamBundle(); // 这里就是把req的成员转化为了bundle
这个方法有必要提一下,bundler都是键值对,listener是不会存进去的。他是怎么做的呢
createRequestParamBundle调用了AuthRequestParam父类的方法
public Bundle createRequestParamBundle() { Bundle data = new Bundle(); if(!TextUtils.isEmpty(this.mUrl)) { data.putString("key_url", this.mUrl); } if(this.mLaucher != null) { data.putSerializable("key_launcher", this.mLaucher); // 这里是传递的是BrowserLauncher.AUTH } if(!TextUtils.isEmpty(this.mSpecifyTitle)) { data.putString("key_specify_title", this.mSpecifyTitle); } this.onCreateRequestParamBundle(data); return data; }
看下AuthRequestParam的onCreateRequestParamBundle方法
public void onCreateRequestParamBundle(Bundle data) { if(this.mAuthInfo != null) { data.putBundle("key_authinfo", this.mAuthInfo.getAuthBundle()); } if(this.mAuthListener != null) { WeiboCallbackManager manager = WeiboCallbackManager.getInstance(this.mContext); this.mAuthListenerKey = manager.genCallbackKey(); manager.setWeiboAuthListener(this.mAuthListenerKey, this.mAuthListener);// 将lisener放到了WeiboCallbackManager中管理 data.putString("key_listener", this.mAuthListenerKey);// 这里其实是传递了一个listener对应的key值 } }
接下来回到H5授权页面WeiboSdkBrowser
protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if(!this.initDataFromIntent(this.getIntent())) { this.finish(); } else { this.setContentView(); this.initWebView(); if(this.isWeiboShareRequestParam(this.mRequestParam)) { this.startShare(); } else { this.openUrl(this.mUrl); } } }
这里初始化webView是WebViewClient 用的是WeiboWebViewClient 在后文给出,这里就是关键处理回调的地方
@SuppressLint({"SetJavaScriptEnabled"}) private void initWebView() { this.mWebView.getSettings().setJavaScriptEnabled(true); if(this.isWeiboShareRequestParam(this.mRequestParam)) { this.mWebView.getSettings().setUserAgentString(Utility.generateUA(this)); } this.mWebView.getSettings().setSavePassword(false); this.mWebView.setWebViewClient(this.mWeiboWebViewClient); this.mWebView.setWebChromeClient(new WeiboSdkBrowser.WeiboChromeClient((WeiboSdkBrowser.WeiboChromeClient)null)); this.mWebView.requestFocus(); this.mWebView.setScrollBarStyle(0); if(VERSION.SDK_INT >= 11) { this.mWebView.removeJavascriptInterface("searchBoxJavaBridge_"); } else { this.removeJavascriptInterface(this.mWebView); } }
private boolean initDataFromIntent(Intent data) { Bundle bundle = data.getExtras(); this.mRequestParam = this.createBrowserRequestParam(bundle); if(this.mRequestParam != null) { this.mUrl = this.mRequestParam.getUrl(); this.mSpecifyTitle = this.mRequestParam.getSpecifyTitle(); } else { String url = bundle.getString("key_url"); String specifyTitle = bundle.getString("key_specify_title"); if(!TextUtils.isEmpty(url) && url.startsWith("http")) { this.mUrl = url; this.mSpecifyTitle = specifyTitle; } } // 这里的url在createRequestParamBundle中传递所以不为空,该方法返回true if(TextUtils.isEmpty(this.mUrl)) { return false; } else { LogUtil.d(TAG, "LOAD URL : " + this.mUrl); return true; } }
进入initDataFromIntent 会调用 createBrowserRequestParam
private BrowserRequestParamBase createBrowserRequestParam(Bundle data) { this.isFromGame = Boolean.valueOf(false); Object result = null; BrowserLauncher launcher = (BrowserLauncher)data.getSerializable("key_launcher"); if(launcher == BrowserLauncher.AUTH) { // 这里就是上文提到的launcher,会走到该分支 AuthRequestParam gameRequestParam1 = new AuthRequestParam(this); gameRequestParam1.setupRequestParam(data); this.installAuthWeiboWebViewClient(gameRequestParam1); return gameRequestParam1; } else { if(launcher == BrowserLauncher.SHARE) { ShareRequestParam gameRequestParam = new ShareRequestParam(this); gameRequestParam.setupRequestParam(data); this.installShareWeiboWebViewClient(gameRequestParam); result = gameRequestParam; } else if(launcher == BrowserLauncher.WIDGET) { WidgetRequestParam gameRequestParam2 = new WidgetRequestParam(this); gameRequestParam2.setupRequestParam(data); this.installWidgetWeiboWebViewClient(gameRequestParam2); result = gameRequestParam2; } else if(launcher == BrowserLauncher.GAME) { this.isFromGame = Boolean.valueOf(true); GameRequestParam gameRequestParam3 = new GameRequestParam(this); gameRequestParam3.setupRequestParam(data); this.installWeiboWebGameClient(gameRequestParam3); result = gameRequestParam3; } return (BrowserRequestParamBase)result; } }
这里需要在介绍下AuthRequestParam的onSetupRequestParam方法
protected void onSetupRequestParam(Bundle data) { Bundle authInfoBundle = data.getBundle("key_authinfo"); if(authInfoBundle != null) { this.mAuthInfo = AuthInfo.parseBundleData(this.mContext, authInfoBundle); } // 这里获取onCreateRequestParamBundle中listener的key值从manager中取出listener this.mAuthListenerKey = data.getString("key_listener"); if(!TextUtils.isEmpty(this.mAuthListenerKey)) { this.mAuthListener = WeiboCallbackManager.getInstance(this.mContext).getWeiboAuthListener(this.mAuthListenerKey); } }
接下来是installAuthWeiboWebViewClient 方法
//这里只有两行代码,初始化webViewClient 设置回调 如此就看看AuthWeiboWebViewClient 这个类
private void installAuthWeiboWebViewClient(AuthRequestParam param) { this.mWeiboWebViewClient = new AuthWeiboWebViewClient(this, param); this.mWeiboWebViewClient.setBrowserRequestCallBack(this); }
先把AuthWeiboWebViewClient 父类代码贴出
abstract class WeiboWebViewClient extends WebViewClient { protected BrowserRequestCallBack mCallBack; WeiboWebViewClient() { } public void setBrowserRequestCallBack(BrowserRequestCallBack callback) { this.mCallBack = callback; } }
interface BrowserRequestCallBack { void onPageStartedCallBack(WebView var1, String var2, Bitmap var3); boolean shouldOverrideUrlLoadingCallBack(WebView var1, String var2); void onPageFinishedCallBack(WebView var1, String var2); void onReceivedErrorCallBack(WebView var1, int var2, String var3, String var4); void onReceivedSslErrorCallBack(WebView var1, SslErrorHandler var2, SslError var3); }
AuthWeiboWebViewClient 实际上继承的WebViewClient
class AuthWeiboWebViewClient extends WeiboWebViewClient { private Activity mAct; private AuthRequestParam mAuthRequestParam; private WeiboAuthListener mListener; private boolean isCallBacked = false; public AuthWeiboWebViewClient(Activity activity, AuthRequestParam requestParam) { this.mAct = activity; this.mAuthRequestParam = requestParam;
// 这个就是最开始那个AuthListener,传递了好多层。传递了这么久你终于要派上用场了
this.mListener = this.mAuthRequestParam.getAuthListener(); } public void onPageStarted(WebView view, String url, Bitmap favicon) { if(this.mCallBack != null) { this.mCallBack.onPageStartedCallBack(view, url, favicon); } AuthInfo authInfo = this.mAuthRequestParam.getAuthInfo(); if(url.startsWith(authInfo.getRedirectUrl()) && !this.isCallBacked) {// 微博授权结束会重定向到RedirectUrl 在这里用上了,如果是重定向页面,并且没有回调,进入该分支 this.isCallBacked = true;
// 处理方法 此方法在该类底端 this.handleRedirectUrl(url); view.stopLoading();
// 关闭H5授权页 WeiboSdkBrowser.closeBrowser(this.mAct, this.mAuthRequestParam.getAuthListenerKey(), (String)null); } else { super.onPageStarted(view, url, favicon); } } public boolean shouldOverrideUrlLoading(WebView view, String url) { if(this.mCallBack != null) { this.mCallBack.shouldOverrideUrlLoadingCallBack(view, url); } if(url.startsWith("sms:")) { Intent sendIntent = new Intent("android.intent.action.VIEW"); sendIntent.putExtra("address", url.replace("sms:", "")); sendIntent.setType("vnd.android-dir/mms-sms"); this.mAct.startActivity(sendIntent); return true; } else if(url.startsWith("sinaweibo://browser/close")) { if(this.mListener != null) { this.mListener.onCancel(); } WeiboSdkBrowser.closeBrowser(this.mAct, this.mAuthRequestParam.getAuthListenerKey(), (String)null); return true; } else { return super.shouldOverrideUrlLoading(view, url); } } public void onPageFinished(WebView view, String url) { if(this.mCallBack != null) { this.mCallBack.onPageFinishedCallBack(view, url); } super.onPageFinished(view, url); } public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) { if(this.mCallBack != null) { this.mCallBack.onReceivedErrorCallBack(view, errorCode, description, failingUrl); } super.onReceivedError(view, errorCode, description, failingUrl); } public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) { if(this.mCallBack != null) { this.mCallBack.onReceivedSslErrorCallBack(view, handler, error); } super.onReceivedSslError(view, handler, error); } private void handleRedirectUrl(String url) { Bundle values = Utility.parseUrl(url); String errorType = values.getString("error"); String errorCode = values.getString("error_code"); String errorDescription = values.getString("error_description"); if(errorType == null && errorCode == null) {
// 如果授权正常,回调onComplete,终于找到你,还好没放弃。 if(this.mListener != null) { this.mListener.onComplete(values); } } else if(this.mListener != null) {
// 如果授权失败,回调onWeiboException
this.mListener.onWeiboException(new WeiboAuthException(errorCode, errorType, errorDescription)); } }}那么onCancel在哪里调用呢
public boolean onKeyUp(int keyCode, KeyEvent event) { if(keyCode == 4) {// 返回按钮 if(this.mRequestParam != null) { this.mRequestParam.execRequest(this, 3);
// 这个mRequestParam 就是AuthRequestParam 可以看前文initDataFromIntent 对其进行的复值 } this.finish(); return true; } else { return super.onKeyUp(keyCode, event); } }
AuthRequestParam 的execRequest 方法
public void execRequest(Activity act, int action) { if(action == 3) { if(this.mAuthListener != null) {
// onCancel在这里 this.mAuthListener.onCancel(); } // 关闭WebView页面 WeiboSdkBrowser.closeBrowser(act, this.mAuthListenerKey, (String)null); } }
至此,整个H5授权过程就结束了,饶了好大一圈,不过一步一步看下去总算看明白了,第一次写源码分析,好多代码,不过最终看明白了流程感觉还是棒棒的。