การรีเฟรชโทเค็น OAuth โดยใช้ชุดติดตั้งเพิ่มเติมโดยไม่แก้ไขการโทรทั้งหมด


157

เรากำลังใช้ Retrofit ในแอพ Android ของเราเพื่อสื่อสารกับเซิร์ฟเวอร์ที่ปลอดภัย OAuth2 ทุกอย่างใช้งานได้ดีเราใช้ RequestInterceptor เพื่อรวมโทเค็นการเข้าถึงกับการโทรแต่ละครั้ง อย่างไรก็ตามจะมีบางครั้งที่โทเค็นการเข้าถึงจะหมดอายุและโทเค็นจะต้องมีการรีเฟรช เมื่อโทเค็นหมดอายุการโทรครั้งต่อไปจะกลับมาพร้อมรหัส HTTP ที่ไม่ได้รับอนุญาตดังนั้นจึงง่ายต่อการตรวจสอบ เราสามารถแก้ไขการโทรติดตั้งเพิ่มเติมแต่ละครั้งด้วยวิธีต่อไปนี้: ในการโทรกลับล้มเหลวให้ตรวจสอบรหัสข้อผิดพลาดหากเท่ากับไม่ได้รับอนุญาตให้รีเฟรชโทเค็น OAuth จากนั้นทำซ้ำการโทรติดตั้งเพิ่มเติม อย่างไรก็ตามสำหรับสิ่งนี้การโทรทั้งหมดควรได้รับการแก้ไขซึ่งไม่สามารถบำรุงรักษาได้ง่ายและเป็นทางออกที่ดี มีวิธีการทำเช่นนี้หรือไม่โดยไม่แก้ไขการโทรติดตั้งเพิ่มทั้งหมดหรือไม่


1
นี้มีลักษณะที่เกี่ยวข้องกับของฉันคำถามอื่นฉันจะตรวจสอบอีกครั้งในเร็ว ๆ นี้ แต่วิธีหนึ่งที่เป็นไปได้คือการห่อ OkHttpClient บางสิ่งเช่นนี้: github.com/pakerfeldt/signpost-retrofit นอกจากนี้เนื่องจากฉันใช้ RoboSpice กับ Retrofit การสร้างคลาส Request base อาจเป็นอีกวิธีหนึ่งที่เป็นไปได้เช่นกัน อาจเป็นไปได้ว่าคุณต้องคิดหาวิธีที่จะทำให้การไหลเวียนของคุณไม่มีบริบทแม้ว่าอาจจะใช้อ็อตโต / อีเวนต์บัส
Hassan Ibraheem

1
คุณสามารถแยกมันออกแล้วเอาเคสที่ไม่จำเป็นออกไปได้ ฉันจะพิจารณาเรื่องนี้ในวันนี้และโพสต์ที่นี่หากฉันประสบความสำเร็จบางอย่างที่อาจช่วยแก้ปัญหาของเรา
Daniel Zolnai

2
ปรากฏว่าห้องสมุดไม่ได้จัดการโทเค็นการรีเฟรช แต่ให้แนวคิดแก่ฉัน ฉันทำส่วนเล็กน้อยเกี่ยวกับรหัสไม่ได้ทดสอบ แต่ตามทฤษฎีแล้วฉันคิดว่าควรใช้งานได้: gist.github.com/ZolnaiDani/9710849
Daniel Zolnai

3
@neworld โซลูชันที่ฉันนึกถึง: ทำการเปลี่ยนแปลง TokenInRequest (... ) ที่ซิงโครไนซ์และในบรรทัดแรกให้ตรวจสอบว่าเป็นครั้งสุดท้ายที่มีการรีเฟรชโทเค็นหรือไม่ หากผ่านไปไม่กี่วินาที (มิลลิวินาที) ที่ผ่านมาอย่ารีเฟรชโทเค็น คุณยังสามารถตั้งค่ากรอบเวลานี้เป็น 1 ชั่วโมงหรือมากกว่านั้นเพื่อหยุดขอโทเค็นใหม่ตลอดเวลาเมื่อมีปัญหาอื่นนอกโทเค็นที่ล้าสมัย
Daniel Zolnai

2
ชุดติดตั้งเพิ่ม 1.9.0 เพิ่งเพิ่มการรองรับสำหรับ OkHttp 2.2 ซึ่งมีตัวดัก สิ่งนี้จะทำให้งานของคุณง่ายขึ้นมาก สำหรับข้อมูลเพิ่มเติมโปรดดู: github.com/square/retrofit/blob/master/ ...... และgithub.com/square/okhttp/wiki/ ตัวรับสัญญาณคุณต้องขยาย OkHttp ด้วยเช่นกัน
Daniel Zolnai

คำตอบ:


214

กรุณาอย่าใช้Interceptorsเพื่อจัดการกับการตรวจสอบ

