programing

모든 호출을 수정하지 않고 Retrofit을 사용하여 OAuth 토큰 새로 고침

lastmoon 2023. 7. 31. 21:49
반응형

모든 호출을 수정하지 않고 Retrofit을 사용하여 OAuth 토큰 새로 고침

우리는 Android 앱에서 Retrofit을 사용하여 OAuth2 보안 서버와 통신하고 있습니다.모든 것이 잘 작동합니다. 요청을 사용합니다.각 호출에 액세스 토큰을 포함하는 인터셉터.그러나 액세스 토큰이 만료되어 토큰을 새로 고쳐야 하는 경우가 있습니다.토큰이 만료되면 다음 호출이 승인되지 않은 HTTP 코드와 함께 반환되므로 쉽게 모니터링할 수 있습니다.다음과 같은 방법으로 각 Retrofit 호출을 수정할 수 있습니다.실패 콜백에서 오류 코드를 확인하고 인증되지 않은 경우 OAuth 토큰을 새로 고친 다음 Retrofit 호출을 반복합니다.그러나 이를 위해서는 모든 통화를 수정해야 하며, 이는 쉽게 유지 관리할 수 있는 좋은 해결책이 아닙니다.모든 Retrofit 호출을 수정하지 않고 이 작업을 수행할 수 있는 방법이 있습니까?

▁do를 하지 마십시오.Interceptors인증을 처리합니다.

현재 인증을 처리하는 가장 좋은 방법은 이러한 목적을 위해 특별히 설계된 새로운 API를 사용하는 것입니다.

OkHttp는 자동으로 다음을 묻습니다.Authenticator이 응이다경인증정보인 자격 401 Not Authorised 마지막으로 실패한 요청을 다시 시도하는 중입니다.

public class TokenAuthenticator implements Authenticator {
    @Override
    public Request authenticate(Proxy proxy, Response response) throws IOException {
        // Refresh your access_token using a synchronous api request
        newAccessToken = service.refreshToken();

        // Add new header to rejected request and retry it
        return response.request().newBuilder()
                .header(AUTHORIZATION, newAccessToken)
                .build();
    }

    @Override
    public Request authenticateProxy(Proxy proxy, Response response) throws IOException {
        // Null indicates no attempt to authenticate.
        return null;
    }

을 .Authenticator완전히OkHttpClient당신이 하는 것과 같은 방식으로Interceptors

OkHttpClient okHttpClient = new OkHttpClient();
okHttpClient.setAuthenticator(authAuthenticator);

을 때 이 합니다.Retrofit RestAdapter

RestAdapter restAdapter = new RestAdapter.Builder()
                .setEndpoint(ENDPOINT)
                .setClient(new OkClient(okHttpClient))
                .build();
return restAdapter.create(API.class);

Retrofit을 사용하는 경우 >=1.9.0그러면 당신은 OkHttp의 새로운 가로채기를 사용할 수 있습니다.OkHttp 2.2.0응용 프로그램 가로채기를 사용하여 다음 작업을 수행할 수 있습니다.retry and make multiple calls.

인터셉트카는 다음 유사 코드처럼 보일 수 있습니다.

public class CustomInterceptor implements Interceptor {

    @Override
    public Response intercept(Chain chain) throws IOException {
        Request request = chain.request();

        // try the request
        Response response = chain.proceed(request);

        if (response shows expired token) {
            // close previous response
            response.close()

            // get a new token (I use a synchronous Retrofit call)

            // create a new request and modify it accordingly using the new token
            Request newRequest = request.newBuilder()...build();

            // retry the request
            return chain.proceed(newRequest);
        }

        // otherwise just pass the original response on
        return response;
    }

}

이 당신의 음을정후를 에.Interceptor를 작성합니다.OkHttpClient응용 프로그램 가로채기로 인터셉트를 추가합니다.

    OkHttpClient okHttpClient = new OkHttpClient();
    okHttpClient.interceptors().add(new CustomInterceptor());

그리고 마지막으로, 이것을 사용합니다.OkHttpClient를 할 때RestAdapter.

