[Résolu][Best Practices] Gérer la mise à  jour d'un token

MarcoDahMarcoDah Membre
janvier 2015 modifié dans API UIKit #1

Bonjour à  tous !


 


Tout d'abord Bonne Année !!


 


Je viens vers vous car je me demandais comment je pourrais gérer la mise à  jour de mon token. Je m'explique dans un cas plus concret.


 


1° - Je veux afficher une page avec beaucoup de données.


2° - J'attaque mon API avec un access_token


3° - Je récupère donc mes données sous format JSON ( jusqu'ici pas de soucis )


 


Dans un soucis d'esthétique, je bloque tout affichage durant cette recherche et affiche à  la place un début de view bluré avec un spinner d'attente. Encore une fois ce passage ne me pose pas de difficulté. J'utilise un sémaphore pour bloquer tout autre action.


 


Le problème apparaà®t lorsque je veux refresh mon token. En effet, j'ai donc un objet "APIService " que j'utilise pour faire tout mes appels. Une des méthodes de cet objet est 



getProduct(NSString *) idWine

Lorsque que le token est expiré j'ai donc dans la partie " failure" de ma requête un appel à  la méthode



hasToRefreshToken(Token * tokenToRefresh)

Et c'est là  qu'apparaà®t le problème. A cause du sémaphore il m'est impossible d'accéder à  cette fonction. A croire que tant que je suis dans un sémaphore je ne peux pas faire appel à  une autre méthode dans celui ci. Voici donc comment je procède :



// Dans ce sens ça marche mais ma page
// n'affiche rien car la requête se termine après que l'affichage se fasse


AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
manager.responseSerializer = [AFJSONResponseSerializer serializer];
manager.responseSerializer.acceptableContentTypes = [NSSet setWithObject:@application/json];

dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);

manager.completionQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

NSDictionary *parameters = @{@access_token: token.accessToken};

AFHTTPRequestOperation* operation = [manager GET:queryURL parameters:parameters
success:^(AFHTTPRequestOperation *operation, id responseObject)
{
//NSLog(@JSON : %@", responseObject);
product = responseObject;

[KVNProgress showSuccessWithStatus:@Chargement effectué !];

dispatch_semaphore_signal(semaphore);


}
failure:^(AFHTTPRequestOperation *operation, NSError *error)
{
//NSLog(@Erreur n ° %ld,(long)error.code);
NSLog(@ERROR PRODUCT :%@",error.description);

dispatch_semaphore_signal(semaphore);

if(error.code == -1011)
{
NSLog(@On refresh le token);

[self hasToRefreshToken:token];

[self getProduct:idWine];
}
}];
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

// Comme ci dessous, la requête se bloque
// En gros mon application se bloque dans la fonction de MagicalRecord saveContext
// Et ne sors jamais de cette état bloquant


AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
manager.responseSerializer = [AFJSONResponseSerializer serializer];
manager.responseSerializer.acceptableContentTypes = [NSSet setWithObject:@application/json];

dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);

manager.completionQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

NSDictionary *parameters = @{@access_token: token.accessToken};

AFHTTPRequestOperation* operation = [manager GET:queryURL parameters:parameters
success:^(AFHTTPRequestOperation *operation, id responseObject)
{
//NSLog(@JSON : %@", responseObject);
product = responseObject;

[KVNProgress showSuccessWithStatus:@Chargement effectué !];

dispatch_semaphore_signal(semaphore);


}
failure:^(AFHTTPRequestOperation *operation, NSError *error)
{
NSLog(@Erreur n ° %ld,(long)error.code);
NSLog(@ERROR PRODUCT :%@",error.description);

if(error.code == -1011)
{
NSLog(@On refresh le token);

[self hasToRefreshToken:token];

[self getProduct:idWine];
}

dispatch_semaphore_signal(semaphore);


}];
dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);

Bon ça peut paraà®tre imbuvable mais la question que je me pose c'est comment gérer la phase de refresh_token pour qu'elle soit transparente aux yeux de l'utilisateur. 


 


Voila ce que je sous entend :


 


- sélection du produit par l'utilisateur


