天天看點

解讀某OAuth 2.0的開源示例android-oauth-app

解讀某OAuth 2.0的開源示例android-oauth-app

  • OpenContextApps的android-oauth-app是學習OAuth認證時候找到的一個開源代碼. 是4年前的一個Android OAuth 2.0 Demo Application. 認證的位址都已經失效了, 但是還是值得一看的.
  • 項目的github位址: https://github.com/OpenConextApps/android-oauth-app

簡介:

This mobile application is able to connect to web service secured with OAuth 2.0.

這個app能夠使用OAuth 2.0進行web service安全認證.

OAuth Properties 關于OAuth屬性的配置檔案

The properties file located at res/raw/demo.properties contains the OAuth configuration parameters: 在res/raw/demo.properties中有OAuth的配置參數.

authorize_url=https://frko.surfnetlabs.nl/workshop/php-oauth/authorize.php
authorize_response_type=code
authorize_grant_type=authorization_code
authorize_client_id=oauth-mobile-app
authorize_scope=grades
authorize_redirect_uri=oauth-mobile-app://callback
token_url=https://frko.surfnetlabs.nl/workshop/php-oauth/token.php
token_grant_type=authorization_code
webservice_url=https://frko.surfnetlabs.nl/workshop/php-oauth-grades-rs/api.php/grades/@me
           

You can modify them for instance to use your own environment. 你可以根據你自己的情況修改它們.

下面開始分析過程:

分析manifest檔案

<application
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme" >
        <activity
            android:name=".StartActivity"
            android:label="@string/title_activity_start" >
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
        <activity
            android:name=".SchemeCaptureActivity"
            android:exported="true"
            android:label="@string/title_activity_scheme_capture"
            android:permission="android.permission.INTERNET" >
            <intent-filter>
                <action android:name="android.intent.action.VIEW" />
                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="android.intent.category.BROWSABLE" />
                <data android:scheme="oauth-mobile-app" />
            </intent-filter>
        </activity>
    </application>
           

隻有兩個activity. 注意到SchemeCaptureActivity中可以單獨為Activity設定網絡通路權限. 另外注意到intent-filter的設定

<intent-filter>
    <action android:name="android.intent.action.VIEW" />
    <category android:name="android.intent.category.DEFAULT" />
    <category android:name="android.intent.category.BROWSABLE" /> //指定該Activity能被浏覽器安全調用
    <data android:scheme="oauth-mobile-app" />
</intent-filter>
           
  • 這裡表示android:scheme指定接受Uri的Scheme為oauth-mobile-app, BROWSABLE的意思就是浏覽器在特定條件下可以打開你的activity.
  • 整個流程是StartActivity打開浏覽器, 然後浏覽器在特定的條件下打開SchemeCaptureActivity.

分析StartActivity類

解讀某OAuth 2.0的開源示例android-oauth-app
/**
 * The Home Activity to start the application with. The current settings can be
 * shown here. With the start button the Authentication flow will be started.
 * The scheme of the redirect_url will be catch by The other Activity
 * (SchemeCaptureActivity).
 * 程式的主Activity. 将顯示目前設定. 點選開始按鈕将開始認證流程. redirect_url将會被SchemeCaptureActivity捕獲.
 *
 * @author jknoops @ iprofs.nl
 */
public class StartActivity extends Activity {
    private AuthenticationDbService service;
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Log.v("demo.SActivity", "Starting Demo Application");
        setContentView(R.layout.activity_start);
        Log.d("demo.SActivity", "Loading properties");
        service = AuthenticationDbService.getInstance(this); //加載屬性值
        Log.d("demo.SActivity", "Initialiing the screen.");
        EditText editTextUrl = (EditText) findViewById(R.id.editText_autorize_url);
        editTextUrl.setText(service.getAuthorize_url()); //設定認證位址
        EditText editTextResponseType = (EditText) findViewById(R.id.editText_response_type);
        editTextResponseType.setText(service.getAuthorize_response_type()); //是指響應類型
        EditText editTextClientId = (EditText) findViewById(R.id.editText_client_id);
        editTextClientId.setText(service.getAuthorize_client_id());//設定認證用戶端id
        EditText editTextRedirectUri = (EditText) findViewById(R.id.editText_redirect_uri);
        editTextRedirectUri.setText(service.getAuthorize_redirect_uri()); //設定重定向uri
        Button startButton = (Button) findViewById(R.id.button_start);
        startButton.setOnClickListener(new View.OnClickListener() {
            public void onClick(View v) {
                //拼接OAuth認證位址
                StringBuilder sb = new StringBuilder();
                // basic authorize
                sb.append(service.getAuthorize_url());
                // response type
                sb.append("?");
                sb.append("response_type=");
                sb.append(service.getAuthorize_response_type());
                // client_id
                sb.append("&");
                sb.append("client_id=");
                sb.append(service.getAuthorize_client_id());
                // scope
                sb.append("&");
                sb.append("scope=");
                sb.append(service.getAuthorize_scope());
                // redirect
                sb.append("&");
                sb.append("redirect_uri=");
                sb.append(service.getAuthorize_redirect_uri());
                String url = sb.toString();
                Log.d("demo.SActivity", "Starting (Starting class) with url = " + url);
                //拼接之後的url為https://frko.surfnetlabs.nl/workshop/php-oauth/authorize.php?response_type=code&client_id=oauth-mobile-app&scope=grades&redirect_uri=oauth-mobile-app://callback
                Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
                startActivity(i);
            }
        });
    }
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.activity_start, menu);
        return true;
    }
}
           
  • StartActivity從AuthenticationDbService對象中獲得了很多資料. 這些資料是從properties中或則從網絡請求中獲得的資料, 然後儲存在SP檔案中.

分析AuthenticationDbService類

/**
 * A Helper class for storing/retrieving some data. The tokens are stored in a
 * Preferences file. The properties are loaded from the properties file.
 * 存儲/擷取一些資料的幫助類. token值被存儲在sp檔案中. 屬性值儲存在properties檔案中.
 */
