Its time for a confession: weve been deliberately ignoring an incredibly important aspect of Web development prior to this point. So far, weve thought of the traffic visiting our sites as some faceless, anonymous mass hurtling itself against our carefully designed pages.
This isnt true, of course. The browsers hitting our sites have real humans behind them (some of the time, at least). Thats a big thing to ignore: the Internet is at its best when it serves to connect people , not machines. If were going to develop truly compelling sites, eventually were going to have to deal with the bodies behind the browsers.
Unfortunately, its not all that easy. HTTP is designed to be stateless that is, each and every request happens in a vacuum. Theres no persistence between one request and the next, and we cant count on any aspects of a request (IP address, user agent, etc.) to consistently indicate successive requests from the same person.
In this chapter youll learn how to handle this lack of state. Well start at the lowest level (cookies ), and work up to the high-level tools for handling sessions, users and registration.
Browser developers long ago recognized that HTTPs statelessness poses a huge problem for Web developers, and thus cookies were born. A cookie is a small piece of information that browsers store on behalf of Web servers. Every time a browser requests a page from a certain server, it gives back the cookie that it initially received.
Lets take a look how this might work. When you open your browser and type in google.com , your browser sends an HTTP request to Google that starts something like this:
GET / HTTP/1.1 Host: google.com ...
When Google replies, the HTTP response looks something like the following:
HTTP/1.1 200 OK Content-Type: text/html Set-Cookie: PREF=ID=5b14f22bdaf1e81c:TM=1167000671:LM=1167000671; expires=Sun, 17-Jan-2038 19:14:07 GMT; path=/; domain=.google.com Server: GWS/2.1 ...
Notice the Set-Cookie header. Your browser will store that cookie value (PREF=ID=5b14f22bdaf1e81c:TM=1167000671:LM=1167000671 ) and serve it back to Google every time you access the site. So the next time you access Google, your browser is going to send a request like this:
GET / HTTP/1.1 Host: google.com Cookie: PREF=ID=5b14f22bdaf1e81c:TM=1167000671:LM=1167000671 ...
Google then can use that Cookie value to know that youre the same person who accessed the site earlier. This value might, for example, be a key into a database that stores user information. Google could (and does) use it to display your name on the page.
When dealing with persistence in Django, most of the time youll want to use the higher-level session and/or user frameworks discussed a little later in this chapter. However, well pause and look at how to read and write cookies at a low level. This should help you understand how the rest of the tools discussed in the chapter actually work, and it will come in handy if you ever need to play with cookies directly.
Reading cookies that are already set is incredibly simple. Every request object has a COOKIES object that acts like a dictionary; you can use it to read any cookies that the browser has sent to the view:
def show_color(request): if "favorite_color" in request.COOKIES: return HttpResponse("Your favorite color is %s" % \ request.COOKIES["favorite_color"]) else: return HttpResponse("You don't have a favorite color.")
Writing cookies is slightly more complicated. You need to use the set_cookie() method on an HttpResponse object. Heres an example that sets the favorite_color cookie based on a GET parameter:
def set_color(request): if "favorite_color" in request.GET: # Create an HttpResponse object... response = HttpResponse("Your favorite color is now %s" % \ request.GET["favorite_color"]) # ... and set a cookie on the response response.set_cookie("favorite_color", request.GET["favorite_color"]) return response else: return HttpResponse("You didn't give a favorite color.")
You can also pass a number of optional arguments to response.set_cookie() that control aspects of the cookie, as shown in Table 12-1.
Parameter | Default | Description |
---|---|---|
max_age | None | Age (in seconds) that the cookie should last. If this parameter is None , the cookie will last only until the browser is closed. |
expires | None | The actual date/time when the cookie should expire. It needs to be in the format "Wdy, DD-Mth-YY HH:MM:SS GMT" . If given, this parameter overrides the max_age parameter. |
path | "/" | The path prefix that this cookie is valid for. Browsers will only pass the cookie back to pages below this path prefix, so you can use this to prevent cookies from being sent to other sections of your site. This is especially useful when you dont control the top level of your sites domain. |
domain | None | The domain that this cookie is valid for. You can use this parameter to set a cross-domain cookie. For example, domain=".example.com" will set a cookie that is readable by the domains www.example.com , www2.example.com , and an.other.sub.domain.example.com . If this parameter is set to None , a cookie will only be readable by the domain that set it. |
secure | False | If set to True , this parameter instructs the browser to only return this cookie to pages accessed over HTTPS. |
You might notice a number of potential problems with the way cookies work. Lets look at some of the more important ones:
Storage of cookies is essentially voluntary; browsers dont guarantee anything. In fact, all browsers enable users to control the policy for accepting cookies. If you want to see just how vital cookies are to the Web, try turning on your browsers prompt to accept every cookie option.
Despite their nearly universal use, cookies are still the definition of unreliability. This means that developers should check that a user actually accepts cookies before relying on them.
More important, you should never store important data in cookies. The Web is filled with horror stories of developers whove stored unrecoverable information in browser cookies only to have that data purged by the browser for one reason or another.
Cookies (especially those not sent over HTTPS) are not secure. Because HTTP data is sent in cleartext, cookies are extremely vulnerable to snooping attacks. That is, an attacker snooping on the wire can intercept a cookie and read it. This means you should never store sensitive information in a cookie.
Theres an even more insidious attack, known as a man-in-the-middle attack, wherein an attacker intercepts a cookie and uses it to pose as another user. Chapter 19 discusses attacks of this nature in depth, as well as ways to prevent it.
Cookies arent even secure from their intended recipients. Most browsers provide easy ways to edit the content of individual cookies, and resourceful users can always use tools like mechanize ( http://wwwsearch.sourceforge.net/mechanize/ ) to construct HTTP requests by hand.
So you cant store data in cookies that might be sensitive to tampering. The canonical mistake in this scenario is storing something like IsLoggedIn=1 in a cookie when a user logs in. Youd be amazed at the number of sites that make mistakes of this nature; it takes only a second to fool these sites security systems.
With all of these limitations and potential security holes, its obvious that cookies and persistent sessions are examples of those pain points in Web development. Of course, Djangos goal is to be an effective painkiller, so it comes with a session framework designed to smooth over these difficulties for you.
This session framework lets you store and retrieve arbitrary data on a per-site visitor basis. It stores data on the server side and abstracts the sending and receiving of cookies. Cookies use only a hashed session IDnot the data itselfthus protecting you from most of the common cookie problems.
Lets look at how to enable sessions and use them in views.
Sessions are implemented via a piece of middleware (see Chapter 15) and a Django model. To enable sessions, youll need to follow these steps:
Edit your MIDDLEWARE_CLASSES setting and make sure MIDDLEWARE_CLASSES contains 'django.contrib.sessions.middleware.SessionMiddleware' .
Make sure 'django.contrib.sessions' is in your INSTALLED_APPS setting (and run manage.py syncdb if you have to add it).
The default skeleton settings created by startproject have both of these bits already installed, so unless youve removed them, you probably dont have to change anything to get sessions to work.
If you dont want to use sessions, you might want to remove the SessionMiddleware line from MIDDLEWARE_CLASSES and 'django.contrib.sessions' from your INSTALLED_APPS . It will save you only a small amount of overhead, but every little bit counts.
When SessionMiddleware is activated, each HttpRequest objectthe first argument to any Django view functionwill have a session attribute, which is a dictionary-like object. You can read it and write to it in the same way youd use a normal dictionary. For example, in a view you could do stuff like this:
# Set a session value: request.session["fav_color"] = "blue" # Get a session value -- this could be called in a different view, # or many requests later (or both): fav_color = request.session["fav_color"] # Clear an item from the session: del request.session["fav_color"] # Check if the session has a given key: if "fav_color" in request.session: ...
You can also use other mapping methods like keys() and items() on request.session .
There are a couple of simple rules for using Djangos sessions effectively:
Use normal Python strings as dictionary keys on request.session (as opposed to integers, objects, etc.). This is more of a convention than a hard-and-fast rule, but its worth following.
Session dictionary keys that begin with an underscore are reserved for internal use by Django. In practice, the framework uses only a small number of underscore-prefixed session variables, but unless you know what they all are (and you are willing to keep up with any changes in Django itself), staying away from underscore prefixes will keep Django from interfering with your application.
Dont replace request.session with a new object, and dont access or set its attributes. Use it like a Python dictionary.
Lets take a look at a few quick examples. This simplistic view sets a has_commented variable to True after a user posts a comment. Its a simple (but not particularly secure) way of preventing a user from posting more than one comment:
def post_comment(request, new_comment): if request.session.get('has_commented', False): return HttpResponse("You've already commented.") c = comments.Comment(comment=new_comment) c.save() request.session['has_commented'] = True return HttpResponse('Thanks for your comment!')
This simplistic view logs in a member of the site:
def login(request): try: m = Member.objects.get(username__exact=request.POST['username']) if m.password == request.POST['password']: request.session['member_id'] = m.id return HttpResponse("You're logged in.") except Member.DoesNotExist: return HttpResponse("Your username and password didn't match.")
And this one logs out a member, according to login() :
def logout(request): try: del request.session['member_id'] except KeyError: pass return HttpResponse("You're logged out.")
Note
In practice, this is a lousy way of logging users in. The authentication framework discussed shortly handles this task for you in a much more robust and useful manner. These examples are deliberately simplistic so that you can easily see whats going on.
As just mentioned, you cant rely on every browser accepting cookies. So, as a convenience, Django provides an easy way to test whether a users browser accepts cookies. You just need to call request.session.set_test_cookie() in a view, and check request.session.test_cookie_worked() in a subsequent viewnot in the same view call.
This awkward split between set_test_cookie() and test_cookie_worked() is necessary due to the way cookies work. When you set a cookie, you cant actually tell whether a browser accepted it until the browsers next request.
Its good practice to use delete_test_cookie() to clean up after yourself. Do this after youve verified that the test cookie worked.
Heres a typical usage example:
def login(request): # If we submitted the form... if request.method == 'POST': # Check that the test cookie worked (we set it below): if request.session.test_cookie_worked(): # The test cookie worked, so delete it. request.session.delete_test_cookie() # In practice, we'd need some logic to check username/password # here, but since this is an example... return HttpResponse("You're logged in.") # The test cookie failed, so display an error message. If this # was a real site we'd want to display a friendlier message. else: return HttpResponse("Please enable cookies and try again.") # If we didn't post, send the test cookie along with the login form. request.session.set_test_cookie() return render_to_response('foo/login_form.html')
Note
Again, the built-in authentication functions handle this check for you.
Internally, each session is just a normal Django model defined in django.contrib.sessions.models . Each session is identified by a more-or-less random 32-character hash stored in a cookie. Because its a normal model, you can access sessions using the normal Django database API:
>>> from django.contrib.sessions.models import Session >>> s = Session.objects.get(pk='2b1189a188b44ad18c35e113ac6ceead') >>> s.expire_date datetime.datetime(2005, 8, 20, 13, 35, 12)
Youll need to call get_decoded() to get the actual session data. This is necessary because the dictionary is stored in an encoded format:
>>> s.session_data 'KGRwMQpTJ19hdXRoX3VzZXJfaWQnCnAyCkkxCnMuMTExY2ZjODI2Yj...' >>> s.get_decoded() {'user_id': 42}
By default, Django only saves to the database if the session has been modified that is, if any of its dictionary values have been assigned or deleted:
# Session is modified. request.session['foo'] = 'bar' # Session is modified. del request.session['foo'] # Session is modified. request.session['foo'] = {} # Gotcha: Session is NOT modified, because this alters # request.session['foo'] instead of request.session. request.session['foo']['bar'] = 'baz'
To change this default behavior, set SESSION_SAVE_EVERY_REQUEST to True . If SESSION_SAVE_EVERY_REQUEST is True , Django will save the session to the database on every single request, even if it wasnt changed.
Note that the session cookie is sent only when a session has been created or modified. If SESSION_SAVE_EVERY_REQUEST is True , the session cookie will be sent on every request. Similarly, the expires part of a session cookie is updated each time the session cookie is sent.
You might have noticed that the cookie Google sent us contained expires=Sun, 17-Jan-2038 19:14:07 GMT; . Cookies can optionally contain an expiration date that advises the browser on when to remove the cookie. If a cookie doesnt contain an expiration value, the browser will expire it when the user closes his or her browser window. You can control the session frameworks behavior in this regard with the SESSION_EXPIRE_AT_BROWSER_CLOSE setting.
By default, SESSION_EXPIRE_AT_BROWSER_CLOSE is set to False , which means session cookies will be stored in users browsers for SESSION_COOKIE_AGE seconds (which defaults to two weeks, or 1,209,600 seconds). Use this if you dont want people to have to log in every time they open a browser.
If SESSION_EXPIRE_AT_BROWSER_CLOSE is set to True , Django will use browser-length cookies.
Besides the settings already mentioned, a few other settings influence how Djangos session framework uses cookies, as shown in Table 12-2.
Setting | Description | Default |
---|---|---|
SESSION_COOKIE_DOMAIN | The domain to use for session cookies. Set this to a string such as ".lawrence.com" for cross-domain cookies, or use None for a standard cookie. | None |
SESSION_COOKIE_NAME | The name of the cookie to use for sessions. This can be any string. | "sessionid" |
SESSION_COOKIE_SECURE | Whether to use a secure cookie for the session cookie. If this is set to True , the cookie will be marked as secure, which means that browsers will ensure that the cookie is only sent via HTTPS. | False |
Technical Details
For the curious, here are a few technical notes about the inner workings of the session framework:
The session dictionary accepts any Python object capable of being pickled. See the documentation for Pythons built-in pickle module for information about how this works.
Session data is stored in a database table named django_session .
Session data is fetched upon demand. If you never access request.session , Django wont hit that database table.
Django only sends a cookie if it needs to. If you dont set any session data, it wont send a session cookie (unless SESSION_SAVE_EVERY_REQUEST is set to True ).
The Django sessions framework is entirely, and solely, cookie based. It does not fall back to putting session IDs in URLs as a last resort, as some other tools (PHP, JSP) do.
This is an intentional design decision. Putting sessions in URLs dont just make URLs ugly, but also make your site vulnerable to a certain form of session ID theft via the Referer header.
If youre still curious, the source is pretty straightforward; look in django.contrib.sessions for more details.
Were now halfway to linking browsers directly to Real People. Sessions give us a way of persisting data through multiple browser requests; the second part of the equation is using those sessions for user login. Of course, we cant just trust that users are who they say they are, so we need to authenticate them along the way.
Naturally, Django provides tools to handle this common task (and many others). Djangos user authentication system handles user accounts, groups, permissions, and cookie-based user sessions. This system is often referred to as an auth/auth (authentication and authorization) system. That name recognizes that dealing with users is often a two-step process. We need to
Verify (authenticate ) that a user is who he or she claims to be (usually by checking a username and password against a database of users)
Verify that the user is authorized to perform some given operation (usually by checking against a table of permissions)
Following these needs, Djangos auth/auth system consists of a number of parts:
Users : People registered with your site
Permissions : Binary (yes/no) flags designating whether a user may perform a certain task
Groups : A generic way of applying labels and permissions to more than one user
Messages : A simple way to queue and display system messages to users
Profiles : A mechanism to extend the user object with custom fields
If youve used the admin tool (detailed in Chapter 6), youve already seen many of these tools, and if youve edited users or groups in the admin tool, youve actually been editing data in the auth systems database tables.
Like the session tools, authentication support is bundled as a Django application in django.contrib , which needs to be installed. Like the session system, its also installed by default, but if youve removed it, youll need to follow these steps to install it:
Make sure the session framework is installed as described earlier in this chapter. Keeping track of users obviously requires cookies, and thus builds on the session framework.
Put 'django.contrib.auth' in your INSTALLED_APPS setting and run manage.py syncdb .
Make sure that 'django.contrib.auth.middleware.AuthenticationMiddleware' is in your MIDDLEWARE_CLASSES setting*after* SessionMiddleware .
With that installation out of the way, were ready to deal with users in view functions. The main interface youll use to access users within a view is request.user ; this is an object that represents the currently logged-in user. If the user isnt logged in, this will instead be an AnonymousUser object (see below for more details).
You can easily tell if a user is logged in with the is_authenticated() method:
if request.user.is_authenticated(): # Do something for authenticated users. else: # Do something for anonymous users.
Once you have a User often from request.user , but possibly through one of the other methods discussed shortlyyou have a number of fields and methods available on that object. AnonymousUser objects emulate some of this interface, but not all of it, so you should always check user.is_authenticated() before assuming youre dealing with a bona fide user object. Tables 12-3 and 12-4 list the fields and methods, respectively, on User objects.
Field | Description |
---|---|
username | Required; 30 characters or fewer. Alphanumeric characters only (letters, digits, and underscores). |
first_name | Optional; 30 characters or fewer. |
last_name | Optional; 30 characters or fewer. |
Optional. Email address. | |
password | Required. A hash of, and metadata about, the password (Django doesnt store the raw password). See the Passwords section for more about this value. |
is_staff | Boolean. Designates whether this user can access the admin site. |
is_active | Boolean. Designates whether this account can be used to log in. Set this flag to False instead of deleting accounts. |
is_superuser | Boolean. Designates that this user has all permissions without explicitly assigning them. |
last_login | A datetime of the users last login. This is set to the current date/time by default. |
date_joined | A datetime designating when the account was created. This is set to the current date/time by default when the account is created. |
Method | Description |
---|---|
is_authenticated() | Always returns True for real User objects. This is a way to tell if the user has been authenticated. This does not imply any permissions, and it doesnt check if the user is active. It only indicates that the user has sucessfully authenticated. |
is_anonymous() | Returns True only for AnonymousUser objects (and False for real User objects). Generally, you should prefer using is_authenticated() to this method. |
get_full_name() | Returns the first_name plus the last_name , with a space in between. |
set_password(passwd) | Sets the users password to the given raw string, taking care of the password hashing. This doesnt actually save the User object. |
check_password(passwd) | Returns True if the given raw string is the correct password for the user. This takes care of the password hashing in making the comparison. |
get_group_permissions() | Returns a list of permission strings that the user has through the groups he or she belongs to. |
get_all_permissions() | Returns a list of permission strings that the user has, both through group and user permissions. |
has_perm(perm) | Returns True if the user has the specified permission, where perm is in the format "package.codename" . If the user is inactive, this method will always return False . |
has_perms(perm_list) | Returns True if the user has all of the specified permissions. If the user is inactive, this method will always return False . |
has_module_perms(app_label) | Returns True if the user has any permissions in the given app_label . If the user is inactive, this method will always return False . |
get_and_delete_messages() | Returns a list of Message objects in the users queue and deletes the messages from the queue. |
email_user(subj, msg) | Sends an email to the user. This email is sent from the DEFAULT_FROM_EMAIL setting. You can also pass a third argument, from_email , to override the From address on the email. |
get_profile() | Returns a site-specific profile for this user. See the Profiles section for more on this method. |
Finally, User objects have two many-to-many fields: groups and permissions . User objects can access their related objects in the same way as any other many-to-many field:
# Set a user's groups: myuser.groups = group_list # Add a user to some groups: myuser.groups.add(group1, group2,...) # Remove a user from some groups: myuser.groups.remove(group1, group2,...) # Remove a user from all groups: myuser.groups.clear() # Permissions work the same way myuser.permissions = permission_list myuser.permissions.add(permission1, permission2, ...) myuser.permissions.remove(permission1, permission2, ...) myuser.permissions.clear()
Django provides built-in view functions for handling logging in and out (and a few other nifty tricks), but before we get to those, lets take a look at how to log users in and out by hand. Django provides two functions to perform these actions in django.contrib.auth : authenticate() and login() .
To authenticate a given username and password, use authenticate() . It takes two keyword arguments, username and password , and it returns a User object if the password is valid for the given username. If the password is invalid, authenticate() returns None :
>>> from django.contrib import auth >>> user = auth.authenticate(username='john', password='secret') >>> if user is not None: ... print "Correct!" ... else: ... print "Oops, that's wrong!"
authenticate() only verifies a users credentials. To log in a user, use login() . It takes an HttpRequest object and a User object and saves the users ID in the session, using Djangos session framework.
This example shows how you might use both authenticate() and login() within a view function:
from django.contrib import auth def login(request): username = request.POST['username'] password = request.POST['password'] user = auth.authenticate(username=username, password=password) if user is not None and user.is_active: # Correct password, and the user is marked "active" auth.login(request, user) # Redirect to a success page. return HttpResponseRedirect("/account/loggedin/") else: # Show an error page return HttpResponseRedirect("/account/invalid/")
To log out a user, use django.contrib.auth.logout() within your view. It takes an HttpRequest object and has no return value:
from django.contrib import auth def logout(request): auth.logout(request) # Redirect to a success page. return HttpResponseRedirect("/account/loggedout/")
Note that logout() doesnt throw any errors if the user wasnt logged in.
In practice, you usually will not need to write your own login/logout functions; the authentication system comes with a set of views for generically handling logging in and out.
The first step in using the authentication views is to wire them up in your URLconf. Youll need to add this snippet:
from django.contrib.auth.views import login, logout urlpatterns = patterns('', # existing patterns here... (r'^accounts/login/$', login), (r'^accounts/logout/$', logout), )
/accounts/login/ and /accounts/logout/ are the default URLs that Django uses for these views.
By default, the login view renders a template at registration/login.html (you can change this template name by passing an extra view argument ,``template_name``). This form needs to contain a username and a password field. A simple template might look like this:
{% extends "base.html" %} {% block content %} {% if form.errors %} <p class="error">Sorry, that's not a valid username or password</p> {% endif %} <form action='.' method='post'> <label for="username">User name:</label> <input type="text" name="username" value="" id="username"> <label for="password">Password:</label> <input type="password" name="password" value="" id="password"> <input type="submit" value="login" /> <input type="hidden" name="next" value="{{ next|escape }}" /> <form action='.' method='post'> {% endblock %}
If the user successfully logs in, he or she will be redirected to /accounts/profile/ by default. You can override this by providing a hidden field called next with the URL to redirect to after logging in. You can also pass this value as a GET parameter to the login view and it will be automatically added to the context as a variable called next that you can insert into that hidden field.
The logout view works a little differently. By default it renders a template at registration/logged_out.html (which usually contains a Youve successfully logged out message). However, you can call the view with an extra argument, next_page , which will instruct the view to redirect after a logout.
Of course, the reason were going through all this trouble is so we can limit access to parts of our site.
The simple, raw way to limit access to pages is to check request.user.is_authenticated() and redirect to a login page:
from django.http import HttpResponseRedirect def my_view(request): if not request.user.is_authenticated(): return HttpResponseRedirect('/login/?next=%s' % request.path) # ...
or perhaps display an error message:
def my_view(request): if not request.user.is_authenticated(): return render_to_response('myapp/login_error.html') # ...
As a shortcut, you can use the convenient login_required decorator:
from django.contrib.auth.decorators import login_required @login_required def my_view(request): # ...
login_required does the following:
If the user isnt logged in, redirect to /accounts/login/ , passing the current absolute URL in the query string as next , for example: /accounts/login/?next=/polls/3/ .
If the user is logged in, execute the view normally. The view code can then assume that the user is logged in.
Limiting access based on certain permissions or some other test, or providing a different location for the login view works essentially the same way.
The raw way is to run your test on request.user in the view directly. For example, this view checks to make sure the user is logged in and has the permission polls.can_vote (more about how permissions works follows):
def vote(request): if request.user.is_authenticated() and request.user.has_perm('polls.can_vote')): # vote here else: return HttpResponse("You can't vote in this poll.")
Again, Django provides a shortcut called user_passes_test . It takes arguments and generates a specialized decorator for your particular situation:
def user_can_vote(user): return user.is_authenticated() and user.has_perm("polls.can_vote") @user_passes_text(user_can_vote, login_url="/login/") def vote(request): # Code here can assume a logged-in user with the correct permission. ...
user_passes_test takes one required argument: a callable that takes a User object and returns True if the user is allowed to view the page. Note that user_passes_test does not automatically check that the User is authenticated; you should do that yourself.
In this example were also showing the second optional argument, login_url , which lets you specify the URL for your login page (/accounts/login/ by default).
Since its a relatively common task to check whether a user has a particular permission, Django provides a shortcut for that case: the permission_required() decorator. Using this decorator, the earlier example can be written as follows:
from django.contrib.auth.decorators import permission_required @permission_required('polls.can_vote', login_url="/login/") def vote(request): # ...
Note that permission_required() also takes an optional login_url parameter, which also defaults to '/accounts/login/' .
Limiting Access to Generic Views
One of the most frequently asked questions on the Django users list deals with limiting access to a generic view. To pull this off, youll need to write a thin wrapper around the view and point your URLconf to your wrapper instead of the generic view itself:
from dango.contrib.auth.decorators import login_required from django.views.generic.date_based import object_detail @login_required def limited_object_detail(*args, **kwargs): return object_detail(*args, **kwargs)
You can, of course, replace login_required with any of the other limiting decorators.
The easiest way by far to manage the auth system is through the admin interface. Chapter 6 discusses how to use Djangos admin interface to edit users and control their permissions and access, and most of the time youll just use that interface.
However, there are low-level APIs you can delve into when you need absolute control, and we discuss these in the sections that follow.
Create users with the create_user helper function:
>>> from django.contrib.auth.models import User >>> user = User.objects.create_user(username='john', ... email='jlennon@beatles.com', ... password='glass onion')
At this point, user is a User instance ready to be saved to the database (create_user() doesnt actually call save() itself). You can continue to change its attributes before saving, too:
>>> user.is_staff = True >>> user.save()
You can change a password with set_password() :
>>> user = User.objects.get(username='john') >>> user.set_password('goo goo goo joob') >>> user.save()
Dont set the password attribute directly unless you know what youre doing. The password is actually stored as a salted hash and thus cant be edited directly.
More formally, the password attribute of a User object is a string in this format:
hashtype$salt$hash
Thats a hash type, the salt, and the hash itself, separated by the dollar sign ($) character.
hashtype is either sha1 (default) or md5 , the algorithm used to perform a one-way hash of the password. salt is a random string used to salt the raw password to create the hash, for example:
sha1$a1976$a36cc8cbf81742a8fb52e221aaeab48ed7f58ab4
The User.set_password() and User.check_password() functions handle the setting and checking of these values behind the scenes.
Is a Salted Hash Some Kind of Drug?
No, a salted hash has nothing to do with marijuana; its actually a common way to securely store passwords. A hash is a one-way cryptographic functionthat is, you can easily compute the hash of a given value, but its nearly impossible to take a hash and reconstruct the original value.
If we stored passwords as plain text, anyone who got their hands on the password database would instantly know everyones password. Storing passwords as hashes reduces the value of a compromised database.
However, an attacker with the password database could still run a brute- force attack, hashing millions of passwords and comparing those hashes against the stored values. This takes some time, but less than you might thinkcomputers are incredibly fast.
Worse, there are publicly available rainbow tables , or databases of precomputed hashes of millions of passwords. With a rainbow table, an attacker can break most passwords in seconds.
Adding a salt basically an initial random valueto the stored hash adds another layer of difficulty to breaking passwords. Since salts differ from password to password, they also prevent the use of a rainbow table, thus forcing attackers to fall back on a brute-force attack, itself made more difficult by the extra entropy added to the hash by the salt.
While salted hashes arent absolutely the most secure way of storing passwords, theyre a good middle ground between security and convenience.
We can use these low-level tools to create views that allow users to sign up. Nearly every developer wants to implement registration differently, so Django leaves writing a registration view up to you. Luckily, its pretty easy.
At its simplest, we could provide a small view that prompts for the required user information and creates those users. Django provides a built-in form you can use for this purpose, which well use in this example:
from django import oldforms as forms from django.http import HttpResponseRedirect from django.shortcuts import render_to_response from django.contrib.auth.forms import UserCreationForm def register(request): form = UserCreationForm() if request.method == 'POST': data = request.POST.copy() errors = form.get_validation_errors(data) if not errors: new_user = form.save(data) return HttpResponseRedirect("/books/") else: data, errors = {}, {} return render_to_response("registration/register.html", { 'form' : forms.FormWrapper(form, data, errors) })
This form assumes a template named registration/register.html . Heres an example of what that template might look like:
{% extends "base.html" %} {% block title %}Create an account{% endblock %} {% block content %} <h1>Create an account</h1> <form action="." method="post"> {% if form.error_dict %} <p class="error">Please correct the errors below.</p> {% endif %} {% if form.username.errors %} {{ form.username.html_error_list }} {% endif %} <label for="id_username">Username:</label> {{ form.username }} {% if form.password1.errors %} {{ form.password1.html_error_list }} {% endif %} <label for="id_password1">Password: {{ form.password1 }} {% if form.password2.errors %} {{ form.password2.html_error_list }} {% endif %} <label for="id_password2">Password (again): {{ form.password2 }} <input type="submit" value="Create the account" /> </label> {% endblock %}
Note
django.contrib.auth.forms.UserCreationForm is, at the time of publication, an oldforms Form. See http://www.djangoproject.com/documentation/0.96/forms/ for details on oldforms. The transition to newforms, as covered in Chapter 7, will be completed in the near future.
The current logged-in user and his or her permissions are made available in the template context when you use RequestContext (see Chapter 10).
Note
Technically, these variables are only made available in the template context if you use RequestContext and your TEMPLATE_CONTEXT_PROCESSORS setting contains "django.core.context_processors.auth" , which is the default. Again, see Chapter 10 for more information.
When using RequestContext , the current user (either a User instance or an AnonymousUser instance) is stored in the template variable {{ user }} :
{% if user.is_authenticated %} <p>Welcome, {{ user.username }}. Thanks for logging in.</p> {% else %} <p>Welcome, new user. Please log in.</p> {% endif %}
This users permissions are stored in the template variable {{ perms }} . This is a template-friendly proxy to a couple of permission methods described shortly.
There are two ways you can use this perms object. You can use something like {{ perms.polls }} to check if the user has any permissions for some given application, or you can use something like {{ perms.polls.can_vote }} to check if the user has a specific permission.
Thus, you can check permissions in template {% if %} statements:
{% if perms.polls %} <p>You have permission to do something in the polls app.</p> {% if perms.polls.can_vote %} <p>You can vote!</p> {% endif %} {% else %} <p>You don't have permission to do anything in the polls app.</p> {% endif %}
There are a few other bits of the authentication framework that weve only dealt with in passing. Well take a closer look at them in the following sections.
Permissions are a simple way to mark users and groups as being able to perform some action. They are usually used by the Django admin site, but you can easily use them in your own code.
The Django admin site uses permissions as follows:
Access to view the add form, and add an object is limited to users with the add permission for that type of object.
Access to view the change list, view the change form, and change an object is limited to users with the change permission for that type of object.
Access to delete an object is limited to users with the delete permission for that type of object.
Permissions are set globally per type of object, not per specific object instance. For example, its possible to say Mary may change news stories, but its not currently possible to say Mary may change news stories, but only the ones she created herself or Mary may only change news stories that have a certain status, publication date, or ID.
These three basic permissionsadd, change, and deleteare automatically created for each Django model that has a class Admin . Behind the scenes, these permissions are added to the auth_permission database table when you run manage.py syncdb .
These permissions will be of the form "<app>.<action>_<object_name>" . That is, if you have a polls application with a Choice model, youll get permissions named "polls.add_choice" , "polls.change_choice" , and "polls.delete_choice" .
Note that if your model doesnt have class Admin set when you run syncdb , the permissions wont be created. If you initialize your database and add class Admin to models after the fact, youll need to run syncdb again to create any missing permissions for your installed applications.
You can also create custom permissions for a given model object using the permissions attribute on Meta . This example model creates three custom permissions:
class USCitizen(models.Model): # ... class Meta: permissions = ( # Permission identifier human-readable permission name ("can_drive", "Can drive"), ("can_vote", "Can vote in elections"), ("can_drink", "Can drink alcohol"), )
This only creates those extra permissions when you run syncdb ; its up to you to check for these permissions in your views.
Just like users, permissions are implemented in a Django model that lives in django.contrib.auth.models . This means that you can use Djangos database API to interact directly with permissions if you like.
Groups are a generic way of categorizing users so you can apply permissions, or some other label, to those users. A user can belong to any number of groups.
A user in a group automatically has the permissions granted to that group. For example, if the group Site editors has the permission can_edit_home_page , any user in that group will have that permission.
Groups are also a convenient way to categorize users to give them some label, or extended functionality. For example, you could create a group 'Special users' , and you could write code that could, say, give those users access to a members-only portion of your site, or send them members-only email messages.
Like users, the easiest way to manage groups is through the admin interface. However, groups are also just Django models that live in django.contrib.auth.models , so once again you can always use Djangos database APIs to deal with groups at a low level.
The message system is a lightweight way to queue messages for given users. A message is associated with a User . Theres no concept of expiration or timestamps.
Messages are used by the Django admin interface after successful actions. For example, when you create an object, youll notice a The object was created successfully message at the top of the admin page.
You can use the same API to queue and display messages in your own application. The API is simple:
To create a new message, use user.message_set.create(message='message_text') .
To retrieve/delete messages, use user.get_and_delete_messages() , which returns a list of Message objects in the users queue (if any) and deletes the messages from the queue.
In this example view, the system saves a message for the user after creating a playlist:
def create_playlist(request, songs): # Create the playlist with the given songs. # ... request.user.message_set.create( message="Your playlist was added successfully." ) return render_to_response("playlists/create.html", context_instance=RequestContext(request))
When you use RequestContext , the current logged-in user and his or her messages are made available in the template context as the template variable {{ messages }} . Heres an example of template code that displays messages:
{% if messages %} <ul> {% for message in messages %} <li>{{ message }}</li> {% endfor %} </ul> {% endif %}
Note that RequestContext calls get_and_delete_messages behind the scenes, so any messages will be deleted even if you dont display them.
Finally, note that this messages framework only works with users in the user database. To send messages to anonymous users, use the session framework directly.
The final piece of the puzzle is the profile system. To understand what profiles are all about, lets first look at the problem.
In a nutshell, many sites need to store more user information than is available on the standard User object. To compound the problem, most sites will have different extra fields. Thus, Django provides a lightweight way of defining a profile object thats linked to a given user. This profile object can differ from project to project, and it can even handle different profiles for different sites served from the same database.
The first step in creating a profile is to define a model that holds the profile information. The only requirement Django places on this model is that it have a unique ForeignKey to the User model; this field must be named user . Other that that, you can use any other fields you like. Heres a strictly arbitrary profile model:
from django.db import models from django.contrib.auth.models import User class MySiteProfile(models.Model): # This is the only required field user = models.ForeignKey(User, unique=True) # The rest is completely up to you... favorite_band = models.CharField(maxlength=100, blank=True) favorite_cheese = models.CharField(maxlength=100, blank=True) lucky_number = models.IntegerField()
Next, youll need to tell Django where to look for this profile object. You do that by setting the AUTH_PROFILE_MODULE setting to the identifier for your model. So, if your model lives in an application called myapp , youd put this in your settings file:
AUTH_PROFILE_MODULE = "myapp.mysiteprofile"
Once thats done, you can access a users profile by calling user.get_profile() . This function could raise a SiteProfileNotAvailable exception if AUTH_PROFILE_MODULE isnt defined, or it could raise a DoesNotExist exception if the user doesnt have a profile already (youll usually catch that exception and create a new profile at that time).
Yes, the session and authorization system is a lot to absorb. Most of the time you wont need all the features described in this chapter, but when you need to allow complex interactions between users, its good to have all that power available.
In the next chapter, well take a look at a piece of Django that builds on top of this session/user system: the comments application. It allows you to easily attach commentsfrom anonymous or authenticated usersto arbitrary objects. Onward and upward!
鍏充簬鏈瘎娉ㄧ郴缁
鏈珯浣跨敤涓婁笅鏂囧叧鑱旂殑璇勬敞绯荤粺鏉ユ敹闆嗗弽棣堜俊鎭備笉鍚屼簬涓鑸鏁寸珷鍋氳瘎娉ㄧ殑鍋氭硶锛 鎴戜滑鍏佽浣犲姣忎竴涓嫭绔嬬殑鈥滄枃鏈潡鈥濆仛璇勬敞銆備竴涓滄枃鏈潡鈥濈湅璧锋潵鏄繖鏍风殑锛
涓涓滄枃鏈潡鈥濇槸涓涓钀斤紝涓涓垪琛ㄩ」锛屼竴娈典唬鐮侊紝鎴栬呭叾浠栦竴灏忔鍐呭銆 浣犻変腑瀹冧細楂樹寒搴︽樉绀:
瑕佸鏂囨湰鍧楀仛璇勬敞锛屼綘鍙渶瑕佺偣鍑诲畠鏃佽竟鐨勬爣璇嗗潡:
鎴戜滑浼氫粩缁嗛槄璇绘瘡涓瘎璁猴紝濡傛灉鍙兘鐨勮瘽鎴戜滑涔熶細鎶婅瘎娉ㄨ冭檻鍒版湭鏉ョ殑鐗堟湰涓幓:
濡傛灉浣犳効鎰忎綘鐨勮瘎娉ㄨ閲囩敤锛岃纭繚鐣欎笅浣犵殑鍏ㄥ悕 (娉ㄦ剰涓嶆槸鏄电О鎴栫畝绉帮級
Many, many thanks to Jack Slocum; the inspiration and much of the code for the comment system comes from Jack's blog, and this site couldn't have been built without his wonderful
YAHOO.ext
library. Thanks also to Yahoo for YUI itself.