Ab sofort besteht wieder die Möglichkeit, sich per Facebook auf dieser Site anzumelden.
Angesichts der Änderungen der API von Facebook funktionierte das Login per Facebook nicht mehr, weshalb ich es zunächst beim Update entfernt hatte. Mit ein paar kleineren Änderungen in der Django-Middleware und im Frontend-Code konnte das Facebook-Login wieder aktiviert werden.
Grundsätzlich funktioniert die Einbindung sehr ähnlich wie auf Facebook Connect Django Middleware mit Graph API beschrieben.
Die erste Änderung betraf die Verwendung des Facebook Python SDK. Zwar stellt Facebook dieses nicht mehr offiziell bereit, aber auf der Community-Ressouce Python for Facebook finden sich einige Quellen und Hinweise. Zunächst habe ich die Rest des alten SDK entfernt und durch die aktuelle Version ersetzt.
Des Weiteren wurde die Django Middleware ein wenig modifiziert und an die neue SDK-Version angepasst:
# Facebook Connect Middleware with new Facebook Python SDK from https://github.com/pythonforfacebook/facebook-sdk and Graph API | |
# | |
# based on http://www.djangosnippets.org/snippets/1252/ | |
# | |
from django.contrib.auth import authenticate, login, logout, get_user | |
from django.contrib.auth.models import User | |
from django.conf import settings | |
from kuerbisorg.comcore.models import ComUser, COUNTRY_CHOICES, ACTIVE_CHOICES | |
import hashlib | |
import facebook | |
PROBLEM_ERROR = 'There was a problem. Try again later.' | |
ACCOUNT_DISABLED_ERROR = 'Your account is not active.' | |
ACCOUNT_PROBLEM_ERROR = 'There is a problem with your account.' | |
class FacebookConnectMiddleware(object): | |
delete_fb_cookies = False | |
facebook_user_is_authenticated = False | |
def process_request(self,request): | |
try: | |
# Set the facebook message to empty. This message can be used to display info from the middleware on a Web page. | |
request.facebook_message = None | |
# Don't bother trying FB Connect login if the user is already logged in | |
if not request.user.is_authenticated(): | |
# FB Connect will set a cookie with a key == 'fbm_' + Application ID if the user has been authenticated | |
if "fbm_" + settings.FB_APP_ID in request.COOKIES: | |
current_user = facebook.get_user_from_cookie(request.COOKIES, settings.FB_APP_ID, settings.FB_API_SECRET) | |
if current_user: | |
graph = facebook.GraphAPI(current_user["access_token"]) | |
profile = graph.get_object("me") | |
try: | |
# Try to get Django account corresponding to friend | |
# Authenticate then login (or display disabled error message) | |
django_user = User.objects.get(username="fb_" + profile['id']) | |
user = authenticate(username="fb_" + profile['id'], | |
password=hashlib.md5("fb_" + profile['id'] + settings.SECRET_KEY).hexdigest()) | |
if user is not None: | |
if user.is_active: | |
login(request, user) | |
self.facebook_user_is_authenticated = True | |
else: | |
request.facebook_message = ACCOUNT_DISABLED_ERROR | |
self.delete_fb_cookies = True | |
else: | |
request.facebook_message = ACCOUNT_PROBLEM_ERROR | |
self.delete_fb_cookies = True | |
except User.DoesNotExist: | |
# There is no Django account for this Facebook user. | |
# Create one, then log the user in. | |
# Create user | |
user = User.objects.create_user("fb_" + profile['id'], '', | |
hashlib.md5("fb_" + profile['id'] + | |
settings.SECRET_KEY).hexdigest()) | |
user.first_name = profile['first_name'] | |
user.last_name = profile['last_name'] | |
user.save() | |
comUser = ComUser(user=user, | |
active='a', | |
source='FB', | |
country='__' # todo: get from locale | |
) | |
regToken = comUser.createToken() | |
comUser.save() | |
# Authenticate and log in (or display disabled error message) | |
user = authenticate(username="fb_" + profile['id'], | |
password=hashlib.md5("fb_" + profile['id'] + settings.SECRET_KEY).hexdigest()) | |
if user is not None: | |
if user.is_active: | |
login(request, user) | |
self.facebook_user_is_authenticated = True | |
else: | |
request.facebook_message = ACCOUNT_DISABLED_ERROR | |
self.delete_fb_cookies = True | |
else: | |
request.facebook_message = ACCOUNT_PROBLEM_ERROR | |
self.delete_fb_cookies = True | |
else: | |
logout(request) | |
self.delete_fb_cookies = True | |
else: | |
get_user(request) | |
self.delete_fb_cookies = True | |
# Logged in | |
else: | |
comUser = ComUser.objects.get(user=request.user) | |
if comUser.source == 'FB': # check successful facebook login. If user is logged per site authentication only, just pass. | |
# FB Connect user? | |
if "fbsr_" + settings.FB_APP_ID in request.COOKIES: | |
# IP hash cookie set | |
if 'fb_ip' in request.COOKIES: | |
real_ip = self.get_real_ip(request) | |
# If IP hash cookie is NOT correct... otherwise: pass | |
if request.COOKIES['fb_ip'] != hashlib.md5(real_ip + settings.FB_API_SECRET + settings.SECRET_KEY).hexdigest(): | |
logout(request) | |
self.delete_fb_cookies = True | |
# FB Connect user without hash cookie set | |
else: | |
logout(request) | |
self.delete_fb_cookies = True | |
else: | |
logout(request) | |
self.delete_fb_cookies = True | |
# Something else happened. Make sure user doesn't have site access until problem is fixed. | |
except: | |
request.facebook_message = PROBLEM_ERROR | |
logout(request) | |
self.delete_fb_cookies = True | |
def process_response(self, request, response): | |
# Delete FB Connect cookies | |
# FB Connect JavaScript may add them back, but this will ensure they're deleted if they should be | |
if self.delete_fb_cookies is True: | |
response.delete_cookie("fbm_" + settings.FB_APP_ID) | |
self.delete_fb_cookies = False | |
if self.facebook_user_is_authenticated is True: | |
real_ip = self.get_real_ip(request) | |
response.set_cookie('fb_ip', hashlib.md5(real_ip + settings.FB_API_SECRET + settings.SECRET_KEY).hexdigest()) | |
# process_response() must always return a HttpResponse | |
return response | |
def get_real_ip(self, request): | |
try: | |
real_ip = request.META['HTTP_X_FORWARDED_FOR'] | |
except KeyError: | |
real_ip = request.META['REMOTE_ADDR'] | |
return real_ip | |
Der JavaScript-Code konnte sogar vereinfacht werden, nach erfolgreichem Login wird ein Reload durchgeführt, wodurch der Server die Authentifizierung starten kann. Dasselbe gilt beim Logout, zunächst wird per JavaScript-Funktion FB.logout() der User von Facebook abgemeldet, wonach die entsprechenden Cookies gelöscht sind, anschliessend erfolgt der Reload, so dass der Server seinerseits den Logout durchführen kann.
Beispiel des JavaScript-Codes:
<!-- slightly modified example of Facebook Connect with JavaScript SDK | |
Requirements: jQuery, Django Facebook Middleware (see https://gist.github.com/396557) | |
--> | |
$(document).ready(function() { | |
// Logout link binding | |
$('#fb-logout').bind('click', function() { | |
FB.logout(function() {}); | |
setTimeout(function() { | |
window.location="/user/do_logout/"; | |
},1000); | |
return false; | |
}); | |
}); | |
[...] | |
<div id="fb-root"></div> | |
<script> | |
window.fbAsyncInit = function() { | |
FB.init({ | |
appId : $('meta[property="fb:app_id"]').prop('content'), // App ID | |
channelUrl : 'CHANNEL_URL, see Facebook documentation', | |
status : true, // check login status | |
cookie : true, // enable cookies to allow the server to access the session | |
xfbml : true // parse XFBML | |
}); | |
FB.Event.subscribe('auth.login', | |
function(response) { | |
window.location.reload(); | |
} | |
); | |
}; | |
// Load the SDK Asynchronously | |
(function(d){ | |
var js, id = 'facebook-jssdk', ref = d.getElementsByTagName('script')[0]; | |
if (d.getElementById(id)) {return;} | |
js = d.createElement('script'); js.id = id; js.async = true; | |
js.src = "//connect.facebook.net/de_DE/all.js"; | |
ref.parentNode.insertBefore(js, ref); | |
}(document)); | |
</script> | |
<!-- Facebook Login button | |
--> | |
<div id="fb-login" class="fb-login-button">Facebook</div> | |
<!-- Facebook Logout --> | |
<a id="fb-logout" href="#"> | |
Logout</a> (Facebook) | |
Insgesamt ist die Einbindung von Facebook-Login zwar scheinbar einfach, aber der gesamte Prozess kann mit all seinen Zuständen durchaus komplex werden. Für Hinweise auf Fehler oder unvorhergesehenes Verhalten bin ich daher sehr dankbar.
Update 16.07.2012: Die Middleware ist noch einmal geändert worden. Der Fall dürfte zwar selten auftreten, aber: Ein User hat in einem Tab die Site geöffnet. In einem anderen loggt er sich per Facebook ein. Wenn nun versucht wurde, den normalen Anmeldeprozess per Site-Login durchzuführen, kam es zu einem Fehler. Nun wird dem Site-Login Vorrang vor dem Facebook-Login gewährt. D.h. in der genannten Situation wird der User als Site-User eingeloggt, nicht als Facebook-User. Beim bzw. nach dem Ausloggen tritt jedoch wieder das Facebook-Login in Kraft, falls der User noch bei Facebook eingeloggt ist und die Site geschke.name mit dem Facebook-Account verknüpft ist. Wie oben beschrieben – der gesamte Prozess ist nicht ganz tritival…
Update 25.04.2015: Der Beitrag bezieht sich auf die Facebook-Einbindung bei der damaligen geschke.name-Website.