Adding authentication and authorization

Một phần của tài liệu single page web applications (Trang 280 - 289)

Now that we’ve created the routes for performing CRUD actions on our objects, we should add authentication. We can do this the hard way and code it ourselves, or do it the easy way and take advantage of another Express middleware. Hmm. Think...

think, which to choose?

Load the routes module.

Use the configRoutes method to set up the routes.

7.4.1 Basic Authentication

Basic Authentication is an HTTP/1.0 and 1.1 standard for how a client provides a user- name and password when making a request; it’s commonly referred to as basic auth.

Remember that middleware is called in the order it’s added to the application, so if you want the application to authorize access to the routes, the middleware needs to be added before the router middleware. That’s easy enough to do as shown in listing 7.25.

Changes are shown in bold:

/*

* app.js - Express server with basic auth

*/

...

// --- BEGIN SERVER CONFIGURATION --- app.configure( function () {

app.use( express.bodyParser() );

app.use( express.methodOverride() );

app.use( express.basicAuth( 'user', 'spa' ) );

app.use( express.static( __dirname + '/public' ) );

app.use( app.router );

});

...

In this case, we’ve hard-coded the app to expect the user to be user and the pass- word to be spa. basicAuth also accepts a function as the third parameter, which can be used to provide more advanced mechanisms, like looking up user details in a database. That function should return true if the user is valid, and false when the user is not. When we restart the server and reload the browser, it should open an alert dialog that looks like fig-

ure 7.2, requiring a valid User Name and Password before allowing access.

If we enter the wrong password, it’ll keep prompting until we get it right. Pressing the Cancel button will bring us to a page that says Unauthorized.

Basic authentication is not recommended for use in a production app. It sends the credentials for every request in plain text—security experts call this a large attack vector.

And even if we use SSL (HTTPS) to encrypt the transmission, we only have one layer of security between the client and server.

Rolling your own authentication mechanism is getting passé these days. Many start- ups and even larger more established companies are using third-party authentication from the likes of Facebook or Google. There are many online tutorials showing how to integrate with these services; the Node.js middleware Passport can get you started.

Listing 7.25 Add basic auth to our server application—webapp/app.js

Figure 7.2 Chrome’s authentication dialog

257 Web sockets and Socket.IO

7.5 Web sockets and Socket.IO

Web sockets are an exciting technology that’s gaining widespread browser support.

Web sockets allow the client and server to maintain a persistent, lightweight, and bi- directional communication channel over a single TCP connection. This lets the cli- ent or server push messages in real-time without the overhead and latency of an HTTP request-response cycle. Prior to web sockets, developers had employed alter- nate—but less efficient—techniques to provide similar capabilities. These tech- niques included using Flash sockets; long-polling, where the browser opens a request to a server and then reinitializes the request when there’s a response or when the request times out; and server polling at close intervals (say, once per second).

The trouble with web sockets is that the specifications haven’t yet been finalized and older browsers will never support it. Socket.IO is a Node.js module that gracefully resolves the latter concern, as it provides browser-to-server messaging over web sockets if available, but will degrade to use other techniques if sockets aren’t available.

7.5.1 Simple Socket.IO

Let’s create a simple Socket.IO application that updates a counter on the server every second and pushes the current count to connected clients. We can install Socket.IO by updating our package.json as shown in listing 7.26. Changes are shown in bold:

{

"name" : "SPA",

"version" : "0.0.3",

"private" : true,

"dependencies" : {

"express" : "3.2.x", "socket.io" : "0.9.x"

} }

Now we can run npm install to ensure both Express and Socket.IO are installed.

Let’s add two files, a server application named webapp/socket.js and browser doc- ument named webapp/socket.html. Let’s start by building a server application that can serve static files and that has a timer that increments once per second. Since we know we are going to use Socket.IO, we will include that library too. Listing 7.27 shows our new socket.js server application:

/*

* socket.js - simple socket.io example

*/