ปัจจุบันวิธีที่ดีที่สุดในการตรวจสอบความถูกจับคือการใช้ใหม่AuthenticatorAPI ได้รับการออกแบบมาโดยเฉพาะสำหรับวัตถุประสงค์นี้

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);

6
นี่หมายความว่าทุกคำขอจะล้มเหลวเสมอ 1 ครั้งหรือคุณเพิ่มโทเค็นเมื่อทำคำขอหรือไม่
Jdruwe

11
@Jdruwe ดูเหมือนว่ารหัสนี้จะล้มเหลว 1 ครั้งจากนั้นจะทำการร้องขอ อย่างไรก็ตามหากคุณเพิ่ม interceptor ที่มีจุดประสงค์เพียงเพื่อเพิ่มโทเค็นการเข้าถึงเสมอ (ไม่ว่าจะหมดอายุหรือไม่ก็ตาม) สิ่งนี้จะถูกเรียกใช้เมื่อได้รับ 401 เท่านั้นซึ่งจะเกิดขึ้นเมื่อโทเค็นนั้นหมดอายุเท่านั้น
narciero

54
TokenAuthenticatorขึ้นอยู่กับserviceชั้นเรียน serviceระดับขึ้นกับOkHttpClientอินสแตนซ์ เพื่อสร้างผมจำเป็นที่จะต้องOkHttpClient TokenAuthenticatorฉันจะทำลายวงจรนี้ได้อย่างไร สองคนแตกต่างกันOkHttpClientอย่างไร พวกเขากำลังจะมีสระว่ายน้ำการเชื่อมต่อที่แตกต่างกัน ...
Brais Gabin

6
มีคำขอแบบขนานจำนวนมากที่ต้องรีเฟรชโทเค็น มันจะเป็นคำขอโทเค็นการรีเฟรชจำนวนมากในเวลาเดียวกัน จะหลีกเลี่ยงได้อย่างไร
Igor Kostenko

10
ตกลงดังนั้นวิธีแก้ไขปัญหาของ @ Ihor สามารถซิงโครไนซ์รหัสภายใน Authenticator มันแก้ไขปัญหาในกรณีของฉัน ในการร้องขอการรับรองความถูกต้อง (... ) วิธีการ: - ทำสิ่งใด ๆ ที่ทำให้มีพลัง - เริ่มการซิงโครไนซ์บล็อก (ซิงโครไนซ์ (MyAuthenticator.class) {... }) - ในบล็อกนั้นเพื่อเรียกการเข้าถึงปัจจุบัน โทเค็นการเข้าถึง (resp.request () ส่วนหัว ( "การอนุญาต").) - ถ้าไม่เพียงแค่ใช้มันอีกครั้งกับการปรับปรุงการเข้าถึงโทเค็น - วิ่งมิฉะนั้นรีเฟรชโทเค็นไหล - ปรับปรุง / ยังคงมีการปรับปรุงการเข้าถึงและการฟื้นฟูโทเค็น - เสร็จสิ้นการทำข้อมูลให้ตรงกันบล็อก - วิ่ง
Dariusz Wiechecki

65

หากคุณกำลังใช้Retrofit > = 1.9.0แล้วคุณสามารถใช้ประโยชน์จากOkHttp ของใหม่InterceptorOkHttp 2.2.0ซึ่งเป็นที่รู้จักใน คุณต้องการใช้Application Interceptorซึ่งอนุญาตให้คุณretry and make multiple callsได้

Interceptor ของคุณอาจมีลักษณะคล้ายรหัสเทียมนี้:

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และเพิ่ม interceptor เป็นแอพลิเคชัน Interceptor

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

และในที่สุดก็ใช้วิธีนี้เมื่อมีการสร้างของคุณOkHttpClientRestAdapter

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

คำเตือน:เนื่องจากJesse Wilson(จากสแควร์) กล่าวถึงที่นี่นี่เป็นพลังงานที่อันตราย

จากที่กล่าวมาฉันคิดว่านี่เป็นวิธีที่ดีที่สุดในการจัดการกับสิ่งนี้ในตอนนี้ หากคุณมีคำถามใด ๆ โปรดอย่าลังเลที่จะถามในความคิดเห็น


2
คุณจะรับสายซิงโครนัสใน Android อย่างไรเมื่อ Android ไม่อนุญาตให้มีการโทรผ่านเครือข่ายในเธรดหลัก ฉันพบปัญหาในการส่งคืนการตอบกลับจากการโทรแบบอะซิงโครนัส
lgdroid57

1
@ lgdroid57 คุณถูกต้องดังนั้นคุณควรอยู่ในเธรดอื่นเมื่อคุณเริ่มต้นคำขอเดิมที่เรียกใช้ตัวรับสัญญาณเพื่อให้ทำงานได้
theblang

