Autenticação baseada em token com retrofit | Android OAuth 2.0

Tempo de leitura: 5 minutes

Retrofit é um cliente HTTP de tipo seguro da Square que foi construído para a plataforma Android. Ele oferece uma maneira fácil e limpa de fazer chamadas de rede REST API e analisa as respostas JSON / XML em objetos Java que podemos usar em nosso aplicativo.

Como medida de segurança, a maioria dos pontos de acesso API exige que os usuários forneçam um token de autenticação que pode ser usado para verificar a identidade do usuário que está fazendo a solicitação, a fim de conceder-lhes acesso aos dados/recursos do back-end. O aplicativo cliente geralmente busca o token após o login ou registro bem-sucedido e, em seguida, salva o token localmente e o anexa às solicitações subsequentes para que o servidor possa autenticar o usuário.

Neste blog, veremos uma maneira limpa de anexar o token do usuário conectado às nossas solicitações de API do aplicativo assim que o usuário fizer login. Nosso caso de uso pressupõe que o usuário precisa buscar uma lista de postagens do servidor.

 

Projeto de configuração

Primeiro, prosseguiremos e criaremos um novo projeto Android Studio. Para este projeto, usaremos Kotlin, no entanto, a mesma implementação funciona para Java.

Adicione as dependências de Retrofit ao seu app/build.gradle:

dependencies {
    ...

    // Network
    implementation 'com.squareup.retrofit2:retrofit:2.7.1'
    implementation 'com.squareup.retrofit2:converter-gson:2.4.0'
    implementation 'com.squareup.okhttp3:okhttp:4.2.1'

    ...
}

Em seguida, adicione a permissão de internet em seu AndroidManifest.xml

<uses-permission android:name="android.permission.INTERNET"/>

 

Modelos de configuração

Vamos criar a classe User.kt que conterá os detalhes básicos do usuário. Para o nosso caso de uso, ele conterá apenas o ID do usuário, nome, sobrenome e e-mail.

data class User (
    @SerializedName("id") 
    var id: String,
    
    @SerializedName("first_name")
    var firstName: String,
    
    @SerializedName("last_name")
    var lastName: String,
    
    @SerializedName("email")
    var email: String
)

Para fazer o login, o usuário deverá fornecer o e-mail e a senha, então vamos criar a classe de dados LoginRequest.kt.

data class LoginRequest (
    @SerializedName("email")
    var email: String,
    
    @SerializedName("password")
    var password: String
)

Com o login bem-sucedido, o usuário receberá uma resposta contendo o código de status, token de autenticação e detalhes do usuário. Vamos criar o LoginResponse.kt.

data class LoginResponse (
    @SerializedName("status_code")
    var statusCode: Int,

    @SerializedName("auth_token")
    var authToken: String,

    @SerializedName("user")
    var user: User
)

 

Configuração de Retrofit

Criaremos uma classe Constants.kt que conterá nossas variáveis estáticas.

object Constants {

    // Endpoints
    const val BASE_URL = "https://baseurl.com/"
    const val LOGIN_URL = "auth/login"
    const val POSTS_URL = "posts"
    
}

Em seguida, criaremos a classe ApiClient.kt que inicializará nossa instância do cliente Retrofit e a interface ApiService.kt onde definiremos nossas funções de solicitação de API.

/**
 * Interface para definir funções de solicitação REST
 */
interface ApiService {
  
    @POST(Constants.LOGIN_URL)
    @FormUrlEncoded
    fun login(@Body request: LoginRequest): Call<LoginResponse>

}
/**
 * Retrofit instance class
 */
class ApiClient {
    private lateinit var apiService: ApiService
    
    fun getApiService(): ApiService {
        
        // Initialize ApiService if not initialized yet
        if (!::apiService.isInitialized) {
            val retrofit = Retrofit.Builder()
                .baseUrl(Constants.BASE_URL)
                .addConverterFactory(GsonConverterFactory.create())
                .build()
            
            apiService = retrofit.create(ApiService::class.java)
        }
        
        return apiService
    }
    
}

 

Buscando o token

Para poder salvar e buscar o token no dispositivo do usuário, criaremos uma classe SessionManager.kt.

/**
 * Gerenciador de sessão para salvar e buscar dados de SharedPreferences
 */
class SessionManager (context: Context) {
    private var prefs: SharedPreferences = context.getSharedPreferences(context.getString(R.string.app_name), Context.MODE_PRIVATE)
    
    companion object {
        const val USER_TOKEN = "user_token"
    }

    /**
     * Função para salvar token de autenticação
     */
    fun saveAuthToken(token: String) {
        val editor = prefs.edit()
        editor.putString(USER_TOKEN, token)
        editor.apply()
    }

    /**
     * Função para buscar token de autenticação
     */
    fun fetchAuthToken(): String? {
        return prefs.getString(USER_TOKEN, null)
    }
}

Com o login bem-sucedido, salvaremos o token obtido.