/*jslint node : true, continue : true, devel : true, indent : 2, maxerr : 50, newcap : true, nomen : true, plusplus : true,

Listing 7.26 Installing Socket.IO—webapp/package.json

Listing 7.27 Begin the server application—webapp/socket.js

regexp : true, sloppy : true, vars : false, white : true

*/

/*global */

// --- BEGIN MODULE SCOPE VARIABLES --- 'use strict';

var countUp,

http = require( 'http' ), express = require( 'express' ), socketIo = require( 'socket.io' ), app = express(),

server = http.createServer( app ), countIdx = 0

;

// --- END MODULE SCOPE VARIABLES --- // --- BEGIN UTILITY METHODS --- countUp = function () {

countIdx++;

console.log( countIdx );

};

// --- END UTILITY METHODS --- // --- BEGIN SERVER CONFIGURATION --- app.configure( function () {

app.use( express.static( __dirname + '/' ) );

});

app.get( '/', function ( request, response ) { response.redirect( '/socket.html' );

});

// --- END SERVER CONFIGURATION --- // --- BEGIN START SERVER --- server.listen( 3000 );

console.log(

'Express server listening on port %d in %s mode', server.address().port, app.settings.env

);

setInterval( countUp, 1000 );

// --- END START SERVER ---

When we start the server—node socket.js—we see it logging a constantly increment- ing number in the terminal. Now, let’s create the webapp/socket.html shown in list- ing 7.28 to display this number. We’ll include jQuery because it makes grabbing the body tag simple:

<!doctype html>

<!-- socket.html - simple socket example -->

<html>

<head>

Listing 7.28 Create the browser document—webapp/socket.html

Create a module-scope count variable.

Create a utility to increment the count and log it.

Direct the application to serve static files from the current working directory.

Use the JavaScript setInterval function to call the countUp function every 1000 milliseconds.

259 Web sockets and Socket.IO

<script type="text/javascript"

src="http://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"

></script>

</head>

<body>

Loading...

</body>

</html>

We should now be able to load http://localhost:3000 and see the nearly blank page. Getting Socket.IO to send this information to the client takes just two additional lines in our server application, as shown in listing 7.29. Changes are shown in bold:

...

server = http.createServer( app ), io = socketIo.listen( server ), countIdx = 0

;

// --- END MODULE SCOPE VARIABLES --- // --- BEGIN UTILITY METHODS --- countUp = function () {

countIdx++;

console.log( countIdx );

io.sockets.send( countIdx );

};

// --- END UTILITY METHODS --- // --- BEGIN SERVER CONFIGURATION --- ...

The browser document requires just an additional six lines to enable Socket.IO, as shown in listing 7.30. Changes are shown in bold:

<!doctype html>

<!-- socket.html - simple socket example -->

<html>

<head>

<script type="text/javascript"

src="http://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"

></script>

<script src="/socket.io/socket.io.js"></script>

<script>

io.connect().on('message', function ( count ) { $('body').html( count );

});

</script>

</head>

<body>

Loading...

</body>

</html>

Listing 7.29 Add web sockets to the server application—webapp/socket.js

Listing 7.30 Add web sockets to the browser document—webapp/socket.html Instruct Socket.IO to listen using our HTTP server.

Send the count to all listening sockets.

The JavaScript file /socket.io/socket.io.js is provided by the Socket.IO installa- tion so there’s no need to create one; it’s also a “magical” file that doesn’t actually exist on the server, so don’t go looking for it. io.connect() returns a Socket.IO con- nection and the on method is similar to the bind method in jQuery, in that it tells it to watch for a certain kind of Socket.IO event. In this case, the event we’re looking for is any message coming over the connection. Then we use jQuery to update the body with the new count. You went looking for the socket.io.js file on the server, didn’t you?

If we open http://localhost:3000/ in a browser we should see the counter incre- menting. When we open another tab to the same location we should see another counter incrementing at the same number and rate because countIdx is a module- scope variable in the server application.

7.5.2 Socket.IO and messaging servers

