Forums Développement Multimédia

Les formations Mediabox
Les formations Mediabox

[Red5] Notions de base et authentification

Compatible ActionScript 3. Cliquer pour en savoir plus sur les compatibilités.Compatible Java. Cliquer pour en savoir plus sur les compatibilités.Par Meardon (Liutwin), le 01 mars 2010

Vous avez réalisé l'exploit de faire marcher red5, fait votre premier “hello world” et souhaitez à présent aller plus loin avec la partie java et la classe ApplicationAdapter ? Nous allons au cours de ce tutoriel aborder les notions de base de la connexion flash-java et leurs principaux moyens de communication : les Remote Procedural Call (appels de méthodes) et les SharedObject (objets partagés), tout en codant un module souvent indispensable aux applications client-serveur : l'authentification avec des identifiants (cet exemple se servira de SQL).

Pré-requis : http://ressources.mediabox.fr/tutoriaux/java/ma_premiere_application_red5

L'authentification

Nous aurons besoin de champs de saisie pour le pseudo et le mot de passe, un bouton de connexion, ainsi qu'un champ de texte dynamique qui indiquera au client la réponse du serveur.

La connexion côté flash

Sans trop entrer dans le détail du document de login, voici la connexion et la méthode associée au bouton de connexion :

var nc:NetConnection = new NetConnection();	// Instance de connexion
 
function appConnect(e:MouseEvent):void
{
	if ((connection.login_pseudo.text == "") || (connection.login_password.text == ""))
		connection.login_status.text = "Un des champs n'est pas rempli !";
	else
	{
		nc.connect("rtmp://monserveur.net/monapplication", connection.login_pseudo.text, connection.login_password.text);
		connection.login_status.text = "Connexion en cours...";
	}
}

Et la réponse du serveur :

function onStatus(e:NetStatusEvent):void
{
	if (e.info.code == "NetConnection.Connect.Success")
	{
		connection.login_status.text = "Connexion acceptée";
		appJoin();
	}
	else if (e.info.code == "NetConnection.Connect.Rejected")
		connection.login_status.text = e.info.application;
	else if (e.info.code == "NetConnection.Connect.Failed")
		connection.login_status.text = "Echec de connexion au serveur";
}
 
nc.addEventListener(NetStatusEvent.NET_STATUS, onStatus);

L'événement Rejected peut, comme nous allons le voir, renvoyer une information personnalisée sur le motif du rejet. La méthode appJoin, faite par vos soins, sera appelée en cas de succès de la connexion.

La classe Application.java

Voyons maintenant à quoi devrait ressembler le squelette de notre future application red5 :

import org.red5.server.adapter.ApplicationAdapter;
import org.red5.server.api.IClient;
import org.red5.server.api.IConnection;
import org.red5.server.api.IScope;
 
public class Application extends ApplicationAdapter
{
	public boolean appStart(IScope scope)
	{
		return true;
	}
 
	public boolean roomStart(IScope room)
	{
		if (!super.roomStart(room))
			return false;
 
		  return true;
	 }
 
	public boolean appConnect(IConnection conn, Object[] params)
	{
		return true;
	}
 
	public boolean appJoin(IClient client, IScope scope)
	{
		return true;
	}
 
	public void appLeave(IClient client, IScope scope)
	{
 
	}
}

Intéressons-nous tout d'abord à la méthode appConnect car c'est elle qui va nous permettre d'accepter ou de refuser la connexion. Le premier paramètre désigne la connexion crée par le client, le deuxième les paramètres qui sont transmis sous la forme d'un tableau d'objets. La valeur de retour permet d'indiquer si on accepte ou refuse la connexion. Nous allons commencer par récupérer les identifiants transmis :

public boolean appConnect(IConnection conn, Object[] params)
{
	String userName = params[0].toString();
	String userPsw = params[1].toString();
	IClient client = conn.getClient();
 
	return true;
}

L'interface IClient représente la session du client. Chose intéressante, on peut y enregistrer n'importe quel type de données qu'on peut par la suite récupérer à tout moment. Nous nous en servirons pour y conserver les identifiants.

Nous sommes dès à présent en mesure d'accepter ou de rejeter la connexion en écrivant une simple condition et en renvoyant false si elle n'est pas remplie. Mais ce ne serait pas très utile dans le cadre d'une application pouvant accueillir plusieurs comptes différents. Nous allons donc créer une nouvelle classe qui s'occupera de l'authentification en fonction des comptes enregistrés dans une base de données SQL.

La classe Login.java

import org.red5.server.adapter.ApplicationAdapter;
import org.red5.server.api.IConnection;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
 
public class Login extends ApplicationAdapter
{
	// Configuration base de données
	public final static String db = "jdbc:mysql://localhost/ma_bdd";
	public final static String db_username = "root";
	public final static String db_password = "";
 
	// Propriétés
	public String username;
	public String user_password;
 
	public Login(String userName, String userPsw)
	{
		username = userName;
		user_password = userPsw;
	}
 
	public boolean connection(IConnection conn)
	{
		boolean connection = false;  // Résultat du test
 
		try
		{
			Connection DBConn = DriverManager.getConnection(db,db_username, db_password);
			Statement stmt = DBConn.createStatement();
 
			String sql = "SELECT username,user_password FROM users_login WHERE username='"+username+"'";
			ResultSet result = stmt.executeQuery(sql);
			result.next();
 
			connection = user_password.equals(result.getString("user_password"));
			username = result.getString("username");
 
			result.close(); stmt.close(); DBConn.close();
		}
		catch (SQLException ex) { }
 
		return connection;
	}
}

