SEO-freundliches, Cookie-unabhängiges Session-Handling in PHP

Der Einsatz von Sessions ist nicht immer unproblematisch, vor allem wenn man auch für strikte Cookie-Verweigerer eine Möglichkeit finden muss. Wie man Suchmaschinen aus der Session außen vor lässt und die URIs dennoch einigermaßen sauber hält, stelle ich hier vor.

Bisher konnte ich mich immer erfolgreich um den Einsatz von Sessions für Webseiten drücken oder ich habe mich darauf verlassen, dass der Besucher Cookies akzeptiert und den Rest PHP überlassen. Für Backends oder andere passwortgeschütze Seiten kann ich guten Gewissens den Benutzer bitten, eine Ausnahme zu machen und ein Cookie zu akzeptieren. Mit dieser Verfahrensweise ist spätestens dann Schluss, wenn man Warenkörbe, mehrseitige Formulare oder vergleichbare Erweiterungen der Funktionalität, unabhängig der Cookie-Einstellungen der Besucher, anbieten möchte. Mich persönlich nervt nichts mehr, als wenn mir eine Website vorschreibt aktiviere dies, installiere jenes um die Inhalte möglichst vollständig zu Gesicht zu bekommen.

Um die Session-ID mit dem folgenden Request wieder parat zu haben, hat man grundsätzlich drei Möglichkeiten zur Auswahl:
Die SID

  • in einem Cookie speichern
  • per Attribut in der URI übergeben
  • als verborgendes Feld in einem Formular per POST-Request (da aber eine Website nicht nur aus Formularen besteht, fällt das als Möglichkeit meist aus)

Am elegantesten ist natürlich Variante 1, denn eine Session-ID in der URI kann eine menge Probleme verursachen. Zum ersten führt es zu Sicherheitslücken, denn so ist die Session-ID offen sichtbar und die Session kann im schlimmsten Fall von einem anderen Nutzer übernommen werden.
Zweites Problem: Suchmaschinen und ihre Spider. Wenn Google & Co URIs mit der Session-ID indizieren, dann ist das formal dublicate content (die selbe Seite wäre ja auch ohne, bzw. mit beliebig vielen Session-IDs erreichbar) und wirkt sich unter Umständen negativ auf das Ranking aus – oder ein Nutzer, der diesen Links folgt, findet vieleicht nicht dass, was er eigentlich sucht, wenn der Inhalt individuell an die Session geknüpft ist.
Und zu guter Letzt sieht es einfach ziemlich blöd aus und es zerstört einem die mühsam zusammengebaute URI-Struktur: http://blog.dnaber.de/2009/artikel?PHPSESSID=a3c4efg6ijk0m8o3ue.
Man sollte also diese Möglichkeit der Session-ID-Weitergabe nur dann nutzen, wenn der Benutzer keine Cookies akzeptiert.

Wie also kann man vorgehen?

Die Grundeinstellungen

Wir wollen, wenn möglich, Cookies verwenden, wir wollen allerdings nicht ausschließlich auf Cookies setzen. Also nutzen wir die Funktion ini_set() um die Laufzeitkonfiguration der Sessionbehandlung in PHP anzupassen:

# Cookieunterstützung aktivieren
ini_set( 'session.use_cookies', 1 );

# Sessionunterstützung ausschließlich mit Cookies deaktivieren
ini_set( 'session.use_only_cookies', 0 );

Je nach Serverkonfiguration kann das auch schon dem Standard entsprechen.

Die Fall-forward-Lösung für Cookie-Freunde

Ein Problem bei Cookies ist, dass man frühestens beim zweiten Request erkennt, ob der Client das Cookie akzeptiert und damit im Request wieder mit gesendet hat. Es bleibt also nichts anderes übrig, als für den ersten Request die Session-ID an die URIs zu hängen.

if ( empty( $_COOKIE[ session_name() ] ) ) 
	ini_set( 'session.use_trans_sid', 1 );

else     
	ini_set( 'session.use_trans_sid', 0 );

Wir aktivieren also für den ersten Request die automatische Weitergabe der Session-ID über die URIs. Beim Start der Session wird dennoch das Cookie an den Client gesendet und – falls akzeptiert – beim nächsten Request mit gesendet und wir können ohne die transparente SessionID Unterstützung weiter machen.
Das ist nicht gerade schön, aber die einzige Möglichkeit die ich sehe, unabhängig von Cookie-Einstellungen des Client mit Sessions zu arbeiten. Wenn jemand eine elegantere Möglichkeit kennt, nur her damit.