When we use Socket.IO to route messages between clients and servers, we’re creating a messaging server. An example of another messaging server is Openfire, which serves messages using XMPP, the protocol used by Google Chat and Jabber. A messaging server must maintain connections to all clients so they can receive and respond to messages quickly. They should also minimize the size of the message by avoiding unnecessary data.

Traditional web servers such as Apache2 are poor messaging servers because they create and assign a process (or thread) for every connection, and each process must live for as long as its connection persists. As you might guess, after a few hundred or thousand connections, a web server will have all its resources consumed by all the processes used to service the connections. Apache2 was never designed for this; it was written as a content server, where the idea is to push data out as fast as possible in response to a request and then close the connection as fast as possible. For these types of uses, Apache2 is a great choice—just ask YouTube.

Node.js, by comparison, is an excellent messaging server. Thanks to its event model, it doesn’t create a process for every connection. Instead it does some bookkeep- ing when a connection is opened or closed, and some maintenance in between.

Therefore it can handle tens or hundreds of thousands of concurrent connections on modest hardware. Node.js doesn’t do any significant work until a messaging event—

like a request or a response—occurs on one or more of its open connections.

The number of messaging clients Node.js can handle depends on the actual work- load the server encounters. If the clients are relatively quiet and the server tasks are lightweight, the server can handle lots of clients. If the clients are chatty and the server tasks are heavier, the server can handle a lot less. It’s conceivable in a high-volume envi- ronment that a load balancer would route traffic between a cluster of Node.js servers that provides messaging, another cluster of Node.js servers that provides dynamic web content, and a cluster of Apache2 servers that provides static content.

There are many benefits of using Node.js over other messaging protocols such as XMPP. Here are just a few:

261 Web sockets and Socket.IO

■ Socket.IO makes cross-browser messaging in a web app almost trivial. We’ve used XMPP before for a production application. Trust us: it’s a lot more work just for the software.

■ We can avoid maintaining a separate server and configuration. Again, another big win.

■ We can work with native JSON protocol instead of a different language. XMPP is XML and requires sophisticated software to encode and decode.

■ We don’t have to worry (at least initially) about the dreaded “same domain” pol- icy that plagues other messaging platforms. This browser policy prevents con- tent from loading into browsers if it doesn’t come from the same server as the JavaScript that’s using it.

Now let’s look at a use of Socket.IO that’s sure to impress: dynamically updating our SPA. 7.5.3 Updating JavaScript with Socket.IO

One challenge with an SPA is ensuring the client software matches the server applica- tion. Imagine if Bobbie loaded our SPA into her browser, and five minutes later we update our server application. Now Bobbie has a problem, because our updated server communicates in a new data format, yet Bobbie’s SPA still expects the old. One way to resolve this situation is to force Bobbie to reload the entire SPA when it recog- nizes it’s out of date—say after we sent it a message announcing the server update. But we can get even fancier—we can selectively update only the JavaScript that has changed in the SPA without forcing the entire application to reload.

So how do we do this magical update? There are three parts to consider:

1 Watching the JavaScript files to detect when they’re modified.

2 Notifying the client the file has been updated.

3 Updating the client side JavaScript when it’s notified of the change.

The first part, detecting when the file is modified, can be accomplished using the native node file system module fs. The second is a matter of sending a Socket.IO noti- fication to the browser as described in the previous section, and updating the client can be accomplished through injecting a new script tag when receiving a notification.

We can update our server application from the last example as shown in listing 7.31.

Changes are shown in bold:

/*

* socket.js - dynamic JS loading example

*/

/*jslint node : true, continue : true, devel : true, indent : 2, maxerr : 50, newcap : true, nomen : true, plusplus : true, regexp : true, sloppy : true, vars : false, white : true

Listing 7.31 Update the server application to watch files—webapp/socket.js

*/

/*global */

// --- BEGIN MODULE SCOPE VARIABLES --- 'use strict';

var

setWatch,

http = require( 'http' ), express = require( 'express' ), socketIo = require( 'socket.io' ), fsHandle = require( 'fs' ), app = express(),

server = http.createServer( app ), io = socketIo.listen( server ), watchMap = {}

