Integracja Facebook Connect z kontami Django na filmaster.pl

W artykule Integracja Facebook Connect z Django opisałem jak skonfigurować aplikację Facebook Connect, oraz jak wstawić button logowania w XFBML na własnej stronie. Dla Filmastera wykonałem integrację Facebook Connect z kontami użytkowników Django, dzięki której użytkownik po zalogowaniu przez FB Connect jest automatycznie rejestrowany/logowany w serwisie.

Podstawą jest middleware, które sprawdza czy są cookies ustawione przez Facebook Connect i jeżeli są to próbuje zalogować, zarejestrować użytkownika Django. FBC tworzy cookie o nazwie takiej jak klucz naszej aplikacji na Facebooku (plus kilka dodatkowych KLUCZ_*). Sprawdzanie poprawności cookie zawarte jest w kodzie middleware:

class fbMiddleware(object):
    """
    Handle Facebook association, autologin
    """
    def process_request(self, request):
        request.facebookconn = False
        request.facebookconn_new = False
        f_name = False
        l_name = False
        # Check if we have the FBConnect cookie
        if settings.FACEBOOK_CONNECT_KEY in request.COOKIES:
            signature_hash = self.get_facebook_signature(request.COOKIES, True)
            # check if cookie is valud
            if signature_hash == request.COOKIES[settings.FACEBOOK_CONNECT_KEY]:
                # check if it didn't expired
                if datetime.fromtimestamp(float(request.COOKIES[settings.FACEBOOK_CONNECT_KEY+'_expires'])) > datetime.now():
                    # get the FB user ID from cookie
                    uid = '%s_user' % settings.FACEBOOK_CONNECT_KEY
                    cookie_uid = request.COOKIES[uid]
                    request.facebookconn = cookie_uid
                    # check if the FB user id is associated to a Django User account
                    try:
                        f = FBAssociation.objects.get(fb_uid=cookie_uid)
                    except Exception, e:
                        logging.debug("new Facebook association")
                        # no account, so we make a new one
                        password = ''.join([choice('1234567890qwertyuiopasdfghjklzxcvbnm') for i in range(10)])
                        if 'fb_name' in request.POST:
                            username = slughifi(request.POST['fb_name'])
                        else:
                            logging.error("NO Facebook username")
                            return None
                        
                        if 'fb_mail' in request.POST:
                            email = request.POST['fb_mail']
                            if len(email) >= 255:
                                # too long
                                email = str(cookie_uid)
                        else:
                            logging.error("No Facebook proxy mail")
                            return None
                        
                        username = username.replace('-', '')
                        # check if username taken
                        try:
                            u = User.objects.get(username=username)
                        except:
                            pass
                        else:
                            username = '%s%s' % (username, str(cookie_uid))
                            # this shouldn't exist
                            try:
                                u = User.objects.get(username=username)
                            except:
                                pass
                            else:
                                logging.error("Existing Facebook usernames %s" % username)
                                return None
                        
                        # check if mail is taken
                        try:
                            u = User.objects.get(email=email)
                        except:
                            pass
                        else:
                            email = str(cookie_uid)
                            try:
                                u = User.objects.get(email=email)
                            except:
                                pass
                            else:
                                logging.error("Existing proxy email %s" % email)
                                return None
                        
                        #make the user
                        user = None
                        try:
                            user = User.objects.create_user(username, email, password)
                            user.save()
                            user = authenticate(username=username, password=password)
                        except Exception, e:
                            try:
                                user.delete()
                            except:
                                pass
                            logging.error("Facebook User Creation Exception")
                            logging.error(e)
                            return None
                        else:
                            # save the Facebook association
                            o = FBAssociation(user=user, fb_uid=request.facebookconn, is_new=True, is_from_facebook=True)
                            o.save()
                        
                        if user is not None:
                            # login user and make the association
                            try:
                                login(request, user)
                            except Exception, e:
                                logging.error("Facebook User Login Exception")
                                logging.error(e)
                                return None
                            
                            if 'fb_pic' in request.POST:
                                try:
                                    path = settings.MEDIA_ROOT + date.today().strftime("avatars/%Y/%b/%d")
                                    if not os.path.isdir(path):
                                          os.makedirs(path)
                                    image = '%s/%s.jpg' %  (path, str(user.username))
                                    img = urllib2.urlopen(request.POST['fb_pic']).read()
                                    tmp = open('%s/%s.jpg' %  (path, str(user.username)), 'wb')
                                    tmp.write(img)
                                    tmp.close()
                                    i = Image.open(image)
                                    i.thumbnail((480, 480), Image.ANTIALIAS)
                                    i.convert("RGB").save(image, "JPEG")
                                    image = '%s/%s.jpg' %  (date.today().strftime("avatars/%Y/%b/%d"), str(user.username))
                                    avatar = Avatar(user=user, image=image, valid=True)
                                    avatar.save()
                                except Exception, e:
                                    logging.error("Could not save avatar from Facebook")
                                    logging.error(e)
                                    return None

                    else:
                        # FB ID is assigned to a Django User account. Login user
                        if not request.user.is_authenticated():
                            user = authenticate(user_id = f.user.id, fb_uid=cookie_uid)
                            if user is not None:
                                login(request, user)
        else:
            if request.user.is_authenticated():
                # not FB cookie but user from FB logged in? logout
                try:
                    f = FBAssociation.objects.get(user=request.user)
                except:
                    pass
                else:
                    if f.is_from_facebook:
                        logout(request)

    def get_facebook_signature(self, values_dict, is_cookie_check=False):
        """
        Generates signatures for FB requests/cookies
        """
        signature_keys = []
        for key in sorted(values_dict.keys()):
            if (is_cookie_check and key.startswith(settings.FACEBOOK_CONNECT_KEY + '_')):
                signature_keys.append(key)
            elif (is_cookie_check is False):
                signature_keys.append(key)
        
        if (is_cookie_check):
            signature_string = ''.join(['%s=%s' % (x.replace(settings.FACEBOOK_CONNECT_KEY + '_',''), values_dict[x]) for x in signature_keys])
        else:
            signature_string = ''.join(['%s=%s' % (x, values_dict[x]) for x in signature_keys])
        signature_string = signature_string + settings.FACEBOOK_CONNECT_SECRET
        
        return md5.new(signature_string).hexdigest()