Die Extrawurst für Suchmaschinen

In den Webmaster-Guidelines von Google lesen wir:

If features such as JavaScript, cookies, session IDs, frames, DHTML, or Macromedia Flash keep you from seeing your entire site in a text browser, then spiders may have trouble crawling it.

Okay, dann starten wir die Session gar nicht, wenn wir einen Besucher als Robot identifizieren. Damit sparen wir Ressourcen und gleichzeitig verhindern wir, dass unschöne URIs über diesen Weg im Index auftauchen. Dazu verwende ich folgende Funktion, der ich den User-Agent-String zur Überprüfung übergebe:

/** 
 * Findet einen Crawler anhand des 
 * User-Agent Headers
 *
 * @param str $ua (User-Agent) 
 * @return bool
 */
function isRobot( $ua = '' ) {

	if ( empty( $ua ) ) 
        	return FALSE;

	$robot_list = array(
		'googlebot',
		'Google-Sitemaps',
		'feedfetcher',
		'Slurp',
		'YahooSeeker',
		'msnbot',
		'archive.org',
		'teoma',
		'ia_archiver',
		'baiduspider',
		'WebDataCentreBot',
		'gonzo',
		'DotBot',
		'orgbybot',
		'web-sniffer'
	);
	foreach ( $robot_list as $needle ) {
		if( FALSE !== stripos( $ua, $needle ) )
			return TRUE;
        }

	return FALSE;
}

Die Liste der User-Agents für Robots habe ich hauptsächlich meinen Zugriffsstatistiken entnommen. Sie ist natürlich weit davon entfernt vollständig zu sein, aber damit sollte man die wichtigsten Spider auf dem Markt erfasst haben. (Um die Laufzeiten des Scripts möglichst kurz zu halten, bietet es sich an, die Spiderbezeichnungen nach absteigender Besuchshäufigkeit in das Array aufzunehmen.)

Und falls doch mal eine unsaubere URI im Netz auftauch?

Es ist natürlich nicht auszuschließen, dass ein Link bzw. die URI eine Session-ID enthält, vieleicht hat ein Besucher einfach die Adresszeile seines Browsers kopiert und verlinkt. Diesen Link werden die Sumas natürlich finden. Um dennoch diese URIs nicht zu indizieren hilft uns der Meta-Tag robots weiter:

# im HTML-head
if ( FALSE !== stripos( $_SERVER[ 'QUERY_STRING' ], session_name() . '=' ) ) {

	echo '<meta name="robots" content="noindex,follow" />';
	# oder kanonische Adresse angeben (siehe Nachtrag)
}

Damit verhindern wir, dass die zusätzliche URI in den Index gelangt. Eine sauberere, aber auch aufwändigere Möglichkeit ist folgende Funktion, die die Session-ID aus der angefragten URI entfernt und die bereinigte URI zurückgibt. An diese können wir dann den Besucher weiterleiten.

/**
 * entfernt die Session-ID aus dem Request-URI
 *
 * @return str
 */
function getCleanURL() {

	if ( FALSE !== strpos( $_SERVER[ 'QUERY_STRING' ], session_name() ) ) {

		# String der Form PHPSESSID=abcde12345 entfernen
		$clean_url = str_replace(
			session_name() . '=' . $_GET[session_name()],
			'',
			$_SERVER['REQUEST_URI']
		);
       
		# & und ? entfernen, falls die am Ende des Strings stehen
		$clean_url = trim( $clean_url, '&?' );

		# sollten wir die Session ID zwischen zwei anderen Get-Parametern, oder 
		# am anfang des Query-Strings herausgelöst haben, müssen wir noch aufräumen:
		$clean_url = str_replace(
			array( '?&', '&&' ), 
			array( '?', '&' ), 
			$clean_url 
		);

		return $clean_url;
	}
	else {
		return $_SERVER['REQUEST_URI'];
	}
}

An diese URI dürfen wir den Besucher nur dann weiterleiten, wenn wir sicher sind, dass er Besucher von einer externen Seite kommt. Andernfalls wäre der ganze Aufwand umsonst gewesen – wir würden unsere eigenen Session-IDs abschneiden. Außerdem brauchen wir die Funktion nicht zu bemühen, wenn gar keine Variablen in der URI mitgegeben wurden, d.h. der Query-String leer ist.

# Ein Referer von der eigene Seite würde so aussehen
# http://meine-seite.tld… 
# also:
$my_referer = ( empty( $_SERVER['HTTPS'] ) ? 'http' : 'https' ) . '://' . $_SERVER[ 'HTTP_HOST' ];