- getProduct(produit)


- access_token expiré


- hasToRefreshToken(token)


- getProduct(produit)


- affichage des informations sur le viewController


 


Une idée des best practices ? Comment faites vous ?


Je viens ici car j'ai trouvé une solution mais je ne suis clairement pas fier de celle ci et j'aimerai trouvé qu'elle que chose de plus "propre" .


 


Merci !


Mots clés:

Réponses

  • Salut,


     


    Bonn année à  toi aussi :).


     


    Pourquoi tu utilises un sémaphore ?


     


    Tu fais juste un enchainement de requêtes. 

  • Eh bien c'est ce que je pensais mais lorsque j'effectue effectivement cette enchainement de requête, le dictionnaire contenant les informations est null car la requête n'est pas fini. Je fais donc ça :



    serviceAPI * myService = [[serviceAPI alloc] init];

    NSDictionary * productInformations;

    productInformations = [myService getProduct:idWineAPI];

    Ensuite, j'utilise ce dictionnaire pour remplir mes différents labels et images. 


    Or à  la fin de cette succession de méthode si je n'ai pas mis de sémaphore mon dictionnaire est null car l'affichage c'est fait trop vite pas rapport à  la requête ( enfin c'est la conclusion conclusive à  laquelle je suis arrivé ! )


     


    Peut être est-ce une erreur avant qui déclenche ce comportement ? .


  • En fait ce que tu voudrais, c'est un espèce de truc que tu lui donnerais une requête HTTP à  exécuter et lorsque la requête serait finie, le truc appèlerait la méthode que tu veux.


     


    En somme un truc comme NSURLSession et les NSURLSessionTask : une requête, un completion handler et Zou !


  • MarcoDahMarcoDah Membre
    janvier 2015 modifié #5

    Alors tout d'abord merci.


     


    Je dois avouer que si c'est si simple que ça je m'en voudrais de m'être "obligé à  passer" via le framework AFNetworking ...


     


    Je vais essayer ça de mon coté je reviens vers vous pour vous tenir au courant.


     


    Merci


     


    EDIT : Alors j'ai regarder d'un peux plus près en suivant vos conseils. Mais je n'ai pas trouver mon bonheur. En regardant de plus près ta réponse je me suis aperçu que peut être j'avais mal compris. Je vais continuer de chercher pour voir si j'ai bien assimilé le NSURLSession.


     


    Mais en gros ce que je veux plus, c'est que l'affichage de mon viewcontroller ne se fasse que lorsque ma requête est fini( ce que je fais actuellement grâce à  un semaphore, même si je suis de moins en moins sure de cette solution ). Et lorsque lors de cette requête, je reçois une erreur de token expiré je puisse faire la phase de refresh token. Tout cela dans l'idée que ça soit transparent pour l'utilisateur. C'est à  dire qu'il n'y voit que du feu.


  • samirsamir Membre
    janvier 2015 modifié #6

    Tu peux t'en sortir avec des requêtes simples asynchrones, l'utilisation d'un sémaphore n'est pas justifié dans ton cas et ça vas juste compliquer ton code.


     


    J'ai l'impression que c'est la notion des blocks et le mode asynchrone que tu ne maitrise pas, non ?


     


    ton getProduct doit être de genre :



    - (void)getProductWithWineId:(NSString *)wineIdentifier withCompletionHandler:(void (^)(NSDictionnary *infosDic, NSError *error))completionBlock
    {
    AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];

    manager.responseSerializer = [AFJSONResponseSerializer serializer];

    manager.responseSerializer.acceptableContentTypes = [NSSet setWithObject:@application/json]

    NSDictionary *parameters = @{@access_token: token.accessToken};

    AFHTTPRequestOperation* operation = [manager GET:queryURL parameters:parameters success:^(AFHTTPRequestOperation *operation, id responseObject)
    {
    NSDictionary *dic = ...

    completionBlock(dic, nil);
    }
    failure:^(AFHTTPRequestOperation *operation, NSError *error)
    {
    if(error.code == -1011)
    {
    }
    }];
    }

    ici la méthode getProduct ne renvoie rien, et prend deux paramètre wineIdentifier et un block qui sera exécuter à  la fin de la méthode. Donc tu dois mettre à  jour ton interface à  la fin de cette méthode, c'est à  dire dans le block.


     


    la même chose pour le refreshToken, c'est une requête asynchrone.

  • Alors en fait c'est pas exactement ça (j'aurais pu le mettre direct cela aurait été plus simple). Je renvoie un dictionnary puisque je l'appelle via le viewController.



    -(NSDictionary *) getProduct:(NSString *) idWine
    {
    __block NSDictionary * product;

    Token * token = [Token MR_findFirst];

    NSString * queryURL = [NSString stringWithFormat:@%@%@%@.json",HOST,PRODUCT,idWine];

    AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
    manager.responseSerializer = [AFJSONResponseSerializer serializer];
    manager.responseSerializer.acceptableContentTypes = [NSSet setWithObject:@application/json];

    dispatch_semaphore_t semaphore = dispatch_semaphore_create(0);

    manager.completionQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);

    NSDictionary *parameters = @{@access_token: token.accessToken};

    AFHTTPRequestOperation* operation = [manager GET:queryURL parameters:parameters
    success:^(AFHTTPRequestOperation *operation, id responseObject)
    {
    //NSLog(@JSON : %@", responseObject);
    product = responseObject;

    dispatch_semaphore_signal(semaphore);

    }
    failure:^(AFHTTPRequestOperation *operation, NSError *error)
    {
    NSLog(@Erreur n ° %ld,(long)error.code);
    NSLog(@ERROR PRODUCT :%@",error.description);

    dispatch_semaphore_signal(semaphore);

    if(error.code == -1011)
    {
    NSLog(@On refresh le token);

    [self hasToRefreshToken:token];

    [self getProduct:idWine];
    }
    }];
    dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER);


    return product;
    }


    Mais effectivement je patoge un peu avec ces notions d'asynchrones car au final c'est ça qui empêche un bon affichage de mon coté ..


    Je vais revoir les notions de blocs dans la doc .


     


    Merci pour ces conseils.


  • Analyse gros grain, je n'ai pas regardé le détail ...


    Il me semble qu'il y a un dead lock, la méthode getProduct est récursive et attend le sémaphore. Du coup le sémaphore est attendu deux fois mais émis qu'une seule fois. 


  • Justement elle est la ton erreur " je renvoie un dictionnaire", c'est un appel asynchrone, la méthode retourne avant même que la requête ne soit finie, donc le dictionnaire renvoyé est nil. 


     


    Revoie la notion des blocks/asynchrone/.... y a des super discussions sur le forum à  propos de tout ça.


  • Ca marche. Je vous tiens au courant quand j'aurais réussis.


     


    Merci pour les pistes !


  • Me revoila ( désolé pour le double post )


     


    Merci Samir, j'ai maintenant compris comment marcher la base des blocks.


    Effectivement, je me compliqué la tâche. Voici donc le résultat pour ceux qui auront du mal comme moi:



    -(void) getProduct:(NSString *) idWine withCompletionHandler:(void (^)(NSDictionary *infosDic, NSError *error))completionBlock
    {
    __block NSDictionary * product;

    Token * token = [Token MR_findFirst];

    NSString * queryURL = [NSString stringWithFormat:@%@%@%@.json",HOST,PRODUCT,idWine];

    AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
    manager.responseSerializer = [AFJSONResponseSerializer serializer];
    manager.responseSerializer.acceptableContentTypes = [NSSet setWithObject:@application/json];

    NSDictionary *parameters = @{@access_token: token.accessToken};

    AFHTTPRequestOperation* operation = [manager GET:queryURL parameters:parameters
    success:^(AFHTTPRequestOperation *operation, id responseObject)
    {
    //NSLog(@JSON : %@", responseObject);
    product = responseObject;


    completionBlock(product, nil);


    }
    failure:^(AFHTTPRequestOperation *operation, NSError *error)
    {
    NSLog(@Erreur n ° %ld,(long)error.code);
    NSLog(@ERROR PRODUCT :%@",error.description);


    if(error.code == -1011)
    {
    [self hasToRefreshToken:token withCompletionHandler:^(NSError * error){

    }];


    }
    }];




    Voila pour le chargement des données et le refresh_token.


    Le problème c'est que du coup ma vue se charge avec rien de nouveau vu que lorsque mon token est expiré je faios la pase de refresh token. Qui soit dit en passant se passe très bien.


     


    J'ai essayé de faire une récursivité comme ceci:



    if(error.code == -1011)
    {
    [self hasToRefreshToken:token withCompletionHandler:^(NSError * error){

    __weak serviceAPI *weakSelf = self;

    [weakSelf getProduct:idWine withCompletionHandler:^(NSDictionary *infosDic, NSError *error){

    NSLog(@Après refresh Token, on relance la recherche de produit : %@", infosDic);

    }];

    }];


    }


    Sans très grand succès. Auriez vous une piste  ?


     


    Je rappelle un peu les étapes pour rafraichir la mémoire :


     


    - getProduct()


    -Si ( token is expire ) alors



    refresh_token

    ( - getProduct() )


    -affficher les données sur le viewController

  • samirsamir Membre
    janvier 2015 modifié #12

    Voila une à  peux près comment le faire. (code non compilé).


     


    ( rajoute le patter weakSelf... pour ne pas créer un cycle de référence dans les blocks, je te laisse regarder dans le forum).



    - (void)updateProductView /* Ou tu le nomme comme tu veux */ {
        
        // Récupère d'abbord ton token
         if (/* token Non valid*/) {
                [self refreshTokenWithCompletionHandler:^(NSString *newToken, NSError *error) {
                
                    if (error) {
                        // Traiter le cas d'erreur
                    }
                    else {
    // surement tu dois mettre à  jour ton nouveau token,
                        [self fetchProducts];
                    }
                    
            }];
        }
         else{
             [self fetchProducts];
         }
    }

    - (void)fetchProducts {
        
        // Affiche ton spinner ici ou la vue de chargement ....
        
        NSString *identifier = nil; // remplie bien les params qu'il faut
        [self getProductWithIdentifier:identifier compeltionHandler:^(NSDictionary *dic, NSError *error) {
            
            // cache ta vue de chargement ou ton spinner
            
            if (error) {
                // Traiter le cas d'erreur
            }
            else{
                // Mise à  jour de ton interface avec tes nouvelles données
            }
        }];

    }

    - (void)getProductWithIdentifier:(NSString *)id compeltionHandler:(void (^)(NSDictionary *dic, NSError *error))completionBlock {
        // implémente de la méthode
    }

    - (void)refreshTokenWithCompletionHandler:(void (^)(NSString *newToken, NSError *error))completionHandler {
    // implémentation de la méthode
    }

  • Bonjour,


     


    J'avais oublier de venir donner mon résultat.


    Tout fonctionne idéalement. Je n'ai malheureusement pas utilisé ta solution @samir car à  mes yeux elle présente un petit problème: Le time stamp entre le device et le serveur. En effet pour tester la validité du token sans faire d'appel sur mon API, je peux regarder mon champs " expire " mais il est possible qu'il y ai un décalage entre le fuseau de mon serveur et celui de mon téléphone.


     


    Du coup, la deuxième solution consiste à  attendre le retour de l'API. Et c'est ce que j'ai utilisé :



    Token * token = [Token MR_findFirst];



    [myService getProduct:idWineAPI withCompletionHandler:^ (NSDictionary* product,NSError * errorProduct){

    if(errorProduct)
    {
    [myService hasToRefreshToken:token withCompletionHandler:^(NSError * errorRefresh)
    {

    if (errorProduct.code == -1011)
    {
    [self tokenHasBeenRefresh];
    }


    }];

    }
    else
    {
    [self updateProductView:product];
    }

    }];


    Et du coup dans la fonction de 'tokenHasBeenRefresh" je refais l'appel à  la fonction de getProduct.


     


    Tout fonctionne parfaitement merci beaucoup !!


Connectez-vous ou Inscrivez-vous pour répondre.