okhttp实现token验证

okhttp实现token验证

前言

公司目前的项目使用了token来验证用户。登陆之后会返回最新的access token,后续在每次请求API时,服务端会返回最新的access token,客户端进行保存。若一段时间内(假定是7天)没有进行操作,则需要重新登陆。

验证流程

token验证流程

在登陆成功之后,后续的token验证流流程如下:

  1. 服务端返回access token,客户端保存
  2. 客户端发起HTTP请求,携带token,添加header Authorization: Bearer {access token}
  3. 服务端验证access token,若过期,则返回状态码401,若未过期,会添加header NewToken: {new access token}
  4. 若返回401,客户端需显示登陆页面,提示用户登陆,若验证通过,正常处理请求,并保存最新的access token,在后续请求中使用。

客户端的处理

思考

需要考虑的点:

  1. 因为我们的APP是必须登陆之后才能使用,后续的接口必须依赖access token,故可以认为(除登录页和闪屏页部分页面的)所有界面都需要验证access token是否有效。
  2. 若是在后续所有页面都进行access token验证的判断,侵入性较强,故access token验证肯定是需要进行同意处理的。
  3. okhttp中,处理access token验证有两种方式:最常用的Interceptor和较少使用的Authenticator。另外,我们也可以统一HTTP请求和响应,不过这和Interceptor的功能重合,所以只需要考虑InterceptorAuthenticator
  4. 实际使用中,会发现Authenticator的调用是在HTTP响应之后(RetryAndFollowUpInterceptor的功能),若是使用Authenticator,则是在一次HTTP相应之后,添加access token再进行一次请求,不可取,所以最后的结论是在Interceptor中添加access token
  5. okhttp中,有两种添加Interceptor的方式,一种是在添加在RetryAndFollowUpInterceptor之前,一种是在CallServerInterceptor之前(即networkInterceptors),后一种是一定需要网络请求时才会调用,前者则是可能在缓存中就得到了值,对于一些静态资源,是不需要access token验证的,所以自然添加到networkInterceptors中。

Interceptor

实现的代码:

object TokenInterceptor : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val oldRequest = chain.request()

        val newRequestBuilder = oldRequest.newBuilder()
            .header("User-Agent", "android/" + AppUtils.getAppVersionName())

        // 判断是否请求的我们公司的服务端
        if (isNeedToken(oldRequest)) {
            // 若存在token,添加token
            DataService.accessToken?.let {
                newRequestBuilder.header("Authorization", "Bearer $it")
            }

            val response = chain.proceed(newRequestBuilder.build())

            // 若是401,表示验证失败
            if (HttpURLConnection.HTTP_UNAUTHORIZED == response.code) {
                // 启动登陆界面
                ActivityUtils.startActivity(LoginActivity.getIntent(ActivityUtils.getTopActivity()))
            }

            // 若是返回新的token,则保存
            response.header("NewToken")?.let {
                DataService.accessToken = it
            }

            return response
        }

        return chain.proceed(newRequestBuilder.build())
    }

    private fun isNeedToken(request: Request): Boolean {
        // 匹配...
    }
}

若同时发起多个HTTP请求,上面的实现则会出现问题,并且这样的情况不可避免。通常在主页会有比较复杂的逻辑,会同时发起多个HTTP请求。下面是可能存在的问题:

  1. 若同时发起多个HTTP请求,则会出现多个请求同时返回401的问题,同时启动多个登陆页面,当然这样的错误也可以避免,如设置登陆页面ActivitylaunchModesingleTask
  2. 多个HTTP响应也会造成页面上可能会同时弹出多个Toast的情况。
  3. 多个HTTP请求携带同样的access token,返回新的access token则不同,可能会因为网络延时,造成服务端最新的access token反而会被前一个access token覆盖。

综上,我给出的解决方案是进行同步:

object TokenInterceptor : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val oldRequest = chain.request()

        val newRequestBuilder = oldRequest.newBuilder()
            .header("User-Agent", "android/" + AppUtils.getAppVersionName())

        // 判断是否请求的我们公司的服务端
        if (isNeedToken(oldRequest)) {
            synchronized(this) {
                // 若存在token,添加token
                DataService.accessToken?.let {
                    newRequestBuilder.header("Authorization", "Bearer $it")
                }

                val response = chain.proceed(newRequestBuilder.build())

                // 若是401,表示验证失败
                if (HttpURLConnection.HTTP_UNAUTHORIZED == response.code) {
                    // 启动登陆界面
                    ActivityUtils.startActivity(LoginActivity.getIntent(ActivityUtils.getTopActivity()))
                }

                // 若是返回新的token,则保存
                response.header("NewToken")?.let {
                    DataService.accessToken = it
                }

                return response
            }
        }

        return chain.proceed(newRequestBuilder.build())
    }

    private fun isNeedToken(request: Request): Boolean {
        // 匹配...
    }
}

除了上面的实现,还需要设置登陆页面ActivitylaunchModesingleTask,减少首页Activity请求失败后弹出Toast,另外可以在闪屏页面进行验证,不过设置超时时间尽量短。

不过使用同步就会使每个需要token验证的请求排队,对于APP并发量较少的情况来说没有问题,若是一些不需要token验证,并发量高的HTTP请求,如加载瀑布流图片,则可以进一步判断,不进入同步代码块,这样也能提高效率。

总结

实现看起来非常简单,不过这是要在理解access token验证的基础上,并且需要经历很多测试(我让服务端的大神设置access token过期时间为5分钟,这样可以频繁触发access token)。建议阅读后面的参考文章。

参考阅读

理解OAuth 2.0

OAuth 2.0 筆記 (6) Bearer Token 的使用方法

OAuth 2.0: Bearer Token Usage

RxJava2 + Retrofit2 完全指南 之 Authenticator处理与Token静默刷新


   转载规则


《okhttp实现token验证》 Mycroft Wong 采用 知识共享署名 4.0 国际许可协议 进行许可。
 上一篇
Permission 权限 Permission 权限
Permission 权限Android引入权限的目的是保护用户隐私。APP一定要在访问用户敏感数据(如短信服务)和系统功能(如相机、网络)时申请权限。对于不同的功能,系统可能会自动通过权限申请,也可能提示用户批准权限。 安卓系统安全架构的
下一篇 
Okio官方文档翻译 Okio官方文档翻译
Okio官方文档翻译Okio是一个辅助java.io和java.nio变得更易于访问、存储、操作数据的库。它一开始是作为Okhttp的组件存在。Okhttp是在Android中非常好用的HTTP客户端。Okio经过充分测试,并且准备好处理新
  目录