if ( FALSE !== strpos( $_SERVER[ 'QUERY_STRING' ], session_name() )
    && 
    ! empty( $_SERVER[ 'HTTP_REFERER' ] ) 
    &&
    FALSE === strpos( $_SERVER[ 'HTTP_REFERER' ], $my_referer )
) {
	# ab zur Adresse ohne Session-ID
	header( 
		'Location: ' 
			. ( empty( $_SERVER[ 'HTTPS' ] ) ? 'http' : 'https' )
			. '://' . $_SERVER[ 'HTTP_HOST' ] 
			. getCleanURL(), 
		TRUE, 
		301
	);
}

Anmerkung

Die hier gezeigte Lösung ist aus der Not heraus geboren, da man nur wenig über das Wie des Session-Handlings im Internet findet – vor allem, wenn man sich die o.g. Ziele steckt, dass die Session unabhängig von Cookies verwendet werden kann und man das SEO-Problem und die Nutzerfreundlichkeit nicht ganz außer Acht lassen will.

Konkret werde ich diese Methode für ein Bestellformular auf einer Seite einsetzen, in dem keine sensiblen Daten in der Session gespeichert werden und die Session nicht obligatorisch für die Funktion der Seite ist. Nach einiger Zeit werde ich hier meine Erfahrungen mit dieser Methode veröffentlichen.

Nachtrag vom 04.08.
Gestern abend laß ich im Blog von Matt Cutts von dem cannonical Link-Element. Damit zeigt man den Suchmaschinen die kanonische Adresse an und vermeidet somit doppelte Seiten. Um diesen Tag sinnvoll einzusetzen, habe ich die Funktion sendToCleanURL() durch die Funktion getCleanURL() ersetzt, weil ich sie so flexiebler einsetzen kann:

if ( FALSE !== stripos( $_SERVER[ 'QUERY_STRING' ], session_name() . '=' ) ) {
     $cannonical_url = 
		  ( $_SERVER['HTTPS'] ? 'http' : 'https')
		. '://'.$_SERVER['HTTP_HOST']
		. getCleanURL();
     echo '<meta name="cannonical" content="' . $cannonical_url . '" />';  
}

Sollten wir also eine unsaubere URI im Umlauf haben, so ist damit sichergestellt, dass die Suchmaschinen keine Probleme damit haben.
Weitere Infos: specify your cannonical

Kommentare

  1. 01) 18.08.2009
    Nico Schubert

    Dies ist ein sehr schönes Tutorial. Dies kann ich bestimmt bei meinen zukünftigen Projekten vielleicht verwenden. Dafür bekommt dieser Artikel einen Platz in meinem Favoriten.

  2. 02) 08.10.2009
    Webshopguy

    Habe was ganz Ähnliches gemacht.

    Nur hänge ich anfangs nicht die Session mit ran, sondern warte, bis der User Sessions wirklich benötigt. Also der Shop ist durchsuchbar und erst wenn der Warenkorb oder andere Sessionwürdige Funktionen benutzt werden, erzwinge ich eine Session.

  3. 03) 11.10.2009
    David

    Ja, das ist natürlich immer sinnvoll, unabhängig davon ob man nun ausschließlich auf Cookies setzt oder nicht.

  4. 04) 17.09.2012
    A. L.

    Hallo,

    in der Funktion isRobot wäre es besser, die Zeile
    if( stripos( $ua, $needle ) )
    in
    if( stripos( $ua, $needle )!==false )
    zu ändern, denn sonst wird false zurückgegeben, wenn die Zeichenkette an Position 0 gefunden wurde.

    LG

  5. 05) 18.09.2012
    David

    Völlig richtig. Vielen Dank.

  6. 06) 12.02.2014
    Frank

    Danke für deine Hilfe. Habe einige Zeit gesucht, wie ich zwischen Cookie und GET umschalten kann. Dein Beitrag ist die Lösung.
    Leider scheint es so, als gibt es wirklich keine elegante Lösung, als mit dem "ersten Klick" die Sessio-ID auch per POST/Get zu übergeben. Schade.

Fragen, Ideen oder Kritik? – Hier ist Platz dafür!

Dein Kommentar

Um ein Kommentar abzugeben, reicht der Text im Kommentarfeld. Die Angabe eines Namens wäre nett, ist aber nicht erforderlich.

Du darfst folgenden HTML-Code verwenden, musst aber nicht:
<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>