天天看点

解读某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(); //执行认证
        }
    }
}
           
  • 基本上整个流程都已经分析完毕了. 虽然代码有点多, 但是不难, 不是吗?