    RestService restService = new RestAdapter().Builder
            ...
            .setClient(new OkClient(okHttpClient))
            .create(RestService.class);

경고: AsJesse Wilson(스퀘어에서) 여기 언급했습니다. 이것은 위험한 양의 힘입니다.

그런 말이 나온 김에, 저는 분명히 지금 이 방법이 이와 같은 일을 처리하는 가장 좋은 방법이라고 생각합니다.질문이 있으시면 언제든지 댓글로 물어보세요.

TokenAuthenticator는 서비스 클래스에 종속됩니다.서비스 클래스는 OkHttpClient 인스턴스에 따라 달라집니다.OkHttpClient를 만들려면 TokenAuthenticator가 필요합니다.어떻게 하면 이 주기를 깰 수 있을까요?두 개의 다른 OkHttpClients?서로 다른 연결 풀을 갖게 될 것입니다.

예를 들어, 레트로핏이 있다면,TokenService당신의 내부에 필요한.Authenticator하지만 당신은 단지 하나를 설정하고 싶을 뿐입니다.OkHttpClient를 사용할 수 있습니다.TokenServiceHolder에 대한 의존으로서.TokenAuthenticator애플리케이션(싱글톤) 수준에서 이에 대한 참조를 유지해야 합니다.단검 2를 사용하는 경우에는 쉽고, 그렇지 않은 경우에는 응용프로그램 내에 클래스 필드를 만들기만 하면 됩니다.

TokenAuthenticator.java

public class TokenAuthenticator implements Authenticator {

    private final TokenServiceHolder tokenServiceHolder;

    public TokenAuthenticator(TokenServiceHolder tokenServiceHolder) {
        this.tokenServiceHolder = tokenServiceHolder;
    }

    @Override
    public Request authenticate(Proxy proxy, Response response) throws IOException {

        //is there a TokenService?
        TokenService service = tokenServiceHolder.get();
        if (service == null) {
            //there is no way to answer the challenge
            //so return null according to Retrofit's convention
            return null;
        }

        // Refresh your access_token using a synchronous api request
        newAccessToken = service.refreshToken().execute();

        // Add new header to rejected request and retry it
        return response.request().newBuilder()
                .header(AUTHORIZATION, newAccessToken)
                .build();
    }

    @Override
    public Request authenticateProxy(Proxy proxy, Response response) throws IOException {
        // Null indicates no attempt to authenticate.
        return null;
    }

TokenServiceHolder.java:

public class TokenServiceHolder {

    TokenService tokenService = null;

    @Nullable
    public TokenService get() {
        return tokenService;
    }

    public void set(TokenService tokenService) {
        this.tokenService = tokenService;
    }
}

클라이언트 설정:

//obtain instance of TokenServiceHolder from application or singleton-scoped component, then
TokenAuthenticator authenticator = new TokenAuthenticator(tokenServiceHolder);
OkHttpClient okHttpClient = new OkHttpClient();    
okHttpClient.setAuthenticator(tokenAuthenticator);

Retrofit retrofit = new Retrofit.Builder()
    .baseUrl("https://api.github.com/")
    .client(okHttpClient)
    .build();

TokenService tokenService = retrofit.create(TokenService.class);
tokenServiceHolder.set(tokenService);

단검 2 또는 유사한 종속성 주입 프레임워크를 사용하는 경우 이 질문에 대한 답변에 몇 가지 예가 나와 있습니다.

사용.TokenAuthenticator@blang 답은 올바른 핸들링 방법입니다.refresh_token.

여기 제 도구가 있습니다(나는 코틀린, 단검, RX를 사용하지만 당신의 경우에는 이 아이디어를 사용할 수 있습니다).
TokenAuthenticator

class TokenAuthenticator @Inject constructor(private val noneAuthAPI: PotoNoneAuthApi, private val accessTokenWrapper: AccessTokenWrapper) : Authenticator {

    override fun authenticate(route: Route, response: Response): Request? {
        val newAccessToken = noneAuthAPI.refreshToken(accessTokenWrapper.getAccessToken()!!.refreshToken).blockingGet()
        accessTokenWrapper.saveAccessToken(newAccessToken) // save new access_token for next called
        return response.request().newBuilder()
                .header("Authorization", newAccessToken.token) // just only need to override "Authorization" header, don't need to override all header since this new request is create base on old request
                .build()
    }
}

@Brais Gabin comment와 같은 의존성 주기를 방지하기 위해, 나는 다음과 같은 2개의 인터페이스를 만듭니다.

interface PotoNoneAuthApi { // NONE authentication API
    @POST("/login")
    fun login(@Body request: LoginRequest): Single<AccessToken>