class LoginActivity : AppCompatActivity() {
    private lateinit var sessionManager: SessionManager
    private lateinit var apiClient: ApiClient

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_login)

        apiClient = ApiClient()
        sessionManager = SessionManager(this)

        apiClient.getApiService().login(LoginRequest(email = "s@sample.com", password = "mypassword"))
            .enqueue(object : Callback<LoginResponse> {
                override fun onFailure(call: Call<LoginResponse>, t: Throwable) {
                    // Error logging in
                }

                override fun onResponse(call: Call<LoginResponse>, response: Response<LoginResponse>) {
                    val loginResponse = response.body()
                    
                    if (loginResponse?.statusCode == 200 && loginResponse.user != null) {
                        sessionManager.saveAuthToken(loginResponse.authToken)
                    } else {
                        // Error logging in
                    }
                }
            })

    }
}

 

Adicionando o token às nossas solicitações

Agora que nosso usuário pode fazer o login, podemos finalmente obter uma lista de postagens. Vamos primeiro criar um objeto Post.kt de amostra.

data class Post (
    @SerializedName("id")
    var id: Int,
    
    @SerializedName("title")
    var title: String,
    
    @SerializedName("description")
    var description: String,
    
    @SerializedName("content")
    var content: String
)

E a classe de dados PostsResponse.kt correspondente.

data class PostsResponse (
    @SerializedName("status_code")
    var status: Int,
    
    @SerializedName("message")
    var message: String,
    
    @SerializedName("posts")
    var posts: List<Post>
)

Para buscar a lista de postagens, podemos adicionar o token de autorização como um cabeçalho para a função de buscar postagens e, em seguida, passá-lo como um parâmetro:

interface ApiService {

    ...

    @GET(Constants.POSTS_URL)
    fun fetchPosts(@Header("Authorization") token: String): Call<PostsResponse>

}
class MainActivity : AppCompatActivity() {
    
    ...

    /**
     * Função para buscar postagens
     */
    private fun fetchPosts() {

        // Pass the token as parameter
        apiClient.getApiService().fetchPosts(token = "Bearer ${sessionManager.fetchAuthToken()}")
            .enqueue(object : Callback<PostsResponse> {
                override fun onFailure(call: Call<PostsResponse>, t: Throwable) {
                    // Error fetching posts
                }

                override fun onResponse(call: Call<PostsResponse>, response: Response<PostsResponse>) {
                    // Handle function to display posts
                }
            })
    }
}

Isso deve funcionar muito bem e devemos ser capazes de obter a lista de postagens. No entanto, usar esse método significa que, para cada solicitação autenticada, teremos que adicionar o parâmetro Header e passar o token da função que faz a solicitação. Não está limpo, está?

 

Usando um interceptor de solicitação

Felizmente, o Retrofit usa o Okhttp, por meio do qual podemos adicionar interceptores ao nosso cliente de retrofit. O retrofit aciona a instância do Interceptor sempre que uma solicitação é feita.

Vamos prosseguir e fazer um AuthInterceptor.kt para nossas solicitações para que possamos adicionar o token à solicitação.

/**
 * Interceptor para adicionar token de autenticação às solicitações
 */
class AuthInterceptor(context: Context) : Interceptor {
    private val sessionManager = SessionManager(context)

    override fun intercept(chain: Interceptor.Chain): Response {
        val requestBuilder = chain.request().newBuilder()

        // If token has been saved, add it to the request
        sessionManager.fetchAuthToken()?.let {
            requestBuilder.addHeader("Authorization", "Bearer $it")
        }

        return chain.proceed(requestBuilder.build())
    }
}

Em seguida, atualizaremos nosso ApiClient.kt para incluir o cliente Okhttp personalizado.

/**
 * Retrofit instance class
 */
class ApiClient {
    private lateinit var apiService: ApiService

    fun getApiService(context: Context): ApiService {

        // Inicializa o ApiService se ainda não foi inicializado
        if (!::apiService.isInitialized) {
            val retrofit = Retrofit.Builder()
                ...
                .client(okhttpClient(context)) // Add our Okhttp client
                .build()

            apiService = retrofit.create(ApiService::class.java)
        }

        return apiService
    }

    /**
     * Inicialize OkhttpClient com nosso interceptor
     */
    private fun okhttpClient(context: Context): OkHttpClient {
        return OkHttpClient.Builder()
            .addInterceptor(AuthInterceptor(context))
            .build()
    }

}

Em seguida, podemos remover o parâmetro de cabeçalho de nossa função de solicitação e da função que faz a solicitação e, em seguida, basta chamar as funções de solicitação diretamente. Para os terminais não autenticados, como login, o valor do token do Gerenciador de Sessão será nulo, portanto, não será adicionado à solicitação.

/**
 * Interface para definir funções de solicitação REST
 */
interface ApiService {

    ...

    @GET(Constants.POSTS_URL)
    fun fetchPosts(): Call<PostsResponse>

}
class MainActivity : AppCompatActivity() {
    ...

    /**
     * Função para buscar postagens
     */
    private fun fetchPosts() {
        apiClient.getApiService(this).fetchPosts()
            .enqueue(object : Callback<PostsResponse> {
                override fun onFailure(call: Call<PostsResponse>, t: Throwable) {
                    // Error fetching posts
                }

                override fun onResponse(call: Call<PostsResponse>, response: Response<PostsResponse>) {
                    // Handle function to display posts
                }
            })
    }
}

 

Conclusão

O retrofit é uma das melhores bibliotecas de solicitação de HTTP para Android e, ao desacoplar a função para adicionar o token ao cabeçalho da solicitação, podemos tornar nosso código mais limpo e mais fácil de manter.