우리는 OAuth2 보안 서버와 통신하기 위해 Android 앱에서 Retrofit을 사용하고 있습니다. 모든 것이 잘 작동합니다. RequestInterceptor를 사용하여 각 호출마다 액세스 토큰을 포함시킵니다. 그러나 액세스 토큰이 만료되어 토큰을 새로 고쳐야 할 때가 있습니다. 토큰이 만료되면 다음 호출이 인증되지 않은 HTTP 코드와 함께 반환되므로 쉽게 모니터링 할 수 있습니다. 각 Retrofit 호출을 다음과 같은 방식으로 수정할 수 있습니다. 실패 콜백에서 오류 코드가 Unauthorized와 같으면 OAuth 토큰을 새로 고친 다음 Retrofit 호출을 반복하십시오. 그러나이를 위해서는 모든 통화를 수정해야합니다. 이는 유지 관리가 용이하지 않고 좋은 해결책이 아닙니다. 모든 Retrofit 호출을 수정하지 않고이를 수행 할 수있는 방법이 있습니까?
답변
Interceptors
인증을 처리 하는 데 사용하지 마십시오 .
현재 인증을 처리하는 가장 좋은 방법 Authenticator
은 이 목적을 위해 특별히 설계된 새로운 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);
답변
당신이 사용하는 경우 개조를 > = 1.9.0
당신은의 사용을 만들 수 OkHttp의 새로운 인터셉터 에 도입되었다 OkHttp 2.2.0
. Application Interceptor 를 사용하여 다음을 수행 할 수 있습니다 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) {
// 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);
경고 :Jesse Wilson
(Square에서) 여기 에서 언급 했듯이 이것은 위험한 양의 힘입니다.
그렇게 말하면서, 나는 이것이 이것이 지금 이와 같은 것을 처리하는 가장 좋은 방법이라고 생각합니다. 궁금한 점이 있으면 언제든지 문의하십시오.
답변
TokenAuthenticator는 서비스 클래스에 따라 다릅니다. 서비스 클래스는 OkHttpClient 인스턴스에 따라 다릅니다. OkHttpClient를 만들려면 TokenAuthenticator가 필요합니다. 이주기를 어떻게 끊을 수 있습니까? 두 개의 다른 OkHttpClient? 그들은 다른 연결 풀을 가질 것입니다.
예를 들어, TokenService
내부에 필요한 개조 가 Authenticator
있지만 하나만 설정하려는 OkHttpClient
경우 TokenServiceHolder
에 대한 종속성으로 사용할 수 있습니다 TokenAuthenticator
. 응용 프로그램 (단일) 수준에서 참조를 유지해야합니다. Dagger 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);
Dagger 2 또는 유사한 의존성 주입 프레임 워크를 사용하는 경우이 질문 에 대한 답변에 몇 가지 예가 있습니다.
답변
TokenAuthenticator
@theblang과 같은 답변을 사용 하는 것이 올바른 방법입니다 refresh_token
.
여기 내 구현이 있습니다 (Kotlin, Dagger, 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 주석과 같은 종속성주기 를 방지하기 위해 다음 과 같은 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())
}
}
마지막으로 서비스 PotoAuthApi를 만들 때 Interceptor
and Authenticator
에 추가 하십시오.OKHttpClient
데모
https://github.com/PhanVanLinh/AndroidMVPKotlin
노트
인증 자 흐름
- API
getImage()
리턴 401 오류 코드 예 authenticate
방법의 내부는TokenAuthenticator
것이다 발사noneAuthAPI.refreshToken(...)
호출 된 동기화noneAuthAPI.refreshToken(...)
응답 후 -> 새 토큰이 헤더에 추가됩니다.getImage()
의지 AUTO라는 (새 헤더로HttpLogging
로그인하지 않을 것이다 (이 호출)를intercept
내부에AuthInterceptor
의지가 호출되지 않습니다 )-
getImage()
오류 401로 여전히 실패 하면authenticate
내부 메소드TokenAuthenticator
가 AGAIN 및 AGAIN 을 발생시키고 호출 메소드에 대한 오류를 여러 번 발생시킵니다 (java.net.ProtocolException: Too many follow-up requests
). 카운트 응답으로 방지 할 수 있습니다 . 예를 들어, 만약 당신이return null
에서authenticate
3 번 시도 후getImage()
것이다 완료 및return response 401
-
경우
getImage()
(당신이 전화 같은 응답 성공하기 => 우리는 일반적으로 결과를 발생합니다getImage()
오류없이)
도움이되기를 바랍니다.
답변
나는 이것이 오래된 실을 알고 있지만 누군가가 넘어 질 경우를 대비하여.
TokenAuthenticator는 서비스 클래스에 따라 다릅니다. 서비스 클래스는 OkHttpClient 인스턴스에 따라 다릅니다. OkHttpClient를 만들려면 TokenAuthenticator가 필요합니다. 이주기를 어떻게 끊을 수 있습니까? 두 개의 다른 OkHttpClient? 그들은 다른 연결 풀을 가질 것입니다.
나는 같은 문제에 직면했지만 TokAuthenticator 자체를 위해 다른 하나가 필요하다고 생각하지 않고 OkHttpClient becuase를 하나만 만들고 싶었습니다 .Dagger2를 사용하고 있었기 때문에 Lazy가 주입 된 서비스 클래스를 제공 했습니다. TokenAuthenticator, 단검 2의 Lazy injection에 대한 자세한 내용은 여기를 참조하십시오 . 그러나 기본적으로 Dagger가 TokenAuthenticator에 필요한 서비스를 작성 하지 말라고 말하는 것과 같습니다 .
샘플 코드는이 SO 스레드를 참조 할 수 있습니다. Dagger2를 계속 사용하면서 순환 종속성을 해결하는 방법은 무엇입니까?
답변
특정 예외를 포착 한 다음 필요에 따라 작동 할 수있는 모든 로더에 대한 기본 클래스를 작성할 수 있습니다. 행동을 넓히려면 다른 모든 로더를 기본 클래스에서 확장하십시오.
답변
오랜 연구 끝에, 액세스 토큰을 매개 변수로 전송하는 RefreshTo Access Forken for Retrofit을 처리하도록 Apache 클라이언트를 사용자 정의했습니다.
쿠키 영구 클라이언트를 사용하여 어댑터 시작
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;
}
}