    @POST("refresh_token")
    @FormUrlEncoded
    fun refreshToken(@Field("refresh_token") refreshToken: String): Single<AccessToken>
}

그리고.

interface PotoAuthApi { // Authentication API
    @GET("api/images")
    fun getImage(): Single<GetImageResponse>
}

AccessTokenWrapper 계급의

class AccessTokenWrapper constructor(private val sharedPrefApi: SharedPrefApi) {
    private var accessToken: AccessToken? = null

    // get accessToken from cache or from SharePreference
    fun getAccessToken(): AccessToken? {
        if (accessToken == null) {
            accessToken = sharedPrefApi.getObject(SharedPrefApi.ACCESS_TOKEN, AccessToken::class.java)
        }
        return accessToken
    }

    // save accessToken to SharePreference
    fun saveAccessToken(accessToken: AccessToken) {
        this.accessToken = accessToken
        sharedPrefApi.putObject(SharedPrefApi.ACCESS_TOKEN, accessToken)
    }
}

AccessToken 계급의

data class AccessToken(
        @Expose
        var token: String,

        @Expose
        var refreshToken: String)

내 인터셉트

class AuthInterceptor @Inject constructor(private val accessTokenWrapper: AccessTokenWrapper): Interceptor {

    override fun intercept(chain: Interceptor.Chain): Response {
        val originalRequest = chain.request()
        val authorisedRequestBuilder = originalRequest.newBuilder()
                .addHeader("Authorization", accessTokenWrapper.getAccessToken()!!.token)
                .header("Accept", "application/json")
        return chain.proceed(authorisedRequestBuilder.build())
    }
}

마지막으로 추가Interceptor그리고.Authenticator당신에게OKHttpClient서비스 PotoAuthApi를 생성할 때

데모

https://github.com/PhanVanLinh/AndroidMVPKotlin

메모

Authenticator flow
  • 예제 APIgetImage()401 오류 코드를 반환합니다.
  • authenticate내부의 방법TokenAuthenticator발사될 것
  • 동기화noneAuthAPI.refreshToken(...)불렀다
  • 끝나고noneAuthAPI.refreshToken(...)response -> 새 토큰이 헤더에 추가됩니다.
  • getImage() 헤더를 사용하여 자동으로 호출됩니다(HttpLogging 통화를 기록하지 않음)(intercept안에서.AuthInterceptor 호출 안 함)
  • 한다면getImage()오류 401로 인해 여전히 실패했습니다.authenticate내부의 방법TokenAuthenticator몇 번이고 반복해서 작동하고 호출 방법에 대한 오류를 여러 번 던집니다(java.net.ProtocolException: Too many follow-up requests) 카운트 응답으로 방지할 수 있습니다.예를 들어, 만약 당신이return nullauthenticate3번 재시도 후,getImage()끝날 것이고.return response 401

  • 한다면getImage()응답 성공 => 우리는 정상적으로 결과를 낼 것입니다 (당신이 부르는 것처럼).getImage()오류 없음)

도움이 되길 바랍니다.

브레이스 가빈이 댓글에서 말했듯이 저는 문제가 있었습니다.TokenAuthenticator서비스 클래스에 따라 다릅니다.서비스 클래스는 다음에 따라 달라집니다.OkHttpClient인스턴스 및 생성OkHttpClient나는 그것이 필요합니다.TokenAuthenticator.

그래서 어떻게 이 주기를 깼을까요?

새 항목을 작성했습니다.okHttpClient객체, 새로운Retrofit객체와 그 객체를 사용하여 새로운 토큰을 얻기 위해 호출을 수행했습니다.refreshToken( getUpdate 확인)토큰() 함수

class TokenAuthenticator : Authenticator {

    override fun authenticate(route: Route?, response: Response): Request? {
        return runBlocking {

            // 1. Refresh your access_token using a synchronous api request
           val response = getUpdatedToken(refreshToken)

           //2. In my case here I store the new token and refreshToken into SharedPreferences

           response.request.newBuilder()
                        .header("Authorization", "Bearer   ${tokenResponse.data?.accessToken}")
                        .build()

           // 3. If there's any kind of error I return null
           
        }
    }