public class AuthenticationDbService {
    /**
     * the local AuthenticationDbService.
     * 本地驗證資料服務對象
     */
    private static AuthenticationDbService _instance;
    private static Context _context;
    //聲明Oauth認證中要使用的參數
    private static final String ACCESS_TOKEN = "access_token"; //通路的token
    private static final String REFRESH_TOKEN = "refresh_token"; //重新整理的token
    private static final String TOKEN_TYPE = "token_type"; //token類型
    private static final String EXPIRES_IN = "expires_in"; //過期時長
    private static final String SCOPE = "scope"; //範圍
    private static final String EXPIRES_IN_LONG = "expires_in_long"; //過期時間的毫秒值
    public static final String RESPONSE_TYPE_TOKEN = "token"; //響應類型token
    public static final String RESPONSE_TYPE_CODE = "code"; //響應類型code
    /**
     * The properties from demo.properties are loaded inside this Properties.
     * 從demo.properties中獲得屬性值
     */
    private Properties demoProperties;
    /**
     * The values for the tokens are stored inside the preferences file.
     * 這些token值儲存在sp中
     */
    private SharedPreferences mPrefs;
    /**
     * Static getInstance() method for Singleton pattern.
     * 單例模式
     *
     * @return the AuthenticationDbService
     */
    public static AuthenticationDbService getInstance(Context context) {
        if (_instance == null) {
            _context = context;
            _instance = new AuthenticationDbService();
        }
        return _instance;
    }
    /**
     * Private constructor
     * 私有化構造方法,從sp中讀取配置資訊
     */
    private AuthenticationDbService() {
        mPrefs = _context.getSharedPreferences("OpenConext.demo",
                Context.MODE_PRIVATE);
        loadProperties(); //加載屬性值
    }
    /**
     * Loading properties from the file system.
     * 從屬性檔案中獲得屬性
     */
    private void loadProperties() {
        Resources resources = _context.getResources();
        // Read from the /res/raw directory
        //從 /res/raw目錄讀取配置資訊
        try {
            //openRawResource()讀取raw中檔案為流的形式
            InputStream rawResource = resources.openRawResource(R.raw.demo);
            demoProperties = new Properties();
            demoProperties.load(rawResource); //将值到流中
            Log.d("demo.DbService", "The properties are now loaded");
            Log.v("demo.DbService", "properties: " + demoProperties);
        } catch (NotFoundException e) {
            Log.e("demo.DbService.error", "Did not find raw resource: " + e);
        } catch (IOException e) {
            Log.e("demo.DbService.error", "Failed to open demo property file");
        }
    }
    /**
     * Retrieving the refresh token from the local storage on the device.
     * 獲得refresh_token
     */
    public String getRefreshToken() {
        return mPrefs.getString(REFRESH_TOKEN, null);
    }
    /**
     * Storing the refresh token into the local storage on the device.
     * 将refresh_token儲存到sp中
     */
    public void setRefreshToken(final String token) {
        Editor editor = mPrefs.edit();
        editor.putString(REFRESH_TOKEN, token);
        editor.commit();
    }
    /**
     * Retrieving the access token from the local storage on the device. If
     * there is a value "expires in" is stored, there will be checked if the
     * access token is still valid. When there is no "expires in" or the access
     * token is still valid, the access token will be returned. Otherwise an
     * empty access token will be returned.
     * <p/>
     * 從本地獲得access_token. 如果存儲了過期時間資訊, 那麼将要檢查access_token的有效性.
     * 當沒有過期,或者access_token仍然有效, 将會傳回access_token. 否則将會傳回空值.
     */
    public String getAccessToken() {
        //獲得過期時間
        long expires_in_long = mPrefs.getLong(EXPIRES_IN_LONG, -);
        //如果sp不包含expires_in鍵值, 則直接獲得access_token
        if (!mPrefs.contains(EXPIRES_IN)) {
            return mPrefs.getString(ACCESS_TOKEN, null);
        }
        //如果沒有獲得過期時間則access_token為空
        if (expires_in_long == -) {
            return "";
        }
        //獲得目前時間的毫秒值
        long now_long = new Date().getTime();
        //判斷過期時間是否大于目前時間
        if (expires_in_long > now_long) {
            Log.v("access_token", "Expires in " + (expires_in_long - now_long)
                    + " milliseconds");
            //沒有過期則傳回access_token
            return mPrefs.getString(ACCESS_TOKEN, null);
        }
        Log.v("access_token", "Overtime " + (now_long - expires_in_long)
                + " milliseconds");
        return "";
    }
    /**
     * 設定access_token
     */
    public void setAccessToken(final String token) {
        Editor editor = mPrefs.edit();
        editor.putString(ACCESS_TOKEN, token);
        editor.commit();
    }
    /**
     * 獲得token_type
     */
    public String getTokenType() {
        return mPrefs.getString(TOKEN_TYPE, null);
    }
    /**
     * 設定token_type
     */
    public void setTokenType(final String type) {
        Editor editor = mPrefs.edit();
        editor.putString(TOKEN_TYPE, type);
        editor.commit();
    }
    /**
     * 獲得expires_in過期時長
     */
    public Integer getExpiresIn() {
        return mPrefs.getInt(EXPIRES_IN, -);
    }
    /**
     * 設定expires_in過期時長,和expires_in_long過期時間的毫秒值
     * 注意:expires_in和expires_in_long總是一起儲存的或修改的
     */
    public void setExpiresIn(final int expiresIn) {
        //獲得當期時間的毫秒值
        Date nowDate = new Date();
        long nowLong = nowDate.getTime();
        //計算expires值
        long expiresLong = nowLong + (l * expiresIn);
        //寫入sp中
        Editor editor = mPrefs.edit();
        editor.putLong(EXPIRES_IN_LONG, expiresLong);
        editor.putInt(EXPIRES_IN, expiresIn);
        editor.commit();
    }
    /**
     * 擷取scope
     */
    public String getScope() {
        return mPrefs.getString(SCOPE, null);
    }
    /**
     * 設定scope
     */
    public void setScope(final String scope) {
        Editor editor = mPrefs.edit();
        editor.putString(SCOPE, scope);
        editor.commit();
    }

    public String getAuthorize_client_secret() {
        return demoProperties.getProperty("authorize_client_secret", null);
    }
    /**
     * 獲得認證位址
     */
    public String getAuthorize_url() {
        return demoProperties.getProperty("authorize_url");
    }
    public String getAuthorize_response_type() {
        return demoProperties.getProperty("authorize_response_type");
    }
    public String getAuthorize_grant_type() {
        return demoProperties.getProperty("authorize_grant_type");
    }
    public String getAuthorize_client_id() {
        return demoProperties.getProperty("authorize_client_id");
    }
    public String getAuthorize_scope() {
        return demoProperties.getProperty("authorize_scope");
    }
    /**
     * 獲得重定向位址
     */
    public String getAuthorize_redirect_uri() {
        return demoProperties.getProperty("authorize_redirect_uri");
    }
    public String getToken_url() {
        return demoProperties.getProperty("token_url");
    }
    public String getToken_grant_type() {
        return demoProperties.getProperty("token_grant_type");
    }
    public String getWebservice_url() {
        return demoProperties.getProperty("webservice_url");
    }
}
           
  • 整個本地資料存儲服務類AuthenticationDbService都很簡單. 我們接下來看看能夠被浏覽器開啟的SchemeCaptureActivity類. 整個類代碼很長, 需要一點點耐心.

分析SchemeCaptureActivity類

解讀某OAuth 2.0的開源示例android-oauth-app

中間留白的一塊為EditText, 用來顯示一些錯誤資訊和獲得的資料.

/**
 * The Scheme Capture Activity will catch the configured scheme of the
 * redirect_url. The logic of retrieving the refresh/access token will also be
 * done in the class. With a valid access token the configured webservices will
 * be requested to get the data.
 * <p/>
 * SchemeCaptureActivity将捕獲redirect_url配置的scheme. 該類會擷取refresh_token和access_token.
 * 如果webservice請求時攜帶一個有效的access_token,将能伺服器獲得資料
 *
 * @author jknoops @ iprofs.nl
 */
