Autenticação baseada em token com retrofit | Android OAuth 2.0
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.
Conteudo
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.