3
สิ่งนี้ใช้งานได้ดีมากยกเว้นว่าฉันต้องปิดการตอบสนองก่อนหน้านี้หรือฉันจะรั่วการเชื่อมต่อก่อนหน้านี้ ... final Request newRequest = request.newBuilder () .... build (); . response.body () ปิด (); return chain.proceed (newRequest);
DallinDyer

ขอบคุณ! ฉันพบปัญหาที่การติดต่อกลับของคำขอเดิมได้รับข้อความแสดงข้อผิดพลาดเป็น "ปิด" แทนที่จะตอบกลับดั้งเดิมเนื่องจากร่างกายถูกใช้ใน Interceptor ฉันสามารถแก้ไขสิ่งนี้เพื่อการตอบสนองที่ประสบความสำเร็จ แต่ฉันไม่สามารถแก้ไขสิ่งนี้เพื่อการตอบกลับที่ล้มเหลว ข้อเสนอแนะใด ๆ
lgdroid57

ขอบคุณ @mattblang มันดูดี คำถามหนึ่งข้อคือรับประกันว่าจะมีการโทรกลับคำขอแม้ในการลองใหม่
Luca Fagioli

23

TokenAuthenticator ขึ้นอยู่กับระดับบริการ คลาสบริการขึ้นอยู่กับอินสแตนซ์ OkHttpClient ในการสร้าง OkHttpClient ฉันต้องใช้ TokenAuthenticator ฉันจะทำลายวงจรนี้ได้อย่างไร สอง OkHttpClients ที่แตกต่างกันอย่างไร พวกเขาจะมีกลุ่มการเชื่อมต่อที่แตกต่างกัน ..

หากคุณมีพูด Retrofit 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 หรือเฟรมเวิร์กการฉีดพึ่งพาที่คล้ายคลึงกันมีตัวอย่างบางส่วนในคำตอบของคำถามนี้


อยู่ที่ไหนTokenServiceชั้นสร้างขึ้น?
Yogesh Suthar

@YogeshSuthar เป็นบริการติดตั้งเพิ่ม - ดูคำถามที่เกี่ยวข้อง
David Rawson

ขอขอบคุณที่คุณสามารถให้การดำเนินการจากrefreshToken() service.refreshToken().execute();ไม่พบการใช้งานทุกที่
Yogesh Suthar

@Yogesh วิธี refreshToken มาจาก API ของคุณ อะไรก็ตามที่คุณโทรไปรีเฟรชโทเค็น (อาจมีการโทรด้วยชื่อผู้ใช้และรหัสผ่าน) หรืออาจเป็นคำขอที่คุณส่งโทเค็นและการตอบกลับเป็นโทเค็นใหม่
David Rawson

5

ใช้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)

Interceptor ของฉัน

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

บันทึก

กระแสการรับรองความถูกต้อง
  • ตัวอย่าง API getImage()ส่งคืนรหัสข้อผิดพลาด 401
  • authenticateวิธีการภายในTokenAuthenticatorจะถูกไล่ออก
  • ซิงโครไนซ์ที่noneAuthAPI.refreshToken(...)เรียกว่า
  • หลังจากnoneAuthAPI.refreshToken(...)ตอบกลับ -> โทเค็นใหม่จะเพิ่มในส่วนหัว
  • 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()โดยไม่มีข้อผิดพลาด)

หวังว่ามันจะช่วย


โซลูชันนี้ใช้ OkHttpClients 2 แบบที่แตกต่างกันอย่างเห็นได้ชัดในคลาส ServiceGenerator ของคุณ
SpecialSnowflake

@SpecialSnow เกล็ดคุณพูดถูก หากคุณทำตามวิธีแก้ปัญหาของฉันคุณต้องสร้าง 2 OkHttp เพราะฉันสร้างบริการ 2 รายการ (ไม่มีบริการรับรองความถูกต้อง) ฉันคิดว่ามันจะไม่ทำให้เกิดปัญหาใด ๆ แจ้งให้เราทราบความคิดของคุณ
Phan Van Linh

1

ฉันรู้ว่านี่เป็นด้ายเก่า แต่ในกรณีที่มีคนสะดุด

TokenAuthenticator ขึ้นอยู่กับระดับบริการ คลาสบริการขึ้นอยู่กับอินสแตนซ์ OkHttpClient ในการสร้าง OkHttpClient ฉันต้องใช้ TokenAuthenticator ฉันจะทำลายวงจรนี้ได้อย่างไร สอง OkHttpClients ที่แตกต่างกันอย่างไร พวกเขาจะมีกลุ่มการเชื่อมต่อที่แตกต่างกัน ..