    private suspend fun getUpdatedToken( refreshToken: String): TokenResponse {
        val okHttpClient = OkHttpClient().newBuilder()
            .addInterceptor(errorResponseInterceptor)
            .build()

        val retrofit = Retrofit.Builder()
            .baseUrl(BuildConfig.BASE_URL)
            .client(okHttpClient)
            .addConverterFactory(MoshiConverterFactory.create())
            .build()


        val service = retrofit.create(RefreshTokenApi::class.java)
        return service.refreshToken(refreshToken)

    }

}

토큰 API 새로 고침

interface RefreshTokenApi {

    @FormUrlEncoded
    @POST("refreshToken")
    suspend fun refreshToken(
        @Field("refresh_token") refreshToeken: String
    ): TokenResponse
}

이 프로젝트에서 저는 Koin을 사용하고 있으며 다음과 같이 구성했습니다.

object RetrofigConfig {
    fun provideRetrofit(okHttpClient: OkHttpClient): Retrofit {
        return Retrofit.Builder()
            .baseUrl(BuildConfig.BASE_URL)
            .client(okHttpClient)
            .addConverterFactory(MoshiConverterFactory.create())
            .build()
    }

    fun provideOkHttpClient(
        tokenAuthenticator: TokenAuthenticator
    ): OkHttpClient {

        return OkHttpClient().newBuilder()
            .authenticator(tokenAuthenticator)
            .build()
    }

    fun provideServiceApi(retrofit: Retrofit): ServiceApi {
        return retrofit.create(ServiceApi::class.java)
    }
}

여기서 중요한 줄은 OkHttpClient().newBuilder().authenticator(tokenAuthenticator)입니다.

처음 구현하는 것이기 때문에 이것이 최선의 방법인지는 모르겠지만 제 프로젝트에서 작동하는 방식입니다.

오래된 일인 건 알지만 혹시라도 누군가 걸려 넘어질까 봐요.

TokenAuthenticator는 서비스 클래스에 종속됩니다.서비스 클래스는 OkHttpClient 인스턴스에 따라 달라집니다.OkHttpClient를 만들려면 TokenAuthenticator가 필요합니다.어떻게 하면 이 주기를 깰 수 있을까요?두 개의 다른 OkHttpClients?서로 다른 연결 풀을 갖게 될 것입니다.

나는 같은 문제에 직면하고 있었지만, 나는 토큰 인증기 자체에만 다른 것이 필요하다고 생각하지 않기 때문에 OkHttpClient를 하나만 만들고 싶었다, 나는 Dagnet2를 사용하고 있었다, 그래서 나는 TokenAuthenticator에 Lazy injection으로 서비스 클래스를 제공하게 되었다, 여기서 Dagnet2의 Lazy injection에 대해 더 자세히 읽을 수 있다,하지만 기본적으로 Dague에게 토큰 인증자가 필요로 하는 서비스를 즉시 만들지 말라고 말하는 것과 같습니다.

샘플 코드는 다음 SO 스레드를 참조할 수 있습니다.Dague2를 사용하면서 순환 종속성을 해결하는 방법은 무엇입니까?

하나의 인터셉트카(토큰 주입)와 하나의 Authenticator(새로 고침 작업)를 사용하여 작업을 수행하지만 다음 작업은 다음과 같습니다.

저도 이중 통화 문제가 있었습니다. 첫 번째 통화는 항상 401을 반환했습니다. 토큰은 첫 번째 통화(인터셉터)에서 주입되지 않았고 인증자가 호출되었습니다. 두 가지 요청이 있었습니다.

해결책은 요격기의 빌드에 대한 요청을 재확인하는 것이었습니다.

이전:

private Interceptor getInterceptor() {
    return (chain) -> {
        Request request = chain.request();
        //...
        request.newBuilder()
                .header(AUTHORIZATION, token))
                .build();
        return chain.proceed(request);
    };
}

이후:

private Interceptor getInterceptor() {
    return (chain) -> {
        Request request = chain.request();
        //...
        request = request.newBuilder()
                .header(AUTHORIZATION, token))
                .build();
        return chain.proceed(request);
    };
}

