MembershipProvider
1) Dans le projet MvcBlog, dans le répertoire Code, créer une classe MyMembershipProvider qui hérite de MembershipProvider
2) Demander à Visual Studio d'y implémenter les membres abstraits de MembershipProvider
On se retrouver avec un gros paquet de propriétés et de méthodes à implémenter.
Pas de panique, on ne codera pas tout dans la première version. Elle sera simple mais fonctionnelle.
Les première méthodes à regarder sont celles qui permettent de faire une sélection sur un ou plusieurs utilisateurs :
- GetUser (2 surcharges)
- GetUserNameByEmail
- FindUsersByEmail
- FindUsersByName
- GetAllUsers
On note que les trois dernière méthodes retournent une collection de type MembershipUserCollection.
Comme notre couche métier remonte des List<User>, la première chose à faire consiste à créer une méthode utilitaire pour faire la conversion.
On en profitera pour gérer les autres paramètres communs à ces méthodes
3) Dans la région 'Private' / 'Tools', créer la méthode GetUsers
#region Private
#region Inner Tools
/// <summary>
/// Retourne une MembershipUserCollection depuis une List<User>
/// </summary>
private MembershipUserCollection GetUsers(List<User> users, int pageIndex, int pageSize, out int totalRecords)
{
MembershipUserCollection retour = null;
// initialisation du total
totalRecords = 0;
// si valide
if(users != null)
{
// on instancie la collection de retour
retour = new MembershipUserCollection();
// on passe les pages
users.Skip(pageIndex * pageSize);
// on boucle sur les utilisateur
foreach(User user in users)
{
// si valide
if(user != null)
{
// on instancie un MembershipUser
MyMembershipUser mUser = MyMembershipUser.FromUser(user);
// on l'ajoute ) la collection
retour.Add(mUser);
}
}
// on compte le nb de résultats
totalRecords = retour.Count;
}
return retour;
}
#endregion
#endregion
4) Dans la région 'Public' / 'Users selection', coder maintenant les 6 méthodes de sélection d'utilisateur en se basant sur GetUsers pour celles qui retournent une MembershipUserCollection
#region Public
#region Users selection
/// <summary>
/// Récupère un utilisateur par son login
/// 'userIsOnline' n'est pas utilisé
/// </summary>
public override MembershipUser GetUser(string login, bool userIsOnline)
{
MembershipUser retour = null;
// on récupère l'utilisateur associé
User user = User.GetByLogin(login);
// si réussi
if(user != null)
// on instancie
retour = MyMembershipUser.FromUser(user);
return retour;
}
/// <summary>
/// Récupère un utilisateur par son ID
/// 'userIsOnline' n'est pas utilisé
/// </summary>
public override MembershipUser GetUser(object providerUserKey, bool userIsOnline)
{
MembershipUser retour = null;
// si la clef donnée est un entier
if(providerUserKey is Int32)
{
// on cast
int id = (int)providerUserKey;
// si valide
if(id > 0)
{
// on récupère l'utilisateur associé
User user = User.GetById(id);
// si réussi
if(user != null)
// on instancie
retour = MyMembershipUser.FromUser(user);
}
}
return retour;
}
/// <summary>
/// Récupère le login d'un utilisateur par son email
/// </summary>
public override string GetUserNameByEmail(string email)
{
string retour = null;
// on récupère l'utilisateur associé
User user = User.GetByEmail(email);
// si réussi
if(user != null)
// on retourne son login
retour = user.Login;
return retour;
}
/// <summary>
/// Récupère une collection d'utilisateur par un filtre sur l'email
/// </summary>
public override MembershipUserCollection FindUsersByEmail(string emailToMatch, int pageIndex, int pageSize, out int totalRecords)
{
return this.GetUsers(User.ListByEmail(emailToMatch), pageIndex, pageSize, out totalRecords);
}
/// <summary>
/// Récupère une collection d'utilisateur par un filtre sur le login
/// </summary>
public override MembershipUserCollection FindUsersByName(string usernameToMatch, int pageIndex, int pageSize, out int totalRecords)
{
return this.GetUsers(User.ListByLogin(usernameToMatch), pageIndex, pageSize, out totalRecords);
}
/// <summary>
/// Récupère tous les utilisateurs
/// </summary>
public override MembershipUserCollection GetAllUsers(int pageIndex, int pageSize, out int totalRecords)
{
return this.GetUsers(User.List(), pageIndex, pageSize, out totalRecords);
}
#endregion
5) Il faut maintenant s'occuper des tests de cette classe.
On créé une classe TestMembership dans un nouveau répertoire Code.
Les tests étant les même, on va tricher et copier/coller les tests du fichier TestUser, et remplacer dedans, au fur et à mesure qu'on les code, les lignes qu'on peut gérer avec notre MembershipProvider.
Exemple :
/// <summary>
/// Test 3 : chargement par login
/// </summary>
[TestMethod]
public void TestChargementParLogin()
{
MyMembershipProvider provider = new MyMembershipProvider();
// chargement : succes
MembershipUser user = (provider.GetUser(Login1, false));
Assert.IsNotNull(user, "Erreur chargement utilisateur (succes)");
// chargement : echec
user = provider.GetUser(LoginWrong, false);
Assert.IsNull(user, "Erreur chargement utilisateur (echec)");
}
6) Mettre de côté, dans une région 'Methodes non implémentées', les méthodes qu'on implémentera pas, à savoir :
- GetNumberOfUsersOnline
- DeleteUser
- UnlockUser
- GetPassword
- ChangePasswordQuestionAndAnswer
- ResetPassword
7) Renseigner les informations pour les propriétés suivantes, qu'on placera dans une région 'MembershipProvider configuration' :
- EnablePasswordReset
- EnablePasswordRetrieval
- RequiresQuestionAndAnswer
- RequiresUniqueEmail
- MaxInvalidPasswordAttempts
- MinRequiredNonAlphanumericCharacters
- MinRequiredPasswordLength
- PasswordAttemptWindow
- PasswordFormat
- PasswordStrengthRegularExpression
- ApplicationName
Ce qui doit donner :
#region MembershipProvider configuration
/// <summary>
/// Vrai si le provider autorise à réinitialiser le mot de passe
/// </summary>
public override bool EnablePasswordReset
{
get { return false; }
}
/// <summary>
/// Vrai si le provider autorise à retrouver un mot de passe
/// </summary>
public override bool EnablePasswordRetrieval
{
get { return false; }
}
/// <summary>
/// Vrai si le provider nécessite une question / réponse secrète
/// </summary>
public override bool RequiresQuestionAndAnswer
{
get { return false; }
}
/// <summary>
/// Vrai si le provider nécessite un email unique
/// </summary>
public override bool RequiresUniqueEmail
{
get { return true; }
}
/// <summary>
/// Nombre d'essais de mot de passe infructueux autorisés
/// </summary>
public override int MaxInvalidPasswordAttempts
{
get { return 0; }
}
/// <summary>
/// Nombres de caractères non-aplhanumériques autorisés dans le mot de passe
/// </summary>
public override int MinRequiredNonAlphanumericCharacters
{
get { return 0; }
}
/// <summary>
/// Taille minimum du mot de passe
/// </summary>
public override int MinRequiredPasswordLength
{
get { return 3; }
}
/// <summary>
/// Nombre de minutes à attendre si on ne veut pas que l'échec du mot de passe soit enregistré
/// </summary>
public override int PasswordAttemptWindow
{
get { return 1; }
}
/// <summary>
/// Format de stockage du mot de passe
/// </summary>
public override MembershipPasswordFormat PasswordFormat
{
get { return MembershipPasswordFormat.Hashed; }
}
/// <summary>
/// Expression régulière utilisée pour tester la force du mot de passe
/// </summary>
public override string PasswordStrengthRegularExpression
{
get { return "*"; }
}
/// <summary>
/// The name of the application using the custom membership provider.
/// </summary>
public override string ApplicationName
{
get { return "MvcBlog"; }
set
{
throw new ApplicationException("ApplicationName.set pas autorisé");
}
}
#endregion
Il ne reste plus que 4 méthodes, qu'on placera dans une région 'Securité principale'.
8) Implémenter les trois premières. C'est très faciles à coder grâce à notre travail sur User:
- UpdateUser
- ChangePassword
- ValidateUser
#region Securité principale
/// <summary>
/// Met à jour un utilisateur
/// </summary>
public override void UpdateUser(MembershipUser user)
{
// on essaye de le caster en MyMembershipUser
MyMembershipUser mUser = (user as MyMembershipUser);
// si c'en est un
if(mUser != null)
// on demande la mise à jour
mUser.Update();
// si ce n'en n'est pas un
else
//on lance une exception
throw new InvalidOperationException("The given MembershipUser must be a MvcBlog.Code.MyMembershipUser");
}
/// <summary>
/// Met à jour le mot de passe d'un utilisateur
/// </summary>
public override bool ChangePassword(string username, string oldPassword, string newPassword)
{
bool retour = false;
// on charge l'utilisateur
User user = User.GetByLogin(username);
// si réussi
if(user != null)
// on met à jour son mot de passe
retour = user.UpdatePassword(oldPassword, newPassword);
return retour;
}
/// <summary>
/// Authentifie un utilisateur
/// </summary>
public override bool ValidateUser(string username, string password)
{
return User.Authenticate(username, password);
}
#endregion
La dernière méthode, CreateUser est un peu plus complexe, puisqu'elle doit remonter un MembershipCreateStatus.
Pour le renseigner, il va falloir s'appuyer sur les contraintes d'unicité de la base de donnée, car il n'est pas fiable de faire les tests avant la création (voir 'Creation d'un utilisateur' dans cet article)
9) Coder CreateUser, en prenant en compte les contraintes suivantes :
- Vérification des paramètres en entrée
- Remontée d'un statut cohérent si les paramètre en entrée sont incorrects
- Si les paramètres sont corrects mais que la création échoue, test pour essayer d'en déterminer la cause
- Remontée d'une statut cohérent en fonction de ces tests
Ce qui doit donner ceci :
/// <summary>
/// Créé un utilisateur
/// </summary>
public override MembershipUser CreateUser(string username, string password, string email, string passwordQuestion, string passwordAnswer, bool isApproved, object providerUserKey, out MembershipCreateStatus status)
{
MyMembershipUser retour = null;
status = MembershipCreateStatus.UserRejected;
// check if input is valid
if(!string.IsNullOrEmpty(username)
&& !string.IsNullOrEmpty(password)
&& (password.Length < this.MinRequiredPasswordLength)
&& !string.IsNullOrEmpty(email))
{
// create a user instance
User user = User.CreateNew(username, email, password);
// si réussi
if(user != null)
{
// succeeded
status = MembershipCreateStatus.Success;
// instanciate returning instance
retour = MyMembershipUser.FromUser(user);
}
// en cas d'échec de la création
else
{
// si un utilisateur a déjà cet email
if(User.GetByEmail(email) != null)
// problème de duplication d'email
status = MembershipCreateStatus.DuplicateEmail;
// si un utilisateur a déjà ce login
else if(User.GetByLogin(username) != null)
// problème de duplication d'email
status = MembershipCreateStatus.DuplicateUserName;
}
}
// check the constraints
else
{
// if username not valid
if(string.IsNullOrEmpty(username))
status = MembershipCreateStatus.InvalidUserName;
else if(string.IsNullOrEmpty(email))
status = MembershipCreateStatus.InvalidEmail;
else if((string.IsNullOrEmpty(password))
|| (password.Length < this.MinRequiredPasswordLength))
status = MembershipCreateStatus.InvalidPassword;
}
return retour;
}
10) pour finir, remplacer tous les tests de la classe TestMembership en utilisant notre MyMembershipProvider
Noter que vous recontrez des problèmes au bon déroulement de ces tests.
En effet, il va falloir travailler un peu notre solution de test pour la rendre plus fonctionnelle.
C'est le sujet du prochain article.
Configurer le MembershipProvider dans l'application web
11) Modifier la section membership comme suit
<membership defaultProvider="MyMembershipProvider">
<providers>
<clear />
<add name="MyMembershipProvider" type="MvcBlog.Code.MyMembershipProvider" />
</providers>
</membership>
Il est maintenant possible de naviguez dans votre site, d'enregistrer un nouvel utilisateur et de s'authentifier.
Notre MembershipProvider est fonctionnel.
Télécharger le code des fichier décrit dans cette série de deux articles
Aucun commentaire:
Enregistrer un commentaire