Hiho,
es ist ja recht still in der Coding Tutorials-Ecke geworden. Ich wollte schon länger mal ein paar Tutorials schreiben, hatte aber irgentwie nie die Motivation dazu. Mit meiner Teilnahme am Coding-Contest #9 hab ich dann quasi den entscheidenen Schritt getan und ein Basis-Tutorial für eine kleine Tutorial-Reihe geschrieben.
Es folgt, in leicht verbesserter Form, meine Abgabe zum Coding-Contest #9
Kritik ist erwünscht.
[tabmenu]
[tab='Changelog']
16.06.11
- C# Code durch Verwendung von WebClient vereinfacht
- Design etwas angepasst
- Geplante Themen hinzugefügt
[tab='Downloads']
(Noch) keine Downloads verfügbar
[tab='Erweiterungen']
- Umstellung auf XML (in Arbeit)
- Bruteforce Schutz (in Arbeit)
- Adminpanel (geplant)
- Zeitbeschränkungen (geplant)
- Nutzungsbeschränkungen , IP und HardwareID (geplant)
- Sicherheitsaspekte (geplant, Themenvorschläge willkommen)
- Verwenden einer simplen Verschlüsselung (geplant)
- Verwenden einer symmetrischen Verschlüsselung (geplant)
Ich werde versuchen die Themen in dieser Reihenfolge abzuarbeiten.
Weitere Vorschläge sind natürlich willkommen.
[/tabmenu]
1. Vorwort
Um dieses Tutorial zu verstehen benötigt ihr etwas Grundwissen. Ihr solltet mit einem Webspace umgehen können, dass heißt per FTP Dateien hochladen und im Browser aufrufen können und in phpMyAdmin Tabellen erstellen und Datensätze hinzufügen können. Grundlegende PHP-Kenntnisse sind ebenfalls von Vorteil.
Ein Loginsystem wird meist eingesetzt um Zugriffe zu kontrollieren und/oder zu überwachen.
Zu einem Loginsystem gehören zwei Komponenten: Ein Client, der sich einloggen möchte, und ein Server, der den Login erlaubt oder verweigert.
In der Regel programmiert man seinen eigenen Server und lässt den Login über eine TCP Verbindung ablaufen. Das setzt voraus, dass man einen Server im Internet hat, der rund um die Uhr verfügbar ist. Hier kommt man als kleiner Programmierer, der eigentlich nur sein Programm mit einem Login versehen wollte, an seine Grenzen. Entweder man lässt seinen eigenen PC rund um die Uhr laufen oder man mietet sich einen Server der meist gar nicht so billig ist.
Zum Glück gibt es ja kostenlosen Webspace zu genüge im Internet. Und wie ihr einen solchen Webspace für ein Loginsystem verwendet erfahrt ihr in diesem Tutorial.
Bevor ihr weiter macht solltet ihr zunächst einen geeigneten Hoster finden.
Ihr solltet ein paar MB Speicherplatz haben, PHP Unterstützung und eine MySQL Datenbank.
Für den Server werde ich PHP und SQL verwenden. Für den Clienten liefere ich Beispiele in C# und AutoIt. Ursprünglich wollte ich noch Beispiele zu VB.NET machen, aber die wären Identisch mit den C# Beispielen gewesen. Man kann die Beispiele eigentlich Problemlos nach VB.NET umschreiben.
2. Theorie
Der Client (in diesem Fall unser Programm) sendet seine Benutzerdaten an einen Server. Dieser antwortet mit einem Ok oder Nicht Ok.
Wichtig hierbei ist, dass die Überprüfung der Daten im Server statt findet und nicht im Client. Würde man den Client die Passwörter vergleichen lassen, so müsste man dem Client das richtige Passwort mitteilen. So kann man als User zu jedem Benutzer nur mit dem Benutzernamen das Passwort herausfinden.
Fazit: Benutzerdatenüberprüfung im Client ist schlecht.
Server und Client kommunizieren per HTTP-Protokol. Wir werden mit der HTTP-Request-Methode GET die Benutzerdaten an den Server übermitteln.
Dies ist mit jeder mir bekannten Programmiersprache möglich. Einige bieten sogar vorgefertigte Methoden dafür.
3. Realisierung
Wir beginnen mit dem PHP-Script für den Server.
Das Array $_GET enthält alle per GET übergeben Parameter. Wir werden zunächst ohne Datenbank arbeiten, da das jetzt noch zu kompliziert ist.
- <?php
- if($_GET['username'] == "ich")
- {// Wenn der Username ich ist überprüfe das Passwort
- if($_GET['passwort'] == "meinpasswort")
- {// Wenn das Passwort meinpasswort lautet
- // geben wir Richtig aus
- echo "Richtig";
- }
- else
- {// Ansonsten falsch
- echo "Falsch";
- }
- }
- else
- {// Und wenn der Username nicht ich ist ebenfalls falsch
- echo "Falsch";
- }
- ?>
Ob das Ganze auch funktioniert können wir sogar ohne Client testen. Einfach die URL im Webbrowser aufrufen. Auf Groß- und Kleinschreibung ist zu achten.
http://deineadresse.de/login.p…ich&passwort=meinpasswort
Wenn man alles richtig gemacht hat, sollte jetzt „Richtig“ ausgegeben werden. Damit wir auch sicher sein können, dass alles geht geben wir einfach einen falschen Benutzernamen oder ein falsches Passwort an.
http://deineadresse.de/login.p…zer&passwort=meinpasswort
oder
http://deineadresse.de/login.p…wort=meinfalschespasswort
Ich hoffe die Verwendung von GET-Parametern ist nun klar.
Bei vielen Freehostern wird noch ein Werbelayer eingeblendet. Dieser wird automatisch unserer Ausgabeseite angefügt. Im Moment stört er noch nicht, aber sobald wir mit einem Clientprogramm arbeiten müssen wir die Ausgabe filtern. Dazu später mehr.
3.1 Datenbankunterstützung
Das Script kann man nun durch hinzufügen vieler If’s erweitern oder durch ein switch ersetzen. Beides ist viel Schreibaufwand und sehr umständlich zu administrieren.
Daher werden wir nun die MySQL Datenbank hinzunehmen. Zunächst erstellen wir eine Tabelle. Dazu wählt ihr in phpMyAdmin die passende Datenbank aus und seht daraufhin folgendes:
Idealerweise nennen wir diese Tabelle users. Die braucht drei Spalten.
Für Benutzername und Passwort habe ich jeweils maximal 25 Zeichen zugelassen, das sollte mehr als ausreichend sein. Die ID hat einen Maximalwert von 4.294.967.295. Dies ist die maximale Anzahl von Usern die in der Tabelle eingetragen werden können. Die Spalte ID ist außer dem der Primärschlüssel / PrimaryKey.
Wer möchte kann auch einfach den Query in seiner Datenbank ausführen.
SQL-Query der Tabelle:
- CREATE TABLE `users` (
- `id` INT( 10 ) UNSIGNED NOT NULL AUTO_INCREMENT ,
- `username` VARCHAR( 25 ) CHARACTER SET latin1 COLLATE latin1_general_cs NOT NULL ,
- `passwort` VARCHAR( 25 ) CHARACTER SET latin1 COLLATE latin1_general_cs NOT NULL ,
- PRIMARY KEY ( `id` )
- ) ENGINE = MYISAM CHARACTER SET latin1 COLLATE latin1_general_c;
Zum Testen fügt ihr einfach mit phpMyAdmin zwei bis drei User in die Tabelle ein.
Nun müssen wir das PHP-Script erweitern:
1.Verbindung zur Datenbank aufbauen und einloggen
2.Datenbank auswählen
3.SELECT-Query zum Server senden
4.Ergebnis auswerten
5.Verbindung zur Datenbank abbauen
- <?php
- // Verbindung zum Datenbankserver herstellen (Benutzerdaten anpassen)
- $verbindung = mysql_connect('localhost', 'contest', 'abc123#');
- if(!$verbindung)
- die("Keine Verbindung zur Datenbank " . mysql_error());
- // Datenbank auswählen (Datenbankname anpassen)
- $datenbank = mysql_select_db("contest", $verbindung);
- if(!$datenbank)
- die("Auswahl der Datenbank fehlgeschlagen " . mysql_error());
- // Variablen gegen SQL-Injections sichern
- // magic_quotes ist eine Hilfe Funktion die
- // SQL-Injections verhindert. Sie "escape't" kritische Zeichen wie / \ " ' < > usw.
- // magic_quotes ist auf vielen Webservern aktiv. Wenn wir einfach
- // mysql_real_escape_string drüber laufen lassen, werden alle schon escape'ten Zeichen
- // erneut escape't.
- // Daher testen wir erst ob magic_quotes überhaupt aktiv ist
- if (get_magic_quotes_gpc()) {
- $cleanuser = mysql_real_escape_string(stripslashes($_GET['username']));
- } else {
- $cleanuser = mysql_real_escape_string($_GET['username']);
- }
- // Query an den Server senden
- $query = "SELECT *
- FROM users
- WHERE Username = '" . $cleanuser . "'
- LIMIT 1";
- // Wir lassen die erste Zeile ausgeben, in der
- // der Username gleich dem übergeben Username ist
- $result = mysql_query($query);
- if (!mysql_num_rows($result)) {
- // Keine Einträge (Benutzer exsistiert nicht)
- echo "Falsch";
- } else {
- // Benutzer existiert
- // $result in $row "fetchen"
- $row = mysql_fetch_assoc($result);
- // $row enthält nun die ausgelesene Zeile
- // Normalerweise verwendet man hier eine while-schleife
- // und fetcht jede Zeile und bearbeitet diese,
- // Aber da wir nur eine mögliche Zeile haben sparen wir uns das
- // Passwort prüfen
- if($row['passwort'] == $_GET['passwort'])
- { // Passwort richtig
- echo "Richtig";
- } else { // Passwort falsch
- echo "Falsch";
- }
- }
- mysql_close($verbindung);
- ?>
Ob alles funktioniert, kann man wieder über die URL testen. Hierbei verwenden wir nicht die Daten vom ersten Script sondern die Daten, die ihr in der Tabelle eingetragen habt.
Damit haben wir eine funktionierende Grundversion des Loginscripts mit MySQL-Datenbank-Unterstützung.
3.2 Client
Wie schon gesagt, bieten viele Programmiersprachen bereits vorgefertigte Methoden um Daten aus dem Internet abzurufen. Für NetFramework-basierende Sprachen ist dies die WebRequest oder WebClient-Klasse. In AutoIt v3 kann man die Funktionen InetRead oder InetGet verwenden.
Alternativ kann man auch über die Funktion URLDownloadToFile (urlmon.dll) die Datei auf dem Datenträger speichern und danach auslesen.
In allen Beispielen müsst ihr natürlich die URL an euere Eigene anpassen.
Wie in 1.2 schon angekündigt, fügen viele Hoster Werbelayer hinzu. Dies ist der Fall wenn die Ausgabe des Logins nicht explizit „Richtig“ oder „Falsch“ ist sondern noch Tags wie <script> dabei stehen.
Beispiel:
- Richtig<script type="text/javascript">
- var _gaq = _gaq || [];
- _gaq.push(['_setAccount', 'UA-16106315-6']);
- _gaq.push(['_setDomainName', '.ohost.de']);
- _gaq.push(['_trackPageview']);
- (function() {
- var ga = document.createElement('script'); ga.type = 'text/javascript';
- ga.async = true;
- ga.src = ('https:' == document.location.protocol ? 'https://ssl' :
- 'http://www') + '.google-analytics.com/ga.js';
- var s = document.getElementsByTagName('script')[0];
- s.parentNode.insertBefore(ga, s);
- })();
- </script>
Auch wenn dies nicht der Fall ist, rate ich euch die nun folgenden Änderungen trotzdem durchzuführen:
Wir fügen zwei „Split-Tags“ in unsere Ausgabe ein, sodass die Ausgabe ungefähr so aussieht.
- <splittag>Richtig<splittag><script type="text/javascript">
- var _gaq = _gaq || [];
- _gaq.push(['_setAccount', 'UA-16106315-6']);
- _gaq.push(['_setDomainName', '.ohost.de']);
- _gaq.push(['_trackPageview']);
- (function() {
- var ga = document.createElement('script'); ga.type = 'text/javascript';
- ga.async = true;
- ga.src = ('https:' == document.location.protocol ? 'https://ssl' :
- 'http://www') + '.google-analytics.com/ga.js';
- var s = document.getElementsByTagName('script')[0];
- s.parentNode.insertBefore(ga, s);
- })();
- </script>
Man könnte an dieser Stelle auch mit XML arbeiten, aber das finde ich für unsere Zwecke etwas überdimensioniert.
Als Splittag sollte eine Zeichenfolge zum Einsatz kommen, die nicht zufällig in der Ausgabe vorkommen kann (sowohl von eurer Seite, als auch durch den Hoster). Denn dann wäre in dieser Version des Logins das Array vollkommen durcheinander. In diesem Fall, „<splittag>“, ist es relativ unwarscheinlich, dass der Hoster diese Zeichenfolge verwendet.
Mit einer Split-Funktion splitten wir die Ausgabe in ein Array. Das Array würde dann folgendermaßen aussehen:
Achtung in AutoIt ist das Array um ein Feld verschoben da in Feld 0 die Array-Größe steht.
Daher müssen wir das PHP-Script noch einmal verändern. Aber nur diesen Teil:
- if (!mysql_num_rows($result)) {
- // Keine Einträge (Benutzer exsistiert nicht)
- echo "<splittag>Falsch<splittag>";
- } else {
- // Benutzer existiert
- // $result in $row "fetchen"
- $row = mysql_fetch_assoc($result);
- // Passwort prüfen
- if($row['passwort'] == $_GET['passwort'])
- { // Passwort richtig
- echo "<splittag>Richtig<splittag>";
- } else { // Passwort falsch
- echo "<splittag>Falsch<splittag>";
- }
Jetzt sind alle Ausgaben mit den Splittags versehen und wir können mit dem Clienten beginnen.
3.2.1 AutoIt
Das Script ist sehr kurz.
Bitte lasst euch nicht irrtieren das da PHP-Code steht. Ich verwende die php-tags wegen dem Syntax-Highlighting
- #include <GUIConstantsEx.au3>
- Local $user, $pass, $msg
- GuiCreate("Client v1", 320, 240)
- $user = GUICtrlCreateInput("", 10,5, 90, 20)
- $pass = GUICtrlCreateInput("", 10, 30, 90, 20)
- $login = GUICtrlCreateButton("Login", 10, 55, 90)
- GUISetState(@SW_SHOW)
- $msg = 0
- While 1
- $msg = GUiGetMsg()
- Select
- Case $msg = $GUI_EVENT_CLOSE
- ExitLoop
- Case $msg = $login
- $result = DoLogin(GuiCtrlRead($user), GuiCtrlRead($pass))
- Msgbox(0,"",$result)
- EndSelect
- WEnd
- Func DoLogin($username, $password)
- $sData = InetRead("http://localhost/cpg/login.php?username=" & $username & "&passwort=" & $password)
- Return BinaryToString($sData)
- EndFunc
In der Funktion DoLogin wird per InetRead unsere Anfrage mit den GET-Parametern an den Server geschickt. Die Antwort des Servers ist der Rückgabewert der Funktion. Zunächst lassen wir den Rückgabewert der Funktion komplett ausgeben.
Die Ausgabe sollte ähnlich sein.
Wie man sieht, wird unsere Ausgabe von den Splittags umgeben.
Also Splitten wir das Ergebnis.
Auszug aus der AutoIt Hilfe
- StringSplit ( "string", "delimiters" [, flag ] )
- Parameters
- string The string to evaluate. delimiters One or more characters to use as delimiters (case sensitive). flag [optional] Changes how the string split works, add mutliple flag values together if required:
- flag = 0 (default), each character in the delimiter string will mark where to split the string
- flag = 1, entire delimiter string is needed to mark the split
- flag = 2, disable the return the count in the first element - effectively makes the array 0-based (must use UBound() to get the size in this case).
In einem AutoIt Array ist das erste ArrayFeld (0) die Array-Größe. Somit beginnen die Daten erst bei Feld 1. Laut dem Beispiel oben wäre unsere Ausgabe dann in Feld 2.
Das Flag setzen wir auf 1. Dann splittet die Funktion mit dem kompletten Delimiter und nicht mit jedem einzelnen Zeichen.
Den Rückgabewert der Funktion könnt ihr nun mit einem simplen If … Else verarbeiten.
Unser entgültiger Code sieht nun so aus.
- #include <GUIConstantsEx.au3>
- Local $user, $pass, $msg
- GuiCreate("Client v1", 320, 240)
- $user = GUICtrlCreateInput("", 10,5, 90, 20)
- $pass = GUICtrlCreateInput("", 10, 30, 90, 20)
- $login = GUICtrlCreateButton("Login", 10, 55, 90)
- GUISetState(@SW_SHOW)
- $msg = 0
- While 1
- $msg = GUiGetMsg()
- Select
- Case $msg = $GUI_EVENT_CLOSE
- ExitLoop
- Case $msg = $login
- $result = DoLogin(GuiCtrlRead($user), GuiCtrlRead($pass))
- If($result == "Richtig") Then
- Msgbox(0,"","Login Ok")
- Else
- MsgBox(0,"","Login falsch")
- EndIf
- EndSelect
- WEnd
- Func DoLogin($username, $password)
- $sData = InetRead("http://localhost/cpg/login.php?username=" & $username & "&passwort=" & $password)
- $data = BinaryToString($sData)
- $output = StringSplit($data, "<splittag>", 1)
- return $output[2]
- EndFunc
3.2.2 C# (.NET)
In .NET-basierenden Sprachen verwendet man WebRequest bzw. HTTPWebRequest oder auch WebClient.
Als erstes brauchen wir ein GUI.
Bei .NET ist das ganze ähnlich einfach wie bei AutoIt. Wir erstellen ein Web-Client Objekt (mit dem WebRequest geht es auch, aber dieses ist wesentlich komplexer) und lassen die Ausgabe als String zurückgeben.
Zunächst die benötigten Verweise:
Um einen groben Überblick zu bekommen lassen wir zunächst die komplette Antwort des Servers ausgeben.
Aus Platzgründen schreibe ich nicht den kompletten Code hin. Ich gehe davon aus, dass ihr einen Button mit einer Funktion belegen könnt.
Die Ausgabe kann mit einer MessageBox erfolgen.
So siehts ungefähr aus. Nun müssen wir die Nachricht von den Splittags trennen. Das String-Objekt hat eine integrierte Split-Methode die wir verwenden werden.
Wie weiter oben bereits gesagt, wird das Array-Feld 1 die zurückgegeben Daten enthalten. Diese werden wir von der Funktion zurück geben und nun mit einem If … Else behandeln.
Die komplette Funktion sieht nun so aus:
- private string DoLogin(string username, string passwort)
- {
- WebClient client = new WebClient();
- // Ziel URL mit GET-Parametern generieren
- string targetURL = "http://localhost/cpg/login.php?username=" +
- username + "&passwort=" + passwort;
- string retn = client.DownloadString(targetURL);
- // Response splitten
- string[] output = retn.Split(new string[]{ "<splittag>" },
- StringSplitOptions.None);
- return output[1];
- }
4.Abschluss
Wir haben nun eine funktionierende Grundlage eines Loginsystems. Es ist auf nahezu jedem Webspace einsetzbar. Wir können eine Datenbank verwenden in der wir realtiv einfach User hinzufügen und entfernen können.
Als Erweiterung könnte man zum Beispiel weitere Rückmeldungen hinfügen (Passwort falsch, User existiert nicht, Account gesperrt) oder eine Nutzungszeit einfügen oder eine Limitierung der Nutzung auf eine Anzahl von Computern über eine Hardware ID.
Auch einer besseres Einloggverfahren um Crackern das Leben schwer zu machen wäre sicherlich nicht falsch.
Am wichtigsten jedoch ist ein Bruteforce-Schutz, der z.B. die IP-Adresse nach mehreren falschen Versuchen bannt.
Gruß
florian0