단일 블록:

private Interceptor getInterceptor() {
    return (chain) -> {
        Request request = chain.request().newBuilder()
                .header(AUTHORIZATION, token))
                .build();
        return chain.proceed(request);
    };
}

도움이 되길 바랍니다.

편집: 오센티케이터만 사용하고 인터셉트는 사용하지 않고 항상 401을 반환하는 첫 번째 전화를 피할 방법을 찾지 못했습니다.

모든 로더에 대한 기본 클래스를 생성하여 특정 예외를 포착한 다음 필요에 따라 작업할 수 있습니다.동작을 분산하기 위해 모든 로더를 기본 클래스에서 확장합니다.

오랜 연구 끝에 Apache 클라이언트를 사용자 지정하여 Refresh Access를 처리했습니다.액세스 토큰을 매개 변수로 보내는 Retrofit용 토큰입니다.

쿠키 영구 클라이언트를 사용하여 어댑터 시작

restAdapter = new RestAdapter.Builder()
                .setEndpoint(SERVER_END_POINT)
                .setClient(new CookiePersistingClient())
                .setLogLevel(RestAdapter.LogLevel.FULL).build();

쿠키 영구 클라이언트 - 모든 요청에 대해 쿠키를 유지 관리하고 각 요청 응답을 확인합니다. 무단 액세스 ERROR_CODE = 401이면 액세스 토큰을 새로 고치고 요청을 호출합니다. 그렇지 않으면 요청만 처리합니다.

private static class CookiePersistingClient extends ApacheClient {

    private static final int HTTPS_PORT = 443;
    private static final int SOCKET_TIMEOUT = 300000;
    private static final int CONNECTION_TIMEOUT = 300000;

    public CookiePersistingClient() {
        super(createDefaultClient());
    }

    private static HttpClient createDefaultClient() {
        // Registering https clients.
        SSLSocketFactory sf = null;
        try {
            KeyStore trustStore = KeyStore.getInstance(KeyStore
                    .getDefaultType());
            trustStore.load(null, null);

            sf = new MySSLSocketFactory(trustStore);
            sf.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
        } catch (KeyManagementException e) {
            e.printStackTrace();
        } catch (UnrecoverableKeyException e) {
            e.printStackTrace();
        } catch (KeyStoreException e) {
            e.printStackTrace();
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        } catch (CertificateException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        }
        HttpParams params = new BasicHttpParams();
        HttpConnectionParams.setConnectionTimeout(params,
                CONNECTION_TIMEOUT);
        HttpConnectionParams.setSoTimeout(params, SOCKET_TIMEOUT);
        SchemeRegistry registry = new SchemeRegistry();
        registry.register(new Scheme("https", sf, HTTPS_PORT));
        // More customization (https / timeouts etc) can go here...

        ClientConnectionManager cm = new ThreadSafeClientConnManager(
                params, registry);
        DefaultHttpClient client = new DefaultHttpClient(cm, params);

        // Set the default cookie store
        client.setCookieStore(COOKIE_STORE);

        return client;
    }

    @Override
    protected HttpResponse execute(final HttpClient client,
            final HttpUriRequest request) throws IOException {
        // Set the http context's cookie storage
        BasicHttpContext mHttpContext = new BasicHttpContext();
        mHttpContext.setAttribute(ClientContext.COOKIE_STORE, COOKIE_STORE);
        return client.execute(request, mHttpContext);
    }