ฉันกำลังเผชิญกับปัญหาเดียวกัน แต่ฉันต้องการสร้าง OkHttpClient เพียงอันเดียวเพราะฉันไม่คิดว่าฉันต้องการอีกอันหนึ่งสำหรับ TokenAuthenticator เองฉันใช้ Dagger2 ดังนั้นฉันเลยลงเอยด้วยการให้บริการที่Lazy ฉีดเข้าไปใน TokenAuthenticator คุณสามารถอ่านเพิ่มเติมเกี่ยวกับการฉีด Lazy ในกริช 2 ได้ที่นี่แต่ก็เหมือนกับการบอกให้ Dagger ไม่ไปและสร้างบริการที่ต้องการโดย TokenAuthenticator ทันที

คุณสามารถอ้างถึงเธรด SO นี้เพื่อรับโค้ดตัวอย่าง: วิธีแก้ไขการอ้างอิงแบบวนรอบในขณะที่ยังใช้ Dagger2 อยู่


0

คุณสามารถลองสร้างคลาสพื้นฐานสำหรับโหลดเดอร์ทั้งหมดของคุณซึ่งคุณสามารถตรวจจับข้อยกเว้นเฉพาะจากนั้นทำตามที่คุณต้องการ ทำให้รถตักที่แตกต่างของคุณขยายจากคลาสพื้นฐานเพื่อกระจายพฤติกรรม


ชุดติดตั้งเพิ่มเติมไม่ทำงานอย่างนั้น มันใช้คำอธิบายประกอบ java และอินเทอร์เฟซเพื่ออธิบายการเรียก API
Daniel Zolnai

ฉันรู้ว่าชุดติดตั้งเพิ่มเติมทำงานได้อย่างไร แต่คุณยัง "ปิด" การเรียก API ของคุณภายใน AsynTask ไม่ใช่คุณ?
k3v1n4ud3

ไม่ฉันใช้สายกับโทรกลับเพื่อให้พวกเขาทำงานแบบอะซิงโครนัส
Daniel Zolnai

จากนั้นคุณอาจสร้างคลาส callback พื้นฐานและทำให้ callback ทั้งหมดของคุณขยายได้
k3v1n4ud3

2
วิธีการแก้ปัญหานี้? เป็นกรณีของฉันที่นี่ = /
Hugo Nogueira

0

หลังจากการค้นคว้าแบบยาวฉันปรับแต่งไคลเอนต์ Apache เพื่อจัดการ Refreshing AccessToken สำหรับชุดติดตั้งเพิ่มเติมที่คุณส่งโทเค็นการเข้าถึงเป็นพารามิเตอร์

เริ่มต้นอะแดปเตอร์ของคุณด้วย Cookie Persistent Client

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;
    }
}

มีเหตุผลที่คุณใช้ ApacheClient แทนที่จะเป็นวิธีการแก้ปัญหาที่แนะนำหรือไม่? ไม่ใช่ว่ามันไม่ใช่ทางออกที่ดี แต่มันต้องการการเข้ารหัสที่มากขึ้นเมื่อเทียบกับการใช้ Interceptors
Daniel Zolnai

ปรับแต่งให้เป็นไคลเอนต์คุกกี้ถาวรรักษาเซสชันตลอดบริการ แม้ใน Request Intercceptor คุณสามารถเพิ่ม accesstoken ในส่วนหัว แต่ถ้าคุณต้องการที่จะเพิ่มมันเป็นพารามิเตอร์? OKHTTPClient ก็มีข้อ จำกัด เช่นกัน ref: stackoverflow.com/questions/24594823/…
Suneel Prakash

มันถูกทำให้เป็นแนวกว้างมากขึ้นที่จะใช้ในกรณีใด ๆ 1. ไคลเอนต์คุกกี้ถาวร 2. ยอมรับคำขอ HTTP และ HTTPS 3. อัพเดตโทเค็นการเข้าถึงใน Params
Suneel Prakash

0

การใช้ Interceptor หนึ่งตัว (ฉีดโทเค็น) และหนึ่ง Authenticator (การดำเนินการรีเฟรช) ทำงาน แต่:

ฉันมีปัญหาการโทรซ้ำซ้อนเช่นกัน: การโทรครั้งแรกจะส่งคืน 401 : โทเค็นไม่ได้ถูกเรียกที่การโทรครั้งแรก (interceptor) และการรับรองความถูกต้องถูกเรียกว่า: มีการร้องขอสองครั้ง

การแก้ไขเป็นเพียงการยืนยันคำขอไปยังบิลด์ใน Interceptor อีกครั้ง:

ก่อน:

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 เสมอโดยใช้ตัวตรวจสอบสิทธิ์เท่านั้นและไม่มีตัวดัก


-2

สำหรับผู้ที่ต้องการแก้ไขการโทรพร้อมกัน / ขนานเมื่อรีเฟรชโทเค็น นี่เป็นวิธีแก้ปัญหา

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
    }
}
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.