Comme vous pouvez le constater la connexion SQL en java n'est pas compliquée une fois le code sous la main :) “Mais… ça me met des erreurs plein partout ce binz ?” :( Normal, je vous ai fait utiliser des méthodes qui n'existent pas nativement en java, pour les utiliser il vous faudra télécharger la librairie http://dev.mysql.com/downloads/connector/j/5.1.html à placer dans le dossier lib/ext/ du JRE (n'oubliez pas non plus d'importer la librairie dans votre projet java).

Voici notre authentification fonctionnelle :

public boolean appConnect(IConnection conn, Object[] params)
{
	String userName = params[0].toString();
	String userPsw = params[1].toString();
	IClient client = conn.getClient();
 
	Login login = new Login(userName, userPsw);
 
	if (!login.connection(conn))
	{
		rejectClient("Pseudo ou mot de passe incorrect !");
		return false;
	}
	else
	{
		client.setAttribute("userName", login.username);       
		client.setAttribute("userPwd", login.user_password);
		return true;
	}
}

Nous créons une nouvelle instance de Login puis appelons sa méthode connection (j'ai ici transmis l'objet IConnection qui peut être utile dans l'authentification, par exemple pour récupérer l'IP du client).

Nous avons vu l'événement Reject de l'objet NetConnection.Connect, en voici son déclencheur ;-) En cas de succès, les identifiants du client sont enregistrés dans sa session (on aurait aussi bien pu enregistrer directement l'objet de la classe Login).

Appels de méthode et objets partagés

Nous allons ajouter quelques fonctionnalités qui vont nous permettre de découvrir les principaux moyens de communication entre le serveur red5 et flash.

Récupération des identifiants et appels de méthode

Il peut être nécessaire de récupérer les identifiants côté flash. Vous me direz, nous les avons envoyés donc nous les connaissons ! Oui, mais… le serveur a toujours raison et pour des raisons de sécurité (ou de formatage) c'est à lui de valider et fournir ces informations ainsi que toute autre donnée de session dont vous auriez besoin… Nous allons en profiter pour voir une manière pour le serveur de communiquer avec le client : les appels de méthode. Commençons donc par écrire la méthode AS3 à appeler :

nc.client = this;
var login:Object;
 
function setLogin(userId:Number, userName:String, userPsw:String):void
{
	login.id = userId;
	login.pseudo = userName;
	login.password = userPsw;
}

La première instruction indique à la connexion que les appels s'effectueront sur les méthodes de cette classe. Attention : la méthode doit être déclarée comme public pour que le serveur puisse l'appeler !

Retournons côté java. Plutôt que d'envoyer les identifiants lors de la connexion, nous allons les envoyer au moment où le client rejoint réellement l'application, la méthode appJoin qui est appelée juste après appConnect.

public boolean appJoin(IClient client, IScope scope)
{
	// Envoie au client ses identifiants
	IConnection conn = Red5.getConnectionLocal();
	if (conn instanceof IServiceCapableConnection)
	{
		IServiceCapableConnection sc = (IServiceCapableConnection) conn;
		  sc.invoke("setLogin", new Object[]{client.getId(), 	client.getAttribute("userName"), client.getAttribute("userPsw")});
	}
 
	return true;
}

La méthode Red5.getConnectionLocal() permet de récupérer n'importe où dans l'application la connexion du client à l'origine de l'appel de la méthode. L'ID de session du client est un nombre unique incrémenté à chaque nouvelle connexion.

Empêcher les double connexions

Nous allons aller plus loin en empêchant un même client de se connecter plus d'une fois en même temps à l'application. Pour cela, nous avons besoin d'enregistrer la liste des clients connectés. Nous allons cette fois faire appel aux SharedObject. Il s'agit d'un fichier enregistré sur le serveur, qui peut être ou non persistant (qui est écrit sur le disque et subsiste si aucun client n'est connecté).

Observons la signature de la méthode permettant de créer un objet partagé :

public boolean createSharedObject(IScope scope, String name, boolean persistent)

Le deuxième paramètre représente le nom du fichier et le 3ème s'il est persistant ou non. Le premier paramètre a besoin d'un scope. Non, IScope n'est pas une interface pour coder un accessoire de jeu de tir :mrgreen: Le mieux à faire est d'enregistrer le scope dans une propriété globale dès le lancement de l'application pour pouvoir y accéder de n'importe où par la suite :

private IScope appScope;	// Scope de l'application
 
public boolean appStart(IScope scope)
{
	appScope = scope;
}

Nous pouvons à présent récupérer notre objet partagé :

ISharedObject so = getSharedObject(appScope, "onClientsList");

Notons que la méthode getSharedObject accepte un troisième paramètre boolean qui permet de forcer la création de l'objet s'il n'existe pas, nous passant ainsi de l'utilisation de createSharedObject.

Voici donc notre méthode appConnect finalisée :

public boolean appConnect(IConnection conn, Object[] params)
{
	String userName = params[0].toString();
	String userPsw = params[1].toString();
	IClient client = conn.getClient();
 
	Login login = new Login(userName, userPsw);
	ISharedObject so = getSharedObject(appScope, "onClientsList", true);
 
	if (!login.connection(conn))
	{
		rejectClient("Pseudo ou mot de passe incorrect !");
		return false;
	}
	else
	{
		if (so.hasAttribute(login.username))
		{
			rejectClient("Ce compte est déjà logué !");
			return false;
		}
 
		client.setAttribute("userName", login.username);       
		client.setAttribute("userPwd", login.user_password);
		so.setAttribute((String) login.username, client.getId());
		return true;
	}
}

…sans oublier bien sûr la suppression du client de la liste lorsque celui-ci quitte l'application…

public void appLeave(IClient client, IScope scope)
{
	ISharedObject so = getSharedObject(appScope, "onClientsList", true);
	so.removeAttribute((String) client.getAttribute("userName"));
}