Taller de Desarrollo Móvil en Android

Ingeniería de Software y Sistemas Computacionales | La Salle Nezahualcóyotl

Introducción Técnica

En esta práctica, la aplicación dejará de ser estática. Implementaremos una arquitectura donde el Frontend valida la identidad contra un LDAP y utiliza esas credenciales para "firmar" peticiones hacia un Middleware API que gestiona mensajes en una base de datos.

Paso 1: Instalación de Dependencias Nucleares

¿Por qué? Necesitamos el SDK de UnboundID para hablar el protocolo binario de LDAP y Retrofit para el protocolo textual de la API (JSON).
📍 Archivo: Gradle Scripts > build.gradle.kts (Module :app)

Añade estas librerías dentro del bloque dependencies { ... }:

dependencies {
    // 1. Cliente LDAP (Protocolo de Identidad)
    implementation("com.unboundid:unboundid-ldapsdk:6.0.8")
    
    // 2. Retrofit y Gson (Protocolo de Datos)
    implementation("com.squareup.retrofit2:retrofit:2.9.0")
    implementation("com.squareup.retrofit2:converter-gson:2.9.0")
    
    // 3. Corrutinas (Gestión de Hilos)
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4")
}

⚠️ Haz clic en "Sync Now" y espera a que Android Studio termine la indexación.

Paso 2: Definición de Entidades de Datos

¿Por qué? Kotlin necesita saber cómo "dibujar" el JSON que viene de la API. Creamos una Data Class que mapee los campos exactos del endpoint /api/mensajes.
📍 Nuevo Archivo: Mensaje.kt (en mx.lasalle.ciclovida)
package mx.lasalle.ciclovida

data class Mensaje(
    val id: Int,
    val id_usuario: String,
    val mensaje: String,
    val fecha: String
)

Paso 3: Definición del Servicio REST (ApiService)

¿Por qué? Aquí definimos los contratos de comunicación. El uso de @Header es vital: la API no procesará el POST o DELETE si los encabezados x-user y x-password no son válidos en el LDAP.
📍 Nuevo Archivo: ApiService.kt
package mx.lasalle.ciclovida

import retrofit2.Response
import retrofit2.http.*

interface ApiService {
    // Consulta pública de todos los mensajes
    @GET("/api/mensajes")
    suspend fun consultarMensajes(): Response<List<Mensaje>>

    // Publicación protegida por identidad LDAP
    @POST("/api/mensajes")
    suspend fun publicarMensaje(
        @Header("x-user") user: String,
        @Header("x-password") pass: String,
        @Body body: Map<String, String>
    ): Response<Void>
}

Paso 4: Fase de Autenticación (MainActivity.kt)

Lógica: Cuando el alumno presiona "Ingresar", el código inicia una tarea en segundo plano. Se conecta a la IP del profesor (10.118.137.27) y realiza un Bind. Si es exitoso, guarda la sesión y "abre la puerta" hacia la siguiente pantalla.
📍 Archivo: MainActivity.kt (Sustituir lógica del botón)
// Asegúrate de usar ViewBinding
binding.btnEnviar.setOnClickListener {
    val idCapturado = binding.etNombre.text.toString()
    val passCapturada = binding.etPassword.text.toString()

    if (idCapturado.isNotEmpty() && passCapturada.isNotEmpty()) {
        CoroutineScope(Dispatchers.IO).launch {
            try {
                // Conectando al OpenLDAP en el Docker del Profesor
                val connection = LDAPConnection("10.118.137.27", 389)
                val userDN = "cn=$idCapturado,dc=grupov4,dc=lasalle,dc=local"
                
                val result = connection.bind(userDN, passCapturada)

                if (result.resultCode == ResultCode.SUCCESS) {
                    withContext(Dispatchers.Main) {
                        // PERSISTENCIA: Guardamos la identidad para el CRUD posterior
                        val prefs = getSharedPreferences("PREFS_SESION", MODE_PRIVATE).edit()
                        prefs.putString("USER_NAME", idCapturado)
                        prefs.putString("USER_PASS", passCapturada)
                        prefs.apply()

                        startActivity(Intent(this@MainActivity, HomeActivity::class.java))
                        finish()
                    }
                }
            } catch (e: Exception) {
                withContext(Dispatchers.Main) {
                    Toast.makeText(this@MainActivity, "Identidad LDAP no válida", Toast.LENGTH_SHORT).show()
                }
            }
        }
    }
}

Paso 5: Consumo Protegido (HomeActivity.kt)

Visualización: Al cargar HomeActivity, se recuperan los datos de SharedPreferences y se hace la petición a la API. Los resultados se inyectan en el RecyclerView que ya teníamos de prácticas anteriores.
📍 Archivo: HomeActivity.kt
override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    binding = ActivityHomeBinding.inflate(layoutInflater)
    setContentView(binding.root)

    // Recuperar credenciales validadas en el Login
    val user = getSharedPreferences("PREFS_SESION", MODE_PRIVATE).getString("USER_NAME", "") ?: ""
    val pass = getSharedPreferences("PREFS_SESION", MODE_PRIVATE).getString("USER_PASS", "") ?: ""

    // Cargar los mensajes desde la API REST
    obtenerDatosDelServidor()
}

private fun obtenerDatosDelServidor() {
    val retrofit = Retrofit.Builder()
        .baseUrl("http://10.118.137.27:8080") // Middleware API
        .addConverterFactory(GsonConverterFactory.create())
        .build()

    val service = retrofit.create(ApiService::class.java)

    CoroutineScope(Dispatchers.IO).launch {
        val response = service.consultarMensajes()
        if (response.isSuccessful) {
            val lista = response.body()?.map { "${it.id_usuario}: ${it.mensaje}" } ?: emptyList()
            withContext(Dispatchers.Main) {
                // Actualizamos la vista (RecyclerView)
                binding.rvUsuarios.adapter = UserAdapter(lista)
            }
        }
    }
}

📝 Evaluación de Ingeniería de Software

1. Autenticación Delegada: ¿Por qué la aplicación móvil no tiene permiso de "ver" las contraseñas de otros usuarios en el LDAP y solo puede intentar un Bind con la suya propia?

2. Arquitectura Middleware: Si la API REST fallara pero el servidor LDAP siguiera encendido, ¿qué partes de la aplicación seguirían funcionando y cuáles no? Justifica tu respuesta.

3. Seguridad de Red: Estamos usando HTTP en lugar de HTTPS para el laboratorio. Desde la perspectiva de un analista de seguridad, ¿qué herramienta permitiría capturar las contraseñas que viajan en el aire entre el móvil y el hotspot?

💡 Entregable: Realiza un registro exitoso, inicia sesión, publica un mensaje con tu nombre y toma captura de pantalla del RecyclerView actualizado con tu mensaje proveniente de la API.