모든 호출을 수정하지 않고 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- 예제 API
getImage()
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 null
에authenticate
3번 재시도 후,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
'programing' 카테고리의 다른 글
치명적 오류: 선택적 값의 래핑을 해제하는 동안 예기치 않게 0이(가) 발견되었습니다. (0) | 2023.07.31 |
---|---|
PL/SQL BEGIN을 중첩해야 하는 시기...END 블록? (0) | 2023.07.31 |
Powershell 배열의 각 항목을 새 배열로 선택/매핑 (0) | 2023.07.31 |
스프링 부트 애플리케이션 내부에 리액트 웹 애플리케이션과 jar 패키징을 통합하는 방법 (0) | 2023.07.31 |
MySQL GROUP_CONCAT(null 포함) (0) | 2023.07.31 |