public class SchemeCaptureActivity extends Activity {
    Map<String, String> fragments = new HashMap<String, String>(); //使用map儲存查詢參數
    private EditText et;
    //響應傳回類型是否是code
    private boolean isResponseTypeIsCode;
    private AuthenticationDbService service = AuthenticationDbService
            .getInstance(this);
    //獲得資料響應類型碼的任務
    private RetrieveDataResponseTypeCodeTask retrieveDataResponseTypeCodeTask;
    //獲得refresh_token和access_token的任務
    private RetrieveRefreshAndAccessTokenTask retrieveRefreshAndAccessTokenTask;
    //獲得access_token的任務
    private RetrieveAccessTokenTask retrieveAccessTokenTask;
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_schemecapture);
        //響應類型是code嗎
        isResponseTypeIsCode = AuthenticationDbService.RESPONSE_TYPE_CODE
                .equals(service.getAuthorize_response_type());
        Button refreshButton = (Button) findViewById(R.id.button_refresh);
        refreshButton.setOnClickListener(new View.OnClickListener() {
            public void onClick(View v) {
                //重新整理資料
                refreshData();
            }
        });
        //獲得傳過來的url
        Uri data = getIntent().getData();
        Log.v("demo.SCActivity", "Uri = " + data.toString());
        //響應類型是否為code
        if (isResponseTypeIsCode) {
            //如果傳回的是code,則攜帶code獲得請求參數
            retrieveQueryParamatersWithResponseTypeCode(data);
        } else {
            //如果傳回的是token,則攜帶token獲得請求參數
            retrieveQueryParametersWithResponseTypeToken(data);
        }
        et = (EditText) findViewById(R.id.editText_output);
        et.setTextSize();
        refreshData(); //重新整理資料
    }
    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.activity_start, menu);
        return true;
    }
    /**
     * Retrieve the parameters from the response data. This method should only
     * be used when the response type is "token". When the response type is
     * "token" the response is inside the fragment. The data from the fragment
     * will be stored in the local fragments Dictionary.
     * <p/>
     * 從響應資料中獲得參數. 該方法僅僅在響應類型是token的情況下使用.
     *
     * @param data - the data to split into key/values
     */
    private void retrieveQueryParametersWithResponseTypeToken(Uri data) {
        //從Uri中獲得fragment
        String fragment = data.getFragment();
        Log.v("demo.SCActivity", "Fragement (Token) = " + fragment);
        //按照&切割fragment
        String[] pairs = fragment.split("&");
        Log.v("demo.SCActivity", "Pairs (Token) = " + pairs.toString());
        int i = ;
        String key = "";
        String value = "";
        StringBuilder sb = new StringBuilder();
        //将fragment儲存為key-value形式
        while (pairs.length > i) {
            int j = ;
            String[] part = pairs[i].split("=");
            while (part.length > j) {
                String p = part[j];
                if (j == ) {
                    key = p;
                    sb.append(key + " = ");
                } else if (j == ) {
                    value = p;
                    fragments.put(key, value);
                    sb.append(value + "\n");
                }
                Log.v("demo.SCActivity", "[" + i + "," + j + "] = " + p);
                j++;
            }
            i++;
        }
        //從fragment中存儲token
        storeTokenFromFragments();
    }
    /**
     * Retrieve the parameters from the response data. This method should only
     * be used when the response type is "code". When the response type is
     * "code" the response is in the queryparameters. The data from the
     * queryparameters will be stored in the local fragments Dictionary.
     * 從響應資料中獲得參數. 該方法僅僅被使用在響應類型為code時. 查詢參數将會被存儲在本地的fragment目錄中.
     *
     * @param data - the data to split into key/values
     */
    private void retrieveQueryParamatersWithResponseTypeCode(Uri data) {
        //從Uri中獲得查詢參數
        String queryParameters = data.getQuery();
        Log.v("demo.SCActivity", "Queryparameters (Code) = " + queryParameters);
        //切割查詢參數
        String[] pairs = queryParameters.split("&");
        Log.v("demo.SCActivity", "Pairs (Code) = " + pairs.toString());
        int i = ;
        String key = "";
        String value = "";
        StringBuilder sb = new StringBuilder();
        //将查詢參數轉化為key-value形式
        while (pairs.length > i) {
            int j = ;
            String[] part = pairs[i].split("=");
            while (part.length > j) {
                String p = part[j];
                if (j == ) {
                    key = p;
                    sb.append(key + " = ");
                } else if (j == ) {
                    value = p;
                    fragments.put(key, value);
                    sb.append(value + "\n");
                }
                Log.v("demo.SCActivity", "[" + i + "," + j + "] = " + p);
                j++;
            }
            i++;
        }
    }
    /**
     * A task for retrieving the access and (optional) refresh token. The task
     * will be created when needed. The task will be executed in a separate
     * Thread. This method should only be used when the response type is "code".
     * The authorization code is needed to retrieve the requested tokens. This
     * code should be available in the local dictionary fragments.
     * 使用code擷取refresh_token和access_token. 該方法僅僅被用于響應類型為code時.
     * 需要使用refresh_code才能獲得authorization code.
     */
    private void retrieveRefreshAndAccessTokenWithResponseTypeCode() {
        if (retrieveRefreshAndAccessTokenTask == null) {
            retrieveRefreshAndAccessTokenTask = new RetrieveRefreshAndAccessTokenTask();
        }
        Log.d("TASK-1", retrieveRefreshAndAccessTokenTask.toString());
        //擷取之前的任務狀态, 如果為完成,則執行新的異步任務
        if (retrieveRefreshAndAccessTokenTask.getStatus() == Status.FINISHED) {
            retrieveRefreshAndAccessTokenTask = new RetrieveRefreshAndAccessTokenTask();
            Log.d("TASK-1-execute1", "RetrieveRefreshAndAccessTokenTask");
            retrieveRefreshAndAccessTokenTask.execute();
        } else if (retrieveRefreshAndAccessTokenTask.getStatus() == Status.RUNNING) { //如果還有任務正在進行,則什麼都不做,等待上次任務的結束
            // log
            Log.d("TASK-1-wait", "RetrieveRefreshAndAccessTokenTask");
            this.logUI("Please wait...");
        } else { //預設為執行新的異步任務
            Log.d("TASK-1-execute2", "RetrieveRefreshAndAccessTokenTask");
            retrieveRefreshAndAccessTokenTask.execute();
        }
    }
    /**
     * A task for retrieving the access token with a current refresh token. The
     * task will be created when needed. The task will be executed in a separate
     * Thread. This method should only be used when the response type is "code".
     * The refresh token should be available in the local DBService
     * 該方法邏輯和上面的一樣. 使用code獲得access_token
     */
    private void retrieveAccessTokenWithResponseTypeCode() {
        Log.v("demo.OpenConext", "retrieveAccessTokenWithResponseTypeCode");
        if (retrieveAccessTokenTask == null) {
            retrieveAccessTokenTask = new RetrieveAccessTokenTask();
        }
        Log.d("TASK-2", retrieveAccessTokenTask.toString());
        if (retrieveAccessTokenTask.getStatus() == Status.FINISHED) {
            retrieveAccessTokenTask = new RetrieveAccessTokenTask();
            Log.d("TASK-2-execute1", "RetrieveAccessTokenTask");
            retrieveAccessTokenTask.execute();
        } else if (retrieveAccessTokenTask.getStatus() == Status.RUNNING) {
            // log
            Log.d("TASK-2-wait", "RetrieveAccessTokenTask");
            this.logUI("Please wait...");
        } else {
            Log.d("TASK-2-execute2", "RetrieveAccessTokenTask");
            retrieveAccessTokenTask.execute();
        }
    }
    /**
     * A task for retrieving the data with a current access token. The task will
     * be created when needed. The task will be executed in a separate Thread.
     * This method should only be used when the response type is "code". The
     * access token should be available in the local DBService
     * 該方法邏輯和上面的一樣. 使用code,access_token擷取資料
     */
    private void retrieveDataWithAccessTokenWithResponseTypeCode() {
        if (retrieveDataResponseTypeCodeTask == null) {
            retrieveDataResponseTypeCodeTask = new RetrieveDataResponseTypeCodeTask();
        }
        Log.d("TASK-3", retrieveDataResponseTypeCodeTask.toString());
        if (retrieveDataResponseTypeCodeTask.getStatus() == Status.FINISHED) {
            retrieveDataResponseTypeCodeTask = new RetrieveDataResponseTypeCodeTask();
            Log.d("TASK-3-execute1", "RetrieveDataResponseTypeCodeTask");
            retrieveDataResponseTypeCodeTask.execute();
        } else if (retrieveDataResponseTypeCodeTask.getStatus() == Status.RUNNING) {
            // log
            Log.d("TASK-3-wait", "RetrieveDataResponseTypeCodeTask");
            this.logUI("Please wait...");
        } else {
            Log.d("TASK-3-execute2", "RetrieveDataResponseTypeCodeTask");
            retrieveDataResponseTypeCodeTask.execute();
        }
    }
    //界面顯示日志狀态
    private void logUI(String text) {
        Log.v("text-added", text);
        if (et.getText() == null) {
            et.setText("" + text);
        } else {
            et.setText("" + et.getText() + "\n" + text);
        }
    }
    /**
     * A task for retrieving the data with a current access token. The task will
     * be created when needed. The task will be executed in a separate Thread.
     * This method should only be used when the response type is "token". The
     * access token should be available in the local DBService
     * 使用token, access_token獲得資料.
     * 該方法僅僅被使用在響應類型為token時.
     */
    private void retrieveDataWithAccessTokenWithResponseTypeToken() {
        //從記憶體中獲得access_token的值
        String access_token = fragments.get("access_token");
        Log.v("demo.OpenConext", "access_token=" + access_token);
        //獲得webService位址
        String url = service.getWebservice_url();
        Log.v("demo.OpenConext", "url=" + url);
        BufferedReader in = null;
        try {
            URL jsonURL = new URL(url);
            Log.v("demo.OpenConext", jsonURL.toString());
            HttpsURLConnection tc = (HttpsURLConnection) jsonURL
                    .openConnection();
            //https連接配接,添加請求屬性Authorization為access_token
            tc.addRequestProperty("Authorization", "Bearer " + access_token);
            Log.v("demo.OpenConext", tc.toString());
            InputStreamReader isr = new InputStreamReader(tc.getInputStream());
            in = new BufferedReader(isr, );
            //拼接響應資料
            StringBuilder sb = new StringBuilder();
            sb.append(et.getText());
            sb.append("\nLenght=");
            sb.append(tc.getContentLength());
            sb.append("\nType=");
            sb.append(tc.getContentType());
            sb.append("\nCode=");
            sb.append(tc.getResponseCode());
            Log.v("demo.OpenConext", "" + tc.getResponseCode());
            sb.append("\nMessage=");
            sb.append(tc.getResponseMessage());
            //周遊所有的響應頭字段
            for (String key : tc.getHeaderFields().keySet()) {
                Log.v("demo.OpenConext", "key=" + key + " and size="
                        + tc.getHeaderField(key).length());
            }
            //讀取輸出
            String output = "";
            if ((output = in.readLine()) != null) {
                sb.append("\n");
                sb.append(output);
                Log.v("demo.OpenConext", "output=" + output);
            } else {
                sb.append("\n");
                sb.append(output);
                Log.v("demo.OpenConext", "output=" + output);
            }
            et.setText(sb.toString());
            tc.disconnect();
        } catch (Exception e) {
            Log.e("demo.OpenConext.error",
                    "retrieveDataWithAccessTokenWithResponseTypeToken", e);
            doAuthentication(); //執行認證
        } finally {
            try {
                in.close();
            } catch (IOException e) {
                Log.e("demo.OpenConext.error",
                        "retrieveDataWithAccessTokenWithResponseTypeToken", e);
                doAuthentication(); //執行認證
            }
        }
    }
    /**
     * The default flow for retrieving data. If needed, retrieve refresh and
     * access token from the authorization code. If needed, retrieve new access
     * token from the refresh token. If needed, retrieve new authorization code.
     * retrieve the data.
     * 獲得資料的預設流程.
     * 如果有必要,可以使用authorization code中獲得refresh_token和access_token.
     * 如果有必要,可以使用refresh_token中獲得新的access_token.
     * 如果有必要,可以獲得新的authorization code
     */
    private void refreshData() {
        //禁用連接配接池重用
        disableConnectionReuseIfNecessary();
        //如果是code類型
        if (isResponseTypeIsCode) {
            Log.v("demo.OpenConext", "refreshdata: responseType = code");
            String accessToken = service.getAccessToken(); //先從本地獲得access_token
            if (accessToken == null || "".equalsIgnoreCase(accessToken)) { //如果access_token為空
                if (service.getRefreshToken() == null || "".equalsIgnoreCase(service.getRefreshToken())) { //如果refresh_token也為空
                    //使用code獲得refresh_token,access_token
                    retrieveRefreshAndAccessTokenWithResponseTypeCode();
                } else { //如果refresh_token不為空,則直接使用code獲得access_token
                    retrieveAccessTokenWithResponseTypeCode();
                }
            } else { //如果不為空,則直接使用code和access_token獲得資料
                retrieveDataWithAccessTokenWithResponseTypeCode();
            }
        } else {
            //使用token和access_token獲得資料
            retrieveDataWithAccessTokenWithResponseTypeToken();
        }
    }
    /**
     * For use with android 2.2 (FROYO) the property http.keepAlive needs to be
     * set on false. After this the connection pooling won't be used.
     * <p/>
     * 使用android 2.2(FROYO)系統, http頭keepAlive需要設定為false. 設定之後http連接配接池将不會被使用.
     */
    private static void disableConnectionReuseIfNecessary() {
        // HTTP connection reuse which was buggy pre-froyo
        // 安卓2.2之前http連接配接重試的的一個bug
        if (Build.VERSION.SDK_INT == ) {
            System.setProperty("http.keepAlive", "false");
        }
    }
    /**
     * The properties from the tokenString will be stored in the local
     * DBService. The access token and the token type are required.
     */
    private void storeTokens(String tokenString) throws JSONException {
        JSONObject jo = new JSONObject(tokenString);
        if ((!jo.has("access_token")) && (!jo.has("token_type"))) {
            // Nothing to store
            return;
        }
        service.setAccessToken(jo.getString("access_token"));
        service.setTokenType(jo.getString("token_type"));
        if (jo.has("refresh_token")) {
            service.setRefreshToken(jo.getString("refresh_token"));
        }
        if (jo.has("expires_in")) {
            service.setExpiresIn(jo.getInt("expires_in"));
        }
        if (jo.has("scope")) {
            service.setScope(jo.getString("scope"));
        }
    }
    /**
     * The properties from the local fragments will be stored in the DBService.
     * 将access_token和token_type儲存到本地sp檔案
     */
    private void storeTokenFromFragments() {
        //儲存access_token到sp中
        if (fragments.containsKey("access_token")) {
            service.setAccessToken(fragments.get("access_token"));
        } else {
            service.setAccessToken("");
        }
        //儲存token_type到sp中
        if (fragments.containsKey("token_type")) {
            service.setTokenType(fragments.get("toke_type"));
        } else {
            service.setTokenType("");
        }
    }
    /**
     * When the user is required to authenticate again, the refresh and access
     * tokens will be removed from the DBService. The right URL will be made
     * from the authentication properties. A activity will be activated to
     * execute the created URL. Because the of the configured scheme
     * configuration, this application will capture the response from the OAuth
     * server.
     * 當使用者再次請求認證,本地存儲的refresh_token和access_token将會被删除.
     * 将從認證properties中獲得正确的URL. 因為配置的scheme資訊,一個activity将被激活用來執行建立的url. 該應用将捕捉到OAuth認證伺服器的響應.
     */
    public void doAuthentication() {
        //請求access_token和refresh_token
        service.setAccessToken("");
        service.setRefreshToken("");
        //拼接認證的url
        StringBuilder sb = new StringBuilder();
        // basic authorize
        sb.append(service.getAuthorize_url()); //獲得認證位址
        // response type
        sb.append("?");
        sb.append("response_type="); //請求類型
        sb.append(service.getAuthorize_response_type());
        // client_id
        sb.append("&");
        sb.append("client_id="); //用戶端id
        sb.append(service.getAuthorize_client_id());
        // scope
        sb.append("&");
        sb.append("scope="); //範圍
        sb.append(service.getAuthorize_scope());
        // redirect
        sb.append("&");
        sb.append("redirect_uri="); //重定向位址
        sb.append(service.getAuthorize_redirect_uri());
        String url = sb.toString();
        Log.v("demo.OpenConext", "Starting (Scheme Class) with url = " + url);
        Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
        startActivity(i);
    }
    /**
     * A AsyncTask for retrieving the data from the configured webservice with a
     * access token. This task should only be used when the response type is
     * "code". If needed the access token will be retrieved again. If the
     * postExecute the data retrieved from the webservice will be put in a
     * component on the screen.
     * 使用access_token獲得資料.
     * 如果有必要access_token将會被重新獲得.如果能夠擷取資料,将顯示在螢幕中
     *
     * @author jknoops @ iprofs.nl
     */
    private class RetrieveDataResponseTypeCodeTask extends
            AsyncTask<String, Void, String> {
        @Override
        protected String doInBackground(String... params) {
            String result = "";
            HttpURLConnection conn = null;
            try {
                URL webserviceUrl = new URL(service.getWebservice_url());
                conn = (HttpURLConnection) webserviceUrl.openConnection();
                if (service.getTokenType().equalsIgnoreCase("bearer")) {
                    conn.setRequestProperty("Authorization", "Bearer "
                            + service.getAccessToken());
                }
                Log.d("demo.OpenConext", conn.getRequestProperties().toString());
                Log.v("demo.OpenConext", conn.toString());
                InputStreamReader isr = new InputStreamReader(
                        conn.getInputStream());
                BufferedReader in = new BufferedReader(isr, );
                String response = "";
                StringBuilder sb_output = new StringBuilder();
                while ((response = in.readLine()) != null) {
                    Log.v("demo.OpenConext", "response=" + response);
                    sb_output.append(response);
                }
                sb_output.append("\n");
                result = sb_output.toString();
            } catch (MalformedURLException e) {
                Log.e("demo.OpenConext.error",
                        "retrieveDataWithAccessTokenWithResponseTypeCode", e);
            } catch (IOException e) {
                try {
                    Log.d("demo.OpenConext.error", "" + conn.getResponseCode()
                            + " " + conn.getResponseMessage());
                    int responseCode = conn.getResponseCode();
                    if (responseCode == ) {
                        // token invalid
                        // token無效
                        StringBuilder sb_output = new StringBuilder();
                        sb_output.append("\n");
                        sb_output.append("Oops the token is invalid, let me try again!\n");
                        result = sb_output.toString();
                        //擷取access_token
                        retrieveAccessTokenWithResponseTypeCode();
                    } else {
                        // something else
                        // 如果是其他錯誤
                        StringBuilder sb_output = new StringBuilder();
                        sb_output.append("\n");
                        sb_output.append("Oops something happend!\n");
                        sb_output.append("HTTP response code = " + responseCode
                                + "\n");
                        sb_output.append("HTTP response msg  = "
                                + conn.getResponseMessage() + "\n");
                        result = sb_output.toString();
                    }
                } catch (IOException e1) {
                    Log.e("demo.OpenConext.error",
                            "RetrieveDataResponseTypeCodeTask", e);
                }
            }
            return result;
        }
        @Override
        protected void onPostExecute(String result) {
            Log.d("DEBUG-RetrieveDataResponseTypeCodeTask", "onPostExecute = "
                    + result);
            logUI(result);
        }
    }
    /**
     * A asynctask for retrieving the access and (optional) refresh token from
     * the configured OAuth server. This task should only be used when the
     * response type is "code". The authorization code can only be used once.
     * After using the authorization code, the code will be removed from the
     * local DBService. If the postExecute the tokens will be stored and the
     * task for retrieving the data will be executed.
     * <p/>
     * 從OAuth Server獲得refresh_token和access_token的任務.
     * 該任務僅僅被用在響應類型為code時.
     * authorization code隻能被使用一次.
     * 使用了authorization code之後, authorization code将從本地移除.
     * 在postExecute()中将存儲token的值.
     * 并執行獲得資料的方法
     *
     * @author jknoops @ iprofs.nl
     */
    private class RetrieveRefreshAndAccessTokenTask extends
            AsyncTask<String, Void, String> {
        @Override
        protected String doInBackground(String... params) {
            String result = "";
            if (fragments.containsKey("code")) {
                String code = fragments.get("code");
                Log.v("demo.OpenConext", "code=" + code);
                //獲得token_url
                String url = service.getToken_url();
                URL tokenUrl;
                try {
                    tokenUrl = new URL(url);
                    //使用post表單送出
                    HttpsURLConnection conn = (HttpsURLConnection) tokenUrl
                            .openConnection();
                    //将參數進行URL編碼
                    String param = "grant_type="
                            + URLEncoder.encode(
                            service.getAuthorize_grant_type(), "UTF-8")
                            + "&code="
                            + URLEncoder.encode(code, "UTF-8")
                            + "&redirect_uri="
                            + URLEncoder.encode(
                            service.getAuthorize_redirect_uri(),
                            "UTF-8")
                            + "&client_id="
                            + URLEncoder.encode(
                            service.getAuthorize_client_id(), "UTF-8");
                    conn.setDoOutput(true);
                    conn.setRequestMethod("POST");
                    conn.setFixedLengthStreamingMode(param.getBytes().length);
                    conn.setRequestProperty("Content-Type",
                            "application/x-www-form-urlencoded");
                    // send the POST out
                    // 發送post資料
                    PrintWriter out = new PrintWriter(conn.getOutputStream());
                    out.print(param);
                    out.close();
                    // build the string to store the response text from the
                    // server
                    String response = "";
                    Log.d("DEBUG", "" + conn.getResponseCode());
                    Log.d("DEBUG", "" + conn.getResponseMessage());
                    // start listening to the stream
                    Scanner inStream = new Scanner(conn.getInputStream());
                    // process the stream and store it in StringBuilder
                    // 處理流,并将資料存入StringBuilder中
                    while (inStream.hasNextLine()) {
                        response += (inStream.nextLine());
                    }
                    Log.v("demo.OpenConext", response);
                    result = response;
                } catch (MalformedURLException e) {
                    Log.e("demo.OpenConext.error",
                            "retrieveRefreshAndAccessTokenWithResponseTypeCode",
                            e);
                } catch (IOException e) {
                    Log.e("demo.OpenConext.error",
                            "retrieveRefreshAndAccessTokenWithResponseTypeCode",
                            e);
                }
                fragments.remove("code"); //code隻能被使用一次
            }
            return result;
        }
        @Override
        protected void onPostExecute(String result) {
            if (result != null && !"".equals(result)) {
                try {
                    storeTokens(result); //存儲token
                    //使用code,access_token獲得資料
                    retrieveDataWithAccessTokenWithResponseTypeCode();
                } catch (JSONException e) {
                    doAuthentication(); //重新執行認證
                }
            } else {
                doAuthentication(); //重寫執行認證
            }
        }
    }
    /**
     * A AsyncTask for retrieving the access token from the configured OAuth
     * server with a refresh token. This task should only be used when the
     * response type is "code". The refresh token can only be used multiple
     * times. If the postExecute the tokens will be stored and the task for
     * retrieving the data will be executed.
     * 使用refresh_token獲得access_token.
     * 該任務僅僅被使用在響應類型為code時
     * refresh_token可以被使用多次.
     * 執行postExecute()時,将儲存token值,并執行獲得資料的方法.
     *
     * @author jknoops @ iprofs.nl
     */
    private class RetrieveAccessTokenTask extends
            AsyncTask<String, Void, String> {
        @Override
        protected String doInBackground(String... params) {
            String result = "";
            //獲得token_url
            String url = service.getToken_url();
            URL tokenUrl;
            HttpsURLConnection conn = null;
            try {
                tokenUrl = new URL(url);
                conn = (HttpsURLConnection) tokenUrl.openConnection();
                //通過refresh_token獲得access_token,通過post表單送出
                String param = "grant_type="
                        + URLEncoder.encode("refresh_token", "UTF-8")
                        + "&refresh_token="
                        + URLEncoder.encode(service.getRefreshToken(), "UTF-8");
                conn.setDoOutput(true);
                conn.setRequestMethod("POST");
                conn.setFixedLengthStreamingMode(param.getBytes().length);
                conn.setRequestProperty("Content-Type",
                        "application/x-www-form-urlencoded");
                // send the POST out
                PrintWriter out = new PrintWriter(conn.getOutputStream());
                out.print(param);
                out.close();
                // build the string to store the response text from the server
                String response = "";
                // start listening to the stream
                Scanner inStream = new Scanner(conn.getInputStream());
                // process the stream and store it in StringBuilder
                while (inStream.hasNextLine()) {
                    response += (inStream.nextLine());
                }
                Log.v("demo.OpenConext", response);
                result = response;
            } catch (MalformedURLException e) {
                Log.e("demo.OpenConext.error",
                        "retrieveAccessTokenWithResponseTypeCode", e);
            } catch (IOException e) {
                try {
                    Log.d("demo.OpenConext.error", "" + conn.getResponseCode()
                            + " " + conn.getResponseMessage());
                    int responseCode = conn.getResponseCode();
                    if (responseCode == ) {
                        Log.v("demo.SCActivity.error",
                                "Bad request, authenticate again. Refresh token is not valid anymore.");
                        return "";
                    } else {
                        Log.v("demo.SCActivity.error",
                                "Bad request, authenticate again. Refresh token is not valid anymore.");
                        // something else
                        StringBuilder sb_output = new StringBuilder();
                        sb_output.append("Oops something happend!\n");
                        sb_output.append("HTTP response code = " + responseCode
                                + "\n");
                        sb_output.append("HTTP response msg  = "
                                + conn.getResponseMessage() + "\n");
                        Log.v("demo.SCActivity.error", sb_output.toString());
                        return "";
                    }
                } catch (IOException e1) {
                    Log.e("demo.OpenConext.error",
                            "RetrieveDataResponseTypeCodeTask", e);
                }
            }
            return result;
        }
        @Override
        protected void onPostExecute(String result) {
            if (result != null && !"".equals(result)) {
                logUI("Retrieved new Token(s)");
                try {
                    storeTokens(result); //存儲token
                    retrieveDataWithAccessTokenWithResponseTypeCode(); //通過access_token獲得資料
                } catch (JSONException e) {
                    doAuthentication(); //重新執行認證
                }
            } else {
                doAuthentication(); //重新執行認證
            }
        }
    }
}
           
