Commit | Line | Data |
021ca3a3 |
1 | =pod |
2 | |
3 | =head1 NAME |
4 | |
5 | Catalyst::Plugin::Session::Tutorial - Understanding and using sessions. |
6 | |
7 | =head1 ASSUMPTIONS |
8 | |
bfa4f9cc |
9 | This tutorial assumes that you are familiar with web applications in |
10 | general and Catalyst specifically (up to models and configuration), and |
11 | that you know what HTTP is. |
021ca3a3 |
12 | |
13 | =head1 WHAT ARE SESSIONS |
14 | |
15 | When users use a site, especially one that knows who they are (sites you log in |
bfa4f9cc |
16 | to, sites which let you keep a shopping cart, etc.), the server preparing the |
021ca3a3 |
17 | content has to know that request X comes from client A while request Y comes |
5104edbd |
18 | from client B, so that each user gets the content meant for them. |
021ca3a3 |
19 | |
20 | The problem is that HTTP is a stateless protocol. This means that every request |
21 | is distinct, and even if it comes from the same client, it's difficult to know |
22 | that. |
23 | |
24 | The way sessions are maintained between distinct requests is that the client |
bfa4f9cc |
25 | says, for every request, "I'm client A" or "I'm client B". |
021ca3a3 |
26 | |
27 | This piece of data that tells the server "I'm X" is called the session ID, and |
28 | the threading of several requests together is called a session. |
29 | |
30 | =head1 HOW SESSIONS WORK |
31 | |
32 | =head2 Cookies |
33 | |
34 | HTTP has a feature that lets this become easier, called cookies. A cookie is |
35 | something the server asks the client to save somewhere, and resend every time a |
36 | request is made. |
37 | |
2cf2efb9 |
38 | The way they work is that the server sends the C<Set-Cookie> header, with a |
bfa4f9cc |
39 | cookie name, a value, and some metadata (like when it expires, what paths it |
40 | applies to, etc.). The client saves this. |
021ca3a3 |
41 | |
42 | Then, on every subsequent request the client will send a C<Cookie> header, with |
43 | the cookie name and value. |
44 | |
45 | =head2 Cookie Alternatives |
46 | |
47 | Another way is to make sure that the session ID is repeated is to include it in |
48 | every URI. |
49 | |
50 | This can be as either a part of the path, or as a query parameter. |
51 | |
52 | This technique has several issues which are discussed in |
53 | L<Catalyst::Plugin::Session::State::URI/CAVEATS>. |
54 | |
bfa4f9cc |
55 | =head2 Server-Side Behavior |
021ca3a3 |
56 | |
bfa4f9cc |
57 | When the server receives the session ID it can then look this key up in a |
021ca3a3 |
58 | database of some sort. For example the database can contain a shopping cart's |
59 | contents, user preferences, etc. |
60 | |
61 | =head1 USING SESSIONS |
62 | |
63 | In L<Catalyst>, the L<Catalyst::Plugin::Session> plugin provides an API for |
64 | convenient handling of session data. This API is based on the older, less |
65 | flexible and less reliable L<Catalyst::Plugin::Session::FastMmap>. |
66 | |
67 | The plugin is modular, and requires backend plugins to be used. |
68 | |
69 | =head2 State Plugins |
70 | |
71 | State plugins handle session ID persistence. For example |
72 | L<Catalyst::Plugin::Session::State::Cookie> creates a cookie with the session |
73 | ID in it. |
74 | |
75 | These plugins will automatically set C<< $c->sessionid >> at the begining of |
76 | the request, and automatically cause C<< $c->sessionid >> to be saved by the |
77 | client at the end of the request. |
78 | |
79 | =head2 Store Plugins |
80 | |
81 | The backend into which session data is stored is provided by these plugins. For |
82 | example, L<Catalyst::Plugin::Session::Store::DBI> uses a database table to |
83 | store session data, while L<Catalyst::Plugin::Session::Store::FastMmap> uses |
84 | L<Cache::FastMmap>. |
85 | |
86 | =head2 Configuration |
87 | |
88 | First you need to load the appropriate plugins into your L<Catalyst> |
89 | application: |
90 | |
91 | package MyApp; |
92 | |
93 | use Catalyst qw/ |
94 | Session |
95 | Session::State::Cookie |
96 | Session::Store::File |
97 | /; |
98 | |
99 | This loads the session API, as well as the required backends of your choice. |
100 | |
101 | After the plugins are loaded they need to be configured. This is done according |
2cf2efb9 |
102 | to L<Catalyst::Manual::Cookbook/Configure_your_application>. |
021ca3a3 |
103 | |
bfa4f9cc |
104 | Each backend plugin requires its own configuration options (with most plugins |
021ca3a3 |
105 | providing sensible defaults). The session API itself also has configurable |
106 | options listed in L<Catalyst::Plugin::Session/CONFIGURATION>. |
107 | |
108 | For the plugins above we don't need any configuration at all - they should work |
5104edbd |
109 | out of the box, but suppose we did want to change some things around, it'll |
bfa4f9cc |
110 | look like this: |
021ca3a3 |
111 | |
9a50355f |
112 | MyApp->config( 'Plugin::Session' => { |
021ca3a3 |
113 | cookie_name => "my_fabulous_cookie", |
114 | storage => "/path/to/store_data_file", |
115 | }); |
116 | |
117 | =head2 Usage |
118 | |
119 | Now, let's say we have an online shop, and the user is adding an item to the |
120 | shopping cart. |
121 | |
122 | Typically the item the user was viewing would have a form or link that adds the |
123 | item to the cart. |
124 | |
125 | Suppose this link goes to C</cart/add/foo_baz/2>, meaning that we want two |
126 | units of the item C<foo_baz> to be added to the cart. |
127 | |
128 | Our C<add> action should look something like this: |
129 | |
130 | package MyApp::Controller::Cart; |
131 | |
132 | sub add : Local { |
133 | my ( $self, $c, $item_id, $quantity ) = @_; |
134 | $quantity ||= 1; |
135 | |
136 | if ( $c->model("Items")->item_exists($item_id) ) { |
137 | $c->session->{cart}{$item_id} += $quantity; |
138 | } else { |
139 | die "No such item"; |
140 | } |
141 | } |
142 | |
143 | The way this works is that C<< $c->session >> always returns a hash reference |
144 | to some data which is stored by the storage backend plugin. The hash reference |
145 | returned always contains the same items that were in there at the end of the |
146 | last request. |
147 | |
148 | All the mishmash described above is done automatically. First, the method looks |
149 | to see if a session ID is set. This session ID will be set by the State plugin |
150 | if appropriate, at the start of the request (e.g. by looking at the cookies |
151 | sent by the client). |
152 | |
bfa4f9cc |
153 | If a session ID is set, the store will be asked to retrieve the session |
021ca3a3 |
154 | data for that specific session ID, and this is returned from |
155 | C<< $c->session >>. This retrieval is cached, and will only happen once per |
156 | request, if at all. |
157 | |
bfa4f9cc |
158 | If a session ID is not set, a new one is generated, a new anonymous hash is |
021ca3a3 |
159 | created and saved in the store with the session ID as the key, and the |
160 | reference to the hash is returned. |
161 | |
162 | The action above takes this hash reference, and updates a nested hash within |
163 | it, that counts quantity of each item as stored in the cart. |
164 | |
bfa4f9cc |
165 | Any cart-listing code can then look into the session data and use it to display |
021ca3a3 |
166 | the correct items, which will, of course, be remembered across requests. |
167 | |
168 | Here is an action some Template Toolkit example code that could be used to |
169 | generate a cart listing: |
170 | |
171 | sub list_cart : Local { |
172 | my ( $self, $c ) = @_; |
173 | |
174 | # get the cart data, that maps from item_id to quantity |
175 | my $cart = $c->session->{cart} || {}; |
176 | |
177 | # this is our abstract model in which items are stored |
178 | my $storage = $c->model("Items"); |
179 | |
180 | # map from item_id to item (an object or hash reference) |
181 | my %items = map { $_ => $storage->get_item($_) } keys %$cart; |
182 | |
183 | # put the relevant info on the stash |
184 | $c->stash->{cart}{items} = \%items; |
185 | $c->stash->{cart}{quantity} = $cart; |
186 | } |
187 | |
188 | And [a part of] the template it forwards to: |
189 | |
190 | <table> |
191 | |
192 | <thead> |
193 | <tr> |
194 | <th>Item</th> |
195 | <th>Quantity</th> |
196 | <th>Price</th> |
197 | <th>remove</th> |
198 | </tr> |
199 | </thead> |
200 | |
201 | <tbody> |
202 | [%# the table body lists all the items in the cart %] |
203 | [% FOREACH item_id = cart.items.keys %] |
204 | |
bfa4f9cc |
205 | [%# each item has its own row in the table %] |
021ca3a3 |
206 | |
207 | [% item = cart.items.$item_id %] |
208 | [% quantity = cart.quantity.$item_id %] |
209 | |
210 | <tr> |
211 | <td> |
212 | [%# item.name is an attribute in the item |
213 | # object, as loaded from the store %] |
214 | [% item.name %] |
215 | </td> |
216 | |
217 | <td> |
218 | [%# supposedly this is part of a form where you |
219 | # can update the quantity %] |
220 | <input type="text" name="[% item_id %]_quantity" |
221 | value="[% quantity %]" /> |
222 | </td> |
223 | |
224 | <td> $ [% item.price * quantity %] </td> |
225 | |
226 | <td> |
227 | <a href="[% c.uri_for('/cart/remove') %]/[% item_id %]"> |
228 | <img src="/static/trash_can.png" /> |
229 | </a> |
230 | </td> |
231 | [% END %] |
232 | <tbody> |
233 | |
234 | <tfoot> |
235 | <tr> |
236 | <td colspan="2"> Total: </td> |
237 | <td> |
238 | [%# calculate sum in this cell - too |
239 | # much headache for a tutorial ;-) %] |
240 | </td> |
241 | <td> |
242 | <a href="[% c.uri_for('/cart/empty') %]">Empty cart</a> |
243 | </td> |
244 | </tr> |
245 | </tfoot> |
246 | |
247 | </table> |
248 | |
249 | As you can see the way that items are added into C<< $c->session->{cart} >> is |
250 | pretty simple. Since C<< $c->session >> is restored as necessary, and contains |
251 | data from previous requests by the same client, the cart can be updated as the |
252 | user navigates the site pretty transparently. |
253 | |
254 | =head1 SECURITY ISSUES |
255 | |
256 | These issues all relate to how session data is managed, as described above. |
257 | These are not issues you should be concerned about in your application code, |
258 | but are here for their educational value. |
259 | |
260 | =head2 (Not) Trusting the Client |
261 | |
bfa4f9cc |
262 | In order to avoid the overhead of server-side data storage, the session data can |
021ca3a3 |
263 | be included in the cookie itself. |
264 | |
265 | There are two problems with this: |
266 | |
267 | =over 4 |
268 | |
269 | =item 1 |
270 | |
271 | The user can change the data. |
272 | |
273 | =item 2 |
274 | |
275 | Cookies have a 4 kilobyte size limit. |
276 | |
021ca3a3 |
277 | The size limit is of no concern in this section, but data changing is. In the |
278 | database scheme the data can be trusted, since the user can neither read nor |
279 | write it. However, if the data is delegated to the user, then special measures |
280 | have to be added for ensuring data integrity, and perhaps secrecy too. |
281 | |
282 | This can be implemented by encrypting and signing the cookie data, but this is |
283 | a big headache. |
284 | |
285 | =back |
286 | |
287 | =head2 Session Hijacking |
288 | |
289 | What happens when client B says "I'm client A"? Well, basically, the server |
290 | buys it. There's no real way around it. |
291 | |
292 | The solution is to make "I'm client A" a difficult thing to say. This is why |
293 | session IDs are randomized. If they are properly randomized, session IDs are so |
294 | hard to guess that they must be stolen instead. |
295 | |
296 | This is called session hijacking. There are several ways one might hijack |
297 | another user's session. |
298 | |
299 | =head3 Cross Site Scripting |
300 | |
301 | One is by using cross site scripting attacks to steal the cookie data. In |
302 | community sites, where users can cause the server to display arbitrary HTML, |
303 | they can use this to put JavaScript code on the server. |
304 | |
305 | If the server does not enforce a strict subset of tags that may be used, the |
bfa4f9cc |
306 | malicious user could use this code to steal the cookies (there is a JavaScript |
021ca3a3 |
307 | API that lets cookies be accessed, but this code has to be run on the same |
308 | website that the cookie came from). |
309 | |
310 | =head3 Social Engineering |
311 | |
312 | By tricking a user into revealing a URI with session data embedded in it (when |
313 | cookies are not used), the session ID can also be stolen. |
314 | |
315 | Also, a naive user could be tricked into showing the cookie data from the |
316 | browser to a malicious user. |
317 | |
318 | =head1 AUTHOR |
319 | |
320 | Yuval Kogman E<lt>nothingmuch@woobling.orgE<gt> |
321 | |
322 | =cut |