    @Override
    public Response execute(final Request request) throws IOException {
        Response response = super.execute(request);
        if (response.getStatus() == 401) {

            // Retrofit Callback to handle AccessToken
            Callback<AccessTockenResponse> accessTokenCallback = new Callback<AccessTockenResponse>() {

                @SuppressWarnings("deprecation")
                @Override
                public void success(
                        AccessTockenResponse loginEntityResponse,
                        Response response) {
                    try {
                        String accessToken =  loginEntityResponse
                                .getAccessToken();
                        TypedOutput body = request.getBody();
                        ByteArrayOutputStream byte1 = new ByteArrayOutputStream();
                        body.writeTo(byte1);
                        String s = byte1.toString();
                        FormUrlEncodedTypedOutput output = new FormUrlEncodedTypedOutput();
                        String[] pairs = s.split("&");
                        for (String pair : pairs) {
                            int idx = pair.indexOf("=");
                            if (URLDecoder.decode(pair.substring(0, idx))
                                    .equals("access_token")) {
                                output.addField("access_token",
                                        accessToken);
                            } else {
                                output.addField(URLDecoder.decode(
                                        pair.substring(0, idx), "UTF-8"),
                                        URLDecoder.decode(
                                                pair.substring(idx + 1),
                                                "UTF-8"));
                            }
                        }
                        execute(new Request(request.getMethod(),
                                request.getUrl(), request.getHeaders(),
                                output));
                    } catch (IOException e) {
                        e.printStackTrace();
                    }

                }

                @Override
                public void failure(RetrofitError error) {
                    // Handle Error while refreshing access_token
                }
            };
            // Call Your retrofit method to refresh ACCESS_TOKEN
            refreshAccessToken(GRANT_REFRESH,CLIENT_ID, CLIENT_SECRET_KEY,accessToken, accessTokenCallback);
        }

        return response;
    }
}

여기 제 코드가 저를 위해 작동합니다. 누군가에게 도움이 될 수 있습니다.

   class AuthenticationInterceptorRefreshToken @Inject 
   constructor( var hIltModules: HIltModules,) : Interceptor {

   @Throws(IOException::class)
   override fun intercept(chain: Interceptor.Chain): Response {

  val originalRequest = chain.request()
  val response = chain.proceed(originalRequest)

  if (response.code == 401) {
    synchronized(this) {
        val originalRequest = chain.request()
        val authenticationRequest = originalRequest.newBuilder()
            .addHeader("refreshtoken", " $refreshToken")
            .build()
        val initialResponse = chain.proceed(authenticationRequest)

        when (initialResponse.code) {

            401 -> {
                val responseNewTokenLoginModel = runBlocking {
                    hIltModules.provideAPIService().refreshToken()
                }

                when (responseNewTokenLoginModel.statusCode) {
                    200 -> {
                        refreshToken = responseNewTokenLoginModel.refreshToken
                        access_token = responseNewTokenLoginModel.accessToken

                        val newAuthenticationRequest = originalRequest.newBuilder()
                            .header("refreshtoken",
                                " $refreshToken")
                            .build()
                        return chain.proceed(newAuthenticationRequest)
                    }
                    else -> {
                        return null!!
                    }
                }
            }
            else -> return initialResponse
        }
    }
}; return response

}

토큰을 새로 고칠 때 동시/병렬 호출을 해결하고자 하는 모든 사용자에게.해결 방법은 다음과 같습니다.

class TokenAuthenticator: Authenticator {

    override fun authenticate(route: Route?, response: Response?): Request? {
        response?.let {
            if (response.code() == 401) {
                while (true) {
                    if (!isRefreshing) {
                        val requestToken = response.request().header(AuthorisationInterceptor.AUTHORISATION)
                        val currentToken = OkHttpUtil.headerBuilder(UserService.instance.token)

                        currentToken?.let {
                            if (requestToken != currentToken) {
                                return generateRequest(response, currentToken)
                            }
                        }

                        val token = refreshToken()
                        token?.let {
                            return generateRequest(response, token)
                        }
                    }
                }
            }
        }

        return null
    }

    private fun generateRequest(response: Response, token: String): Request? {
        return response.request().newBuilder()
                .header(AuthorisationInterceptor.USER_AGENT, OkHttpUtil.UA)
                .header(AuthorisationInterceptor.AUTHORISATION, token)
                .build()
    }

    private fun refreshToken(): String? {
        synchronized(TokenAuthenticator::class.java) {
            UserService.instance.token?.let {
                isRefreshing = true

                val call = ApiHelper.refreshToken()
                val token = call.execute().body()
                UserService.instance.setToken(token, false)

                isRefreshing = false

                return OkHttpUtil.headerBuilder(token)
            }
        }

        return null
    }

    companion object {
        var isRefreshing = false
    }
}

언급URL : https://stackoverflow.com/questions/22450036/refreshing-oauth-token-using-retrofit-without-modifying-all-calls

반응형