800多行的代碼. 其實很多流程都是類似的. 下面來進行分解.

分析onCreate()方法中重要的代碼

//響應類型是否為code
        if (isResponseTypeIsCode) {
            //如果傳回的是code,則攜帶code獲得請求參數
            retrieveQueryParamatersWithResponseTypeCode(data);
        } else {
            //如果傳回的是token,則攜帶token獲得請求參數
            retrieveQueryParametersWithResponseTypeToken(data);
        }
       //................
       refreshData(); //重新整理資料
           

分析retrieveQueryParamatersWithResponseTypeCode()方法

/**
 * Retrieve the parameters from the response data. This method should only
 * be used when the response type is "code". When the response type is
 * "code" the response is in the queryparameters. The data from the
 * queryparameters will be stored in the local fragments Dictionary.
 * 從響應資料中獲得參數. 該方法僅僅被使用在響應類型為code時. 查詢參數将會被存儲在fragments的map中
 */
private void retrieveQueryParamatersWithResponseTypeCode(Uri data) {
    //從Uri中獲得查詢參數
    String queryParameters = data.getQuery();
    //切割查詢參數
    String[] pairs = queryParameters.split("&");
    int i = ;
    String key = "";
    String value = "";
    //将查詢參數轉化為key-value形式,儲存在fragments的map中
    StringBuilder sb = new StringBuilder();
    while (pairs.length > i) {
        int j = ;
        String[] part = pairs[i].split("=");
        while (part.length > j) {
            String p = part[j];
            if (j == ) {
                key = p;
                sb.append(key + " = ");
            } else if (j == ) {
                value = p;
                fragments.put(key, value);
                sb.append(value + "\n");
            }
            j++;
        }
        i++;
    }
}
           
  • 而retrieveQueryParametersWithResponseTypeToken()和retrieveQueryParamatersWithResponseTypeCode()基本相同, 隻是最後将獲得的token儲存到了本地.