;

// --- END MODULE SCOPE VARIABLES --- // --- BEGIN UTILITY METHODS --- setWatch = function ( url_path, file_type ) {

console.log( 'setWatch called on ' + url_path );

if ( ! watchMap[ url_path ] ) {

console.log( 'setting watch on ' + url_path );

fsHandle.watchFile(

url_path.slice(1),

function ( current, previous ) { console.log( 'file accessed' );

if ( current.mtime !== previous.mtime ) { console.log( 'file changed' );

io.sockets.emit( file_type, url_path );

} } );

watchMap[ url_path ] = true;

} };

// --- END UTILITY METHODS --- // --- BEGIN SERVER CONFIGURATION --- app.configure( function () {

app.use( function ( request, response, next ) { if ( request.url.indexOf( '/js/' ) >= 0 ) {

setWatch( request.url, 'script' );

}

else if ( request.url.indexOf( '/css/' ) >= 0 ) { setWatch( request.url, 'stylesheet' );

} next();

});

app.use( express.static( __dirname + '/' ) );

});

app.get( '/', function ( request, response ) {

Load the file system module into fsHandle.

Instruct the file system module to watch the file for changes.

Trim the / from url_path, as the file system module needs the relative path from the current directory.

Compare the modified timestamps (mtime) of the current state of the file with the previous state of the file to see if it has been modified.

Emit a script or stylesheet event to the client containing the path of the file that changed.

Use custom middleware to set a watch for any statically served files.

If the requested file is in the js folder, consider it a script file.

If the requested file is in the css folder, consider it a stylesheet file.

263 Web sockets and Socket.IO

response.redirect( '/socket.html' );

});

// --- END SERVER CONFIGURATION --- // --- BEGIN START SERVER --- server.listen( 3000 );

console.log(

'Express server listening on port %d in %s mode', server.address().port, app.settings.env

);

// --- END START SERVER ---

Now that we’ve prepared the server application, let’s look at the client, starting with the JavaScript file we’ll be updating and then the index page. Our data file, webapp/js/

data.js, consists of one line assigning some text to a variable, as shown in listing 7.32:

var b = 'SPA';

Changes to our browser document need to be a little more substantial, as shown in listing 7.33. Changes are shown in bold:

<!doctype html>

<!-- socket.html - dynamic JS loading example -->

<html>

<head>

<script type="text/javascript"

src="http://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"

></script>

<script src="/socket.io/socket.io.js"></script>

<script id="script_a" src="/js/data.js"></script>

<script>

$(function () {

$( 'body' ).html( b );

});

io.connect('http://localhost').on( 'script', function ( path ) { $( '#script_a' ).remove();

$( 'head' ).append(

'<script id="script_a" src="' + path +

'"></scr' + 'ipt>' );

$( 'body' ).html( b );

});

</script>

</head>

<body>

Loading...

</body>

</html>

Listing 7.32 Create a data file—webapp/js/data.js

Listing 7.33 Update the browser document—webapp/socket.html

Include the JavaScript file we’ll be updating.

When the page first loads, set the HTML body to the value of the b variable set in the data.js file.

When we receive a script event emitted from the server, execute this function.

Remove the old script tag and inject a new one pointing to the updated JavaScript file. This will execute the JavaScript in that file and, in the case of webapp/js/

data.js, reload the b variable.

Replace the HTML body with the update value of the b variable.

Now we can make the magic happen. First, let’s start our server application (type node socket.js on the command line). Next, let’s open our browser document (webapp/

socket.html). We should see SPA in our browser body. Let’s then edit the webapp/js/

data.js file and change the value of SPA to the meaning of life is a rutabaga or some other equally pithy comment. When we return to the browser, we should see the display change (without reloading the browser) from SPA to the aforementioned pithy comment. There may be a delay of a few seconds because the watchFile com- mand can take that long to notice a file change.1

Một phần của tài liệu single page web applications (Trang 280 - 289)

Tải bản đầy đủ (PDF)

(433 trang)