Middleware rozpoznaje użytkownika z Facebook Connect po jego ID zawartym w cookie KLUCZ_user. Po tym ID tworzone są relacje z kontem w Django. Jeżeli middleware znajdzie powiązanie - to zaloguje na Djangowskie konto. Jeżeli nie ma powiązanego konta to będzie oczekiwało danych o użytkowniku (login, awatar, email), które można pobrać poprzez API Facebook Connect, ale za pomocą JavaScriptu. Dane te pobierane są po zalogowaniu przez FB Connect, przez kod w szablonie:

<script src="http://static.ak.connect.facebook.com/js/api_lib/v0.4/FeatureLoader.js.php" type="text/javascript"></script>
	<script type="text/javascript">
	
	FB_RequireFeatures(["XFBML"], function()
		{
		FB.Facebook.init("{{ connect_key }}", "/fb/xd_receiver.htm");
		FB.Facebook.get_sessionState().waitUntilReady(function()
			{
			var uid = FB.Facebook.apiClient.get_session().uid;
			if (uid)
				{
				{% if not user.is_authenticated %}
				var viewer  = FB.Facebook.apiClient.fql_query('SELECT name, pic_small, proxied_email FROM user WHERE uid='+uid,
					function(results) {
								$.post("/", {fb_name: results[0].name, fb_pic:  results[0].pic_small , fb_mail: results[0].proxied_email} , function(data){
									{% ifequal request.path request.login_url %}
										location.assign("/dashboard/");
									{% else %}
										location.assign("http://{{ request.META.HTTP_HOST }}{{request.path }}");
									{% endifequal %}
									});
								}
					);
				{% endif %}
				}
			});
		});
	
	</script>
Pobrane dane są AJAXem przesyłane do Django (wywołanie dowolnego URLa - middleware to przechwyci) i następnie strona jest odświeżana, a użytkownik jest już zalogowany, zarejestrowany i może w pełni korzystać z serwisu.

W przypadku API Facebooka - XFBML pewien problem stanowi konieczność stosowania API działającego po stronie klienta, co komplikuje działanie komponentu. Trzeba także stosować buttona Facebooka żeby skutecznie wylogować użytkownika (jeżeli usuniemy tylko cookie w serwisie to ponowne wyświetlenie buttona na jakiejś stronie spowoduje automatyczne zalogowanie użytkownika przez ten button)

<fb:login-button autologoutlink="true"></fb:login-button>

Filmaster jest projektem OpenSource i każdy może pomóc w jego rozwoju :) Kod znajduje się na bitbucket.org, a informacje dla programistów na filmaster.org :)
RkBlog

Django, 9 September 2009

Comment article
Comment article RkBlog main page Search RSS Contact