Inhalt
- Einführung
- Bedingungen
- Regex
- HTML - Markup
- Function
- Less
Einführung
In diesem Tutorial wird erklärt, wie eine Funktion zur Passwortrichtlinienprüfung erstellt mit jQuery erstellt wird. Es kommt sehr häufig vor, dass wir User haben, die sich bei uns registrieren sollen, aber deren Passwörter einfache Namen, Geburtsdaten oder Kennzeichen sind. Häufig wird der Benutzername als Passwort genommen oder einfache Folgen auf der Tastaur, wie yaq1XSW2. Sowas gilt es nun einzudämmen. Dieses Tutorial dient nur zur Verdeutlichung und möglichen Umsetzung. Es gibt diverse Dinge, die anders und besser umgesetzt werden können. Alle Regex wurden über Regex101 getestet.
Bedingungen - 1x kleiner Buchstabe - 1x großer Buchstabe - 1x Zahl - 1x Sonderzeichen - Username darf nicht im Passwort vorhanden sein - Keine 3 gleichen Zeichen nacheinander - Keine 4 gleichen Typen nacheinander - Blacklistfilter - Altes Passwort darf nicht im neuen Passwort vorhanden sein - Passwortlänge mindestens 8 Stellen
Es ist auch möglich einzelne Bedingungen abzuschalten, das wird aber später näher behandelt.
Regex
1x kleiner Buchstabe
[a-z]
Erläuterung: Finde einen kleinen Buchstaben von a bis z. Für diese Regel spielt es keine Rolle, wo der kleine Buchstabe aufläuft. Daher wird auf eine Stringbegrenzung, sowie einen Delimiter verzichtet.
1x großer Buchstabe
[A-Z]
Erläuterung: Finde einen großen Buchstaben von A bis Z. Hier gilt das gleiche, wie bei den kleinen Buchstaben.
1x Zahl
[0-9]
// Alternative Schreibweise: [\d]
Erläuterung: Finde eine Zahl von 0 bis 9. Hier gilt das gleiche, wie bei den kleinen Buchstaben. Zum besseren Verständnis, wurde diese Schreibweise gewählt.
1x Sonderzeichen
[^a-zA-Z0-9 ]
Erläuterung: Finde alles was kein kleiner und großer Buchstabe, sowie eine Zahl oder ein Freizeichen ist. Hier gilt das gleiche, wie bei den kleinen Buchstaben.
Keine 3 gleichen Zeichen
^(?:(.)(?!\1\1))*$
Erläuterung: Hier wird es jetzt etwas komplexer, als bei den vorherigen Beispielen.
Wir nutzen eine Stringbegrenzung, die mit dem Zeichen ^ anfängt und $ endet. Es wird dann alles in eine Gruppe zusammengefasst. ?: läutet eine “Non-Capturing Gruppe” ein, was genutzt wird, wenn mehrere Optionen zur Verfügung stehen. Mit der Gruppe (.) wird jedes Zeichen gefunden ( Wichtig für die Non-Capturing Gruppe ). Durch (?!\1\1) wird in der letzten Gruppe ein Negativ Lookahead aus ausgeführt, wo alles gefunden wird, was nicht mindestens 3x nacheinander (z.B.: ddd) auftritt. Ohne ( ?! ( Negativ Lookahead )), wird alles gefunden, was min. 3x auftritt.
Keine 4x gleichen Typen
^(?:(?![a-z]{4,})(?![0-9]{4,})(?![A-Z]{4,})(?![^a-zA-Z0-9_]{4,})(?![_]{4,}).)*$
Erläuterung: Hier ist es ähnlich des Regex mit den 3 gleichen Zeichen. Nur das es hier viel spezieller wird. Die Stringbegrenzung, sowie Non-Capturing Gruppe, sowie dem Negativ Lookahead sollten klar sein. Die erste Klammer nach der Non-Capturing Gruppe sagt folgendes aus. Finde alle kleine Buchstaben, die nur maximal 3x nacheinander vorkommen. Die zweite Klammer, macht dieses mit Zahlen, usw. Hinter der letzten Gruppe, mit dem “_” setzen wir einen Punkt, da nach dem auftreten jedes Zeichen folgen kann. Durch den * wird signalisert, dass unendlich viele Wiederholungen möglich sind.
Username nicht im Passwort
^(?!(?=.*" + username + "))
Delimiter gi
Erläuterung: Hier wird der String mit dem ^angefangen aber nicht geschloßen. Es folgt ein Negativ Lookahead, wo alles was in der Gruppe gefunden wird negiert wird. Die Gruppe sagt aus, finde den Username, es spielt keine Rolle, welche Zeichen davor oder dahinter stehen. Durch die Delimiter g/i wird alles gemachtet und nicht beim ersten abgebrochen, sowie auf Groß- und Kleinschreibung verzichtet.
Strings der Blacklist dürfen nicht vorkommen
^(?!(?=.*password|passw0rd|pass))
Delimiter gi
Erläuterung: Ähnlich der Usernameprüfung, nur dass hier mehrere Wörter gefunden werden können. Getrennt durch eine “Oder-Bedingung |” .
Altes Passwort, darf nicht im neuen Passwort vorkommen.
^(?!(?=.*"altesPasswort"))
Delimiter gi
Erläuterung: Siehe Usernamenprüfung.
Passwortlänge von mindestens 8 Zeichen
.{8,}$
Erläuterung: Wiederhole jedes Zeichen(.) und gebe bei mindestens 8 Zeichen {8,}, den Match zurück.
HTML-Markup
Wir bauen uns ein kleines Formular, wo nur die nötigsten Sachen hinterlegt sind. Es ist ein HTML-Markup von Laravel und Bootstrap.
<form class="form-horizontal"
role="form" method="POST"
action="{{ url('/auth/register') }}">
<input type="hidden" name="_token" value="{{ csrf_token() }}">
<div class="form-group">
<label class="col-md-4 control-label">Username</label>
<div class="col-md-6">
<input type="text" class="form-control"
name="username" id="username"
value="{{ old('username') }}" autofocus>
</div>
</div>
<div class="form-group">
<label class="col-md-4 control-label">Password</label>
<div class="col-md-6">
<input type="password" class="form-control"
name="password_old"
id="password_old" value="l1T!K.3HLl">
</div>
</div>
<div class="form-group">
<label class="col-md-4 control-label">Password</label>
<div class="col-md-6">
<input type="password" class="form-control"
name="password"
id="password">
</div>
</div>
<div class="form-group">
<div class="col-md-6 col-md-offset-4">
<button type="submit" class="btn btn-primary">Register</button>
<div class="result" id="result"></div>
</div>
</div>
</form>
Function
Die komplette Funktion mit den Kommentaren als Beschreibungen.
(function() {
$.fn.strengthChecker = function(userData, useFunctions, boolEnable) {
// strengthChecker aktiv ja oder nein - Wenn kein Wert übergeben wird, dann aktiv
var enable = (typeof boolEnable === 'undefined') ? true : boolEnable;
// Benutzerdefinierte Felder, - Wenn kein Wert übergeben wird, dann default
var data = checkDataParams(userData);
// Einzelne Regeln de/aktivieren, - Wenn kein Wert übergeben wird, dann default
var functions = checkUseFunctions(useFunctions);
// Ergebnisse der Regeln
var result = {} ;
// Checker ist aktiv
if(enable) {
// Durchlaufe alle Regeln
$.each(functions, function (i, val) {
//Setze den Regex und Delimiter
var re = new RegExp(val["regex"], val["delimiter"]);
// Regel ist aktiv
if (val["enable"] === true) {
// Prüfe ob die Regel schon dargestellt wird
if ($('#' + i).length == 0) {
// Lege die Regel und Ihre Beschreibung an
// Standard nicht erfolgreich
$('#result').append(
"<p id='" + i + "'>" + val['description'] + "</p>"
)
.addClass('notSuccess');
}
// Regel ist erfüllt
// Bedingung wird über Css angepasst
if ($(data['passwordField']).val().match(re) != null) {
result[i] = true;
$('#' + i).removeClass('notSuccess').addClass('success');
}
// Regel ist nicht erfüllt
// Bedingung wird über Css angepasst
else
{
result[i] = false;
$('#' + i).removeClass('success').addClass('notSuccess');
}
}
});
// Durchlaufe die Ergebnisse
$.each(result, function (i, val) {
// Mindestens eine Bedingung ist nicht erfüllt
// Bedingungen werden angezeigt
// Button wird deaktiviert
if (val == false) {
$('#result').show();
$("[type=submit]").attr("disabled", "disabled");
return false;
}
// Alle Bedingung sind erfüllt
// Bedingungen werden ausgeblendet
// Button wird aktiviert
else
{
$('#result').hide();
$("[type=submit]").removeAttr("disabled");
}
});
}
// Welche Funktionen werden vom User angewendet
// Setze die Standardfunktionen
// Ändere die Bedingungen auf die vom User übergebenen
function checkUseFunctions(useFunctions)
{
defaultParams = {
lowLetter:{
enable: true,
regex:"[a-z]",
delimiter:"",
description: " Mindestens ein kleiner Buchstabe"},
upLetter:{
enable: true,
regex:"[A-Z]",
delimiter:"",
description: " Mindestens ein großer Buchstabe"},
digits:{
enable: true,
regex:"[0-9]",
delimiter:"",
description: " Mindestens eine Zahl"},
special:{
enable: true,
regex:"[^a-zA-Z0-9 ]",
delimiter:"",
description: " Mindestens ein Sonderzeichen"},
sameSigns:{
enable: true,
regex:"^(?:(.)(?!\\1\\1))*$",
delimiter:"",
description: " Keine 3x wiederholenden Zeichen"},
sameTypes:{
enable: true,
regex:"^(?:(?![a-z]{4,})(?![0-9]{4,})(?![A-Z]{4,})
(?![^a-zA-Z0-9_]{4,})(?![_]{4,}).)*$",
delimiter:"",
description: " Keine 4 gleichen Typen aufeinander"},
sameUser:{
enable: true,
regex:"^(?!(?=.*" + $(data['usernameField']).val() + "))",
delimiter:"gi",
description: " Benutzerame darf nicht im Passwort vorkommen
( Groß- und Kleinschreibung wird nicht unterschieden )"},
blacklist:{
enable: true,
regex:"^(?!(?=.*password|passw0rd|pass))",
delimiter:"gi",
description: " Passwort in der Blacklist vorhanden"},
length:{
enable: true,
regex:".{8,}$",
delimiter:"",
description: "Das Passwort muss mindestens 8 Zeichen lang sein."},
samePass:{
enable: true,
regex:"^(?!(?=.*" + $(data['oldPasswordField']).val() + "))",
delimiter:"gi",
description: " Altes Passwort darf nicht im neuen Passwort vorkommen
( Groß- und Kleinschreibung wird nicht unterschieden )"}
};
// Kein Wert vom User übergeben
if(typeof useFunctions === 'undefined')
{
return defaultParams;
}
else
{
$.each(defaultParams, function (i, val)
{
if(i in useFunctions)
{
defaultParams[i]['enable'] = useFunctions[i];
}
});
return defaultParams;
}
}
// Welche Parameter wurden vom User geändert
// Setze die Standardparameter
// Ändere die Bedingungen auf die vom User übergebenen
function checkDataParams(data)
{
defaultParams = {
usernameField: '#username',
passwordField: '#password',
oldPasswordField: '#password_old'
};
if(typeof data === 'undefined')
{
return defaultParams;
}
else
{
$.each(defaultParams, function (i, val)
{
if(i in data)
{
defaultParams[i] = data[i];
}
});
return defaultParams;
}
}
};
})(jQuery);
Hier kann so ziemlich alles geändert werden, was nur möglich ist. Es ist, wie bereits in der Einleitung geschrieben nicht perfekt und dient nur der Näherbringung von Regex und der Funktionsweise. Die ganzen Standardwerte würde ich via Ajax, nachladen. Die Struktur sollte komplett als Klasse ausgelagert und personalisiert werden. Dadurch entsteht eine höhere Wart- und Lesbarkeit.
Less
Nun zum Abschluss bringen wir noch ein bisschen Farbe ins Spiel.
// Entweder als Package downloaden und integrieren oder via
// https://maxcdn.bootstrapcdn.com/font-awesome/4.5.0/css/font-awesome.min.css
@import "fontawesome/font-awesome";
.result {
display: none;
position:absolute;
top:0;
right: 0;
float: right;
width:300px;
padding:15px;
background:radial-gradient(ellipse, #ededed, darkgray);
font-size:.5em;
border-radius:5px;
.box-shadow(1px 1px 1px);
border:1px solid #ccc;
z-index: 1;
display: none;
&::before {
content: "\25B2";
position:absolute;
top:-12px;
left:45%;
font-size:14px;
line-height:14px;
color:#ddd;
text-shadow:none;
display:block;
}
p {
padding: 2px;
margin: 0;
font-weight: bold;
}
.success {
color: green;
&:before {
font-family: FontAwesome;
content: "\F00C";
}
}
.notSuccess {
color: red;
&:before {
font-family: FontAwesome;
content: "\F00D";
}
}
}
Wenn Unklarheiten oder Verbesserungsvorschläge sind, dann einfach ein kurzes Feedback dalassen.
Viele Grüße
_ Cyrix _