分析retrieveQueryParamatersWithResponseTypeToken()方法

/**
 * Retrieve the parameters from the response data. This method should only
 * be used when the response type is "token". When the response type is
 * "token" the response is inside the fragment. The data from the fragment
 * will be stored in the local fragments Dictionary.
 * <p/>
 * 從響應資料中獲得參數. 該方法僅僅在響應類型是token的情況下使用.
 */
private void retrieveQueryParametersWithResponseTypeToken(Uri data) {
    //從Uri中獲得fragment
    String fragment = data.getFragment();
    //按照&切割fragment
    String[] pairs = fragment.split("&");
    int i = ;
    String key = "";
    String value = "";
    StringBuilder sb = new StringBuilder();
    //将fragment儲存為key-value形式
    while (pairs.length > i) {
        int j = ;
        String[] part = pairs[i].split("=");
        while (part.length > j) {
            String p = part[j];
            if (j == ) {
                key = p;
                sb.append(key + " = ");
            } else if (j == ) {
                value = p;
                fragments.put(key, value);
                sb.append(value + "\n");
            }
            j++;
        }
        i++;
    }
    //将fragments中的token儲存到本地
    storeTokenFromFragments();
}
           
  • 這裡需要說明的是, 這裡的fragment不是我們平常使用的那個fragment, 而是Uri的組成部分之一.
  • Uri的組成部分

    [scheme:][//authority][path][?query][#fragment]

    詳細可以參考這篇文章: Uri詳解之——Uri結構與代碼提取

  • 看看如何将token相關的資訊儲存到本地的storeTokenFromFragments()方法

分析storeTokenFromFragments()方法

/**
 * The properties from the local fragments will be stored in the DBService.
 * 将access_token和token_type儲存到本地sp檔案
 */
private void storeTokenFromFragments() {
    //儲存access_token到sp中
    if (fragments.containsKey("access_token")) {
        service.setAccessToken(fragments.get("access_token"));
    } else {
        service.setAccessToken("");
    }
    //儲存token_type到sp中
    if (fragments.containsKey("token_type")) {
        service.setTokenType(fragments.get("toke_type"));
    } else {
        service.setTokenType("");
    }
}
           
  • 接下來解析refreshData()方法. 這個方法是OAuth認證流程的一個重要的方法.

分析refreshData()方法

/**
 * The default flow for retrieving data. If needed, retrieve refresh and
 * access token from the authorization code. If needed, retrieve new access
 * token from the refresh token. If needed, retrieve new authorization code.
 * retrieve the data.
 * 獲得資料的預設流程.
 * 如果有必要,可以使用authorization code中獲得refresh_token和access_token.
 * 如果有必要,可以使用refresh_token中獲得新的access_token.
 * 如果有必要,可以獲得新的authorization code
 */
private void refreshData() {
    //禁用連接配接池重用
    disableConnectionReuseIfNecessary();
    //如果是code類型
    if (isResponseTypeIsCode) {
        String accessToken = service.getAccessToken(); //先從本地獲得access_token
        if (accessToken == null || "".equalsIgnoreCase(accessToken)) { //如果access_token為空
            if (service.getRefreshToken() == null || "".equalsIgnoreCase(service.getRefreshToken())) { //如果refresh_token也為空
                //使用code獲得refresh_token,access_token
                retrieveRefreshAndAccessTokenWithResponseTypeCode();
            } else { //如果refresh_token不為空,則直接使用code獲得access_token
                retrieveAccessTokenWithResponseTypeCode();
            }
        } else { //如果不為空,則直接使用code和access_token獲得資料
            retrieveDataWithAccessTokenWithResponseTypeCode();
        }
    } else {
        //使用token和access_token獲得資料
        retrieveDataWithAccessTokenWithResponseTypeToken();
    }
}
           
  • 首先還是看看 disableConnectionReuseIfNecessary();//禁用連接配接池重用. 從注釋中可以看出android 2.2之前版本的HTTP連接配接池有一個bug, 這裡就是禁用連接配接池重用.

分析disableConnectionReuseIfNecessary()方法

/**
 * For use with android 2.2 (FROYO) the property http.keepAlive needs to be
 * set on false. After this the connection pooling won't be used.
 * <p/>
 * 使用android 2.2(FROYO)系統, http頭keepAlive需要設定為false. 設定之後http連接配接池将不會被使用.
 */
private static void disableConnectionReuseIfNecessary() {
    // HTTP connection reuse which was buggy pre-froyo
    // 安卓2.2之前http連接配接重試的的一個bug
    if (Build.VERSION.SDK_INT == ) {
        System.setProperty("http.keepAlive", "false");
    }
}
           
  • 其中主要調用的方法有

    retrieveRefreshAndAccessTokenWithResponseTypeCode(),

    retrieveAccessTokenWithResponseTypeCode(),

    retrieveDataWithAccessTokenWithResponseTypeCode(),

    這三個方法的代碼都很類似. 我們隻要看一個就好了.

分析retrieveRefreshAndAccessTokenWithResponseTypeCode()方法

/**
 * A task for retrieving the access and (optional) refresh token. The task
 * will be created when needed. The task will be executed in a separate
 * Thread. This method should only be used when the response type is "code".
 * The authorization code is needed to retrieve the requested tokens. This
 * code should be available in the local dictionary fragments.
 * 使用code擷取refresh_token和access_token. 該方法僅僅被用于響應類型為code時.
 * 需要使用refresh_code才能獲得authorization code.
 */
private void retrieveRefreshAndAccessTokenWithResponseTypeCode() {
    if (retrieveRefreshAndAccessTokenTask == null) {
        retrieveRefreshAndAccessTokenTask = new RetrieveRefreshAndAccessTokenTask();
    }
    //擷取之前的任務狀态, 如果為完成,則執行新的異步任務
    if (retrieveRefreshAndAccessTokenTask.getStatus() == Status.FINISHED) {
        retrieveRefreshAndAccessTokenTask = new RetrieveRefreshAndAccessTokenTask();
        retrieveRefreshAndAccessTokenTask.execute();
    } else if (retrieveRefreshAndAccessTokenTask.getStatus() == Status.RUNNING) { //如果還有任務正在進行,則什麼都不做,等待上次任務的結束
        this.logUI("Please wait...");
    } else { //預設為執行新的異步任務
        retrieveRefreshAndAccessTokenTask.execute();
    }
}
           
  • 從這些方法可以看出這裡使用的是AsyncTask()進行網絡請求的. 那麼重點也就是這些Task了.

1.RetrieveRefreshAndAccessTokenTask類//如果沒有refresh_token和access_token, 則執行RetrieveRefreshAndAccessTokenTask擷取.

2.RetrieveAccessTokenTask類 //如果有refresh_token但是沒有access_token, 則執行RetrieveAccessTokenTask擷取.

3.RetrieveDataResponseTypeCodeTask類 //如果有access_token且響應類型為code, 則執行RetrieveDataResponseTypeCodeTask擷取.

4.剩下的一種情況: 如果響應類型為token,則直接調用retrieveDataWithAccessTokenWithResponseTypeToken()擷取

分析RetrieveRefreshAndAccessTokenTask類

/**
 * A asynctask for retrieving the access and (optional) refresh token from
 * the configured OAuth server. This task should only be used when the
 * response type is "code". The authorization code can only be used once.
 * After using the authorization code, the code will be removed from the
 * local DBService. If the postExecute the tokens will be stored and the
 * task for retrieving the data will be executed.
 * <p/>
 * 從OAuth Server獲得refresh_token和access_token的任務.
 * 該任務僅僅被用在響應類型為code時.
 * authorization code隻能被使用一次.
 * 使用了authorization code之後, authorization code将從本地移除.
 * 在postExecute()中将存儲token的值.
 * 并執行獲得資料的方法
 */
private class RetrieveRefreshAndAccessTokenTask extends
        AsyncTask<String, Void, String> {
    @Override
    protected String doInBackground(String... params) {
        String result = "";
        if (fragments.containsKey("code")) {
            String code = fragments.get("code");
            //獲得token_url
            String url = service.getToken_url();
            URL tokenUrl;
            try {
                tokenUrl = new URL(url);
                //使用post表單送出
                HttpsURLConnection conn = (HttpsURLConnection) tokenUrl
                        .openConnection();
                //将參數進行URL編碼
                String param = "grant_type="
                        + URLEncoder.encode(
                        service.getAuthorize_grant_type(), "UTF-8")
                        + "&code="
                        + URLEncoder.encode(code, "UTF-8")
                        + "&redirect_uri="
                        + URLEncoder.encode(
                        service.getAuthorize_redirect_uri(),
                        "UTF-8")
                        + "&client_id="
                        + URLEncoder.encode(
                        service.getAuthorize_client_id(), "UTF-8");
                conn.setDoOutput(true);
                conn.setRequestMethod("POST");
                conn.setFixedLengthStreamingMode(param.getBytes().length);
                conn.setRequestProperty("Content-Type",
                        "application/x-www-form-urlencoded");
                // send the POST out
                // 發送post資料
                PrintWriter out = new PrintWriter(conn.getOutputStream());
                out.print(param);
                out.close();
                // build the string to store the response text from the
                // server
                String response = "";
                Log.d("DEBUG", "" + conn.getResponseCode());
                Log.d("DEBUG", "" + conn.getResponseMessage());
                // start listening to the stream
                Scanner inStream = new Scanner(conn.getInputStream());
                // process the stream and store it in StringBuilder
                // 處理流,并将資料存入StringBuilder中
                while (inStream.hasNextLine()) {
                    response += (inStream.nextLine());
                }
                result = response;
            } catch (MalformedURLException e) {
                Log.e("demo.OpenConext.error",
                        "retrieveRefreshAndAccessTokenWithResponseTypeCode",
                        e);
            } catch (IOException e) {
                Log.e("demo.OpenConext.error",
                        "retrieveRefreshAndAccessTokenWithResponseTypeCode",
                        e);
            }
            fragments.remove("code"); //code隻能被使用一次
        }
        return result;
    }
    @Override
    protected void onPostExecute(String result) {
        if (result != null && !"".equals(result)) {
            try {
                storeTokens(result); //存儲token
                //使用code,access_token獲得資料
                retrieveDataWithAccessTokenWithResponseTypeCode();
            } catch (JSONException e) {
                doAuthentication(); //重新執行認證
            }
        } else {
            doAuthentication(); //重新執行認證
        }
    }
}
           
  • 使用POST請求攜帶code送出. 請求成功後code失效, 将獲得的tokens資訊儲存到本地. 再通過access_token獲得資料. 如果沒有獲得tokens資訊, 則重寫執行OAuth認證.

    儲存token資訊的方法, 很簡單.

分析storeTokens()方法

/**
 * The properties from the tokenString will be stored in the local
 * DBService. The access token and the token type are required.
 * 将token資訊儲存到本地
 */
private void storeTokens(String tokenString) throws JSONException {
    JSONObject jo = new JSONObject(tokenString);
    if ((!jo.has("access_token")) && (!jo.has("token_type"))) {
        // Nothing to store
        return;
    }
    service.setAccessToken(jo.getString("access_token"));
    service.setTokenType(jo.getString("token_type"));
    if (jo.has("refresh_token")) {
        service.setRefreshToken(jo.getString("refresh_token"));
    }
    if (jo.has("expires_in")) {
        service.setExpiresIn(jo.getInt("expires_in"));
    }
    if (jo.has("scope")) {
        service.setScope(jo.getString("scope"));
    }
}
           
  • 再來看看重新執行認證的方法doAuthentication().

分析doAuthentication()方法

/**
 * When the user is required to authenticate again, the refresh and access
 * tokens will be removed from the DBService. The right URL will be made
 * from the authentication properties. A activity will be activated to
 * execute the created URL. Because the of the configured scheme
 * configuration, this application will capture the response from the OAuth
 * server.
 * 當使用者再次請求認證,本地存儲的refresh_token和access_token将會被删除.
 * 将從認證配置檔案properties中獲得正确的URL. 因為配置的scheme資訊,一個activity将被激活用來執行建立的url. 該應用将捕捉到OAuth認證伺服器的響應.
 */
public void doAuthentication() {
    //請求access_token和refresh_token
    service.setAccessToken("");
    service.setRefreshToken("");
    //拼接認證的url
    StringBuilder sb = new StringBuilder();
    // basic authorize
    sb.append(service.getAuthorize_url()); //獲得認證位址
    // response type
    sb.append("?");
    sb.append("response_type="); //請求類型
    sb.append(service.getAuthorize_response_type());
    // client_id
    sb.append("&");
    sb.append("client_id="); //用戶端id
    sb.append(service.getAuthorize_client_id());
    // scope
    sb.append("&");
    sb.append("scope="); //範圍
    sb.append(service.getAuthorize_scope());
    // redirect
    sb.append("&");
    sb.append("redirect_uri="); //重定向位址
    sb.append(service.getAuthorize_redirect_uri());
    String url = sb.toString();
    Log.v("demo.OpenConext", "Starting (Scheme Class) with url = " + url);
    Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
    startActivity(i);
}
           
  • 這個和StartActivity中的請求驗證的代碼就是一樣的.

分析RetrieveAccessTokenTask類

再來看RetrieveAccessTokenTask, 其實和上面的RetrieveRefreshAndAccessTokenTask流程上沒有什麼差別. 隻是攜帶的post參數不同而已.

/**
 * A AsyncTask for retrieving the access token from the configured OAuth
 * server with a refresh token. This task should only be used when the
 * response type is "code". The refresh token can only be used multiple
 * times. If the postExecute the tokens will be stored and the task for
 * retrieving the data will be executed.
 * 使用refresh_token獲得access_token.
 * 該任務僅僅被使用在響應類型為code時
 * refresh_token可以被使用多次.
 * 執行postExecute()時,将儲存token值,并執行獲得資料的方法.
 */
private class RetrieveAccessTokenTask extends
        AsyncTask<String, Void, String> {
    @Override
    protected String doInBackground(String... params) {
        String result = "";
        //獲得token_url
        String url = service.getToken_url();
        URL tokenUrl;
        HttpsURLConnection conn = null;
        try {
            tokenUrl = new URL(url);
            conn = (HttpsURLConnection) tokenUrl.openConnection();
            //通過refresh_token獲得access_token,通過post表單送出
            String param = "grant_type="
                    + URLEncoder.encode("refresh_token", "UTF-8")
                    + "&refresh_token="
                    + URLEncoder.encode(service.getRefreshToken(), "UTF-8");
            conn.setDoOutput(true);
            conn.setRequestMethod("POST");
            conn.setFixedLengthStreamingMode(param.getBytes().length);
            conn.setRequestProperty("Content-Type",
                    "application/x-www-form-urlencoded");
            // send the POST out
            PrintWriter out = new PrintWriter(conn.getOutputStream());
            out.print(param);
            out.close();
            // build the string to store the response text from the server
            String response = "";
            // start listening to the stream
            Scanner inStream = new Scanner(conn.getInputStream());
            // process the stream and store it in StringBuilder
            while (inStream.hasNextLine()) {
                response += (inStream.nextLine());
            }
            Log.v("demo.OpenConext", response);
            result = response;
        } catch (MalformedURLException e) {
            Log.e("demo.OpenConext.error",
                    "retrieveAccessTokenWithResponseTypeCode", e);
        } catch (IOException e) {
            try {
                Log.d("demo.OpenConext.error", "" + conn.getResponseCode()
                        + " " + conn.getResponseMessage());
                int responseCode = conn.getResponseCode();
                if (responseCode == ) {
                    Log.v("demo.SCActivity.error",
                            "Bad request, authenticate again. Refresh token is not valid anymore.");
                    return "";
                } else {
                    Log.v("demo.SCActivity.error",
                            "Bad request, authenticate again. Refresh token is not valid anymore.");
                    // something else
                    StringBuilder sb_output = new StringBuilder();
                    sb_output.append("Oops something happend!\n");
                    sb_output.append("HTTP response code = " + responseCode
                            + "\n");
                    sb_output.append("HTTP response msg  = "
                            + conn.getResponseMessage() + "\n");
                    Log.v("demo.SCActivity.error", sb_output.toString());
                    return "";
                }
            } catch (IOException e1) {
                Log.e("demo.OpenConext.error",
                        "RetrieveDataResponseTypeCodeTask", e);
            }
        }
        return result;
    }
    @Override
    protected void onPostExecute(String result) {
        if (result != null && !"".equals(result)) {
            logUI("Retrieved new Token(s)");
            try {
                storeTokens(result); //存儲token資訊到本地
                retrieveDataWithAccessTokenWithResponseTypeCode(); //通過access_token獲得資料
            } catch (JSONException e) {
                doAuthentication(); //重新執行認證
            }
        } else {
            doAuthentication(); //重新執行認證
        }
    }
}
           
  • 再來看看RetrieveDataResponseTypeCodeTask.

分析RetrieveDataResponseTypeCodeTask類

/**
 * A AsyncTask for retrieving the data from the configured webservice with a
 * access token. This task should only be used when the response type is
 * "code". If needed the access token will be retrieved again. If the
 * postExecute the data retrieved from the webservice will be put in a
 * component on the screen.
 * 使用access_token獲得資料.
 * 如果有必要access_token将會被重新獲得.如果能夠擷取資料,将顯示在螢幕中
 */
private class RetrieveDataResponseTypeCodeTask extends
        AsyncTask<String, Void, String> {
    @Override
    protected String doInBackground(String... params) {
        String result = "";
        HttpURLConnection conn = null;
        try {
            URL webserviceUrl = new URL(service.getWebservice_url());
            conn = (HttpURLConnection) webserviceUrl.openConnection();
        //攜帶access_token
            if (service.getTokenType().equalsIgnoreCase("bearer")) {
                conn.setRequestProperty("Authorization", "Bearer "
                        + service.getAccessToken());
            }
            InputStreamReader isr = new InputStreamReader(conn.getInputStream());
            BufferedReader in = new BufferedReader(isr, );
            String response = "";
            StringBuilder sb_output = new StringBuilder();
            while ((response = in.readLine()) != null) {
                sb_output.append(response);
            }
            sb_output.append("\n");
            result = sb_output.toString();
        } catch (MalformedURLException e) {
            Log.e("demo.OpenConext.error",
                    "retrieveDataWithAccessTokenWithResponseTypeCode", e);
        } catch (IOException e) {
            try {
                Log.d("demo.OpenConext.error", "" + conn.getResponseCode()
                        + " " + conn.getResponseMessage());
                int responseCode = conn.getResponseCode();
                if (responseCode == ) {
                    // token invalid token無效
                    StringBuilder sb_output = new StringBuilder();
                    sb_output.append("\n");
                    sb_output.append("Oops the token is invalid, let me try again!\n");
                    result = sb_output.toString();
                    //使用該access_token不能獲得資料的話,那麼将重新擷取access_token
                    retrieveAccessTokenWithResponseTypeCode();
                } else {
                    // something else
                    // 如果是其他錯誤
                    StringBuilder sb_output = new StringBuilder();
                    sb_output.append("\n");
                    sb_output.append("Oops something happend!\n");
                    sb_output.append("HTTP response code = " + responseCode
                            + "\n");
                    sb_output.append("HTTP response msg  = "
                            + conn.getResponseMessage() + "\n");
                    result = sb_output.toString();
                }
            } catch (IOException e1) {
                Log.e("demo.OpenConext.error",
                        "RetrieveDataResponseTypeCodeTask", e);
            }
        }
        return result;
    }
    @Override
    protected void onPostExecute(String result) {
        Log.d("DEBUG-RetrieveDataResponseTypeCodeTask", "onPostExecute = "
                + result);
        logUI(result);
    }
}
           

最後看看retrieveDataWithAccessTokenWithResponseTypeToken(), 這個方法名長的有點變态了.

分析retrieveDataWithAccessTokenWithResponseTypeToken()方法

/**
 * A task for retrieving the data with a current access token. The task will
 * be created when needed. The task will be executed in a separate Thread.
 * This method should only be used when the response type is "token". The
 * access token should be available in the local DBService
 * 使用token, access_token獲得資料.
 * 該方法僅僅被使用在響應類型為token時.
 */
private void retrieveDataWithAccessTokenWithResponseTypeToken() {
    //從fragments的map中獲得access_token的值
    String access_token = fragments.get("access_token");
    //獲得webService位址
    String url = service.getWebservice_url();
    BufferedReader in = null;
    try {
        URL jsonURL = new URL(url);

        HttpsURLConnection tc = (HttpsURLConnection) jsonURL
                .openConnection();
        //https連接配接,添加請求屬性Authorization為access_token
        tc.addRequestProperty("Authorization", "Bearer " + access_token);
        Log.v("demo.OpenConext", tc.toString());
        InputStreamReader isr = new InputStreamReader(tc.getInputStream());
        in = new BufferedReader(isr, );
        //拼接響應資料
        StringBuilder sb = new StringBuilder();
        sb.append(et.getText());
        sb.append("\nLenght=");
        sb.append(tc.getContentLength());
        sb.append("\nType=");
        sb.append(tc.getContentType());
        sb.append("\nCode=");
        sb.append(tc.getResponseCode());
        Log.v("demo.OpenConext", "" + tc.getResponseCode());
        sb.append("\nMessage=");
        sb.append(tc.getResponseMessage());
        //周遊所有的響應頭字段
        for (String key : tc.getHeaderFields().keySet()) {
            Log.v("demo.OpenConext", "key=" + key + " and size="
                    + tc.getHeaderField(key).length());
        }
        //讀取輸出
        String output = "";
        if ((output = in.readLine()) != null) {
            sb.append("\n");
            sb.append(output);
            Log.v("demo.OpenConext", "output=" + output);
        } else {
            sb.append("\n");
            sb.append(output);
            Log.v("demo.OpenConext", "output=" + output);
        }
        et.setText(sb.toString());
        tc.disconnect();
    } catch (Exception e) {
        Log.e("demo.OpenConext.error",
                "retrieveDataWithAccessTokenWithResponseTypeToken", e);
        doAuthentication(); //執行認證
    } finally {
        try {
            in.close();
        } catch (IOException e) {
            Log.e("demo.OpenConext.error",
                    "retrieveDataWithAccessTokenWithResponseTypeToken", e);
            doAuthentication(); //執行認證
        }
    }
}
           
  • 基本上整個流程都已經分析完畢了. 雖然代碼有點多, 但是不難, 不是嗎?