In this tutorial, we'll walk through creating a basic login system using Hunchentoot, a web server written in Common Lisp. This is perfect for beginners who are just starting with web development in Lisp. We'll cover setting up a server, handling sessions, and creating protected routes.
Before we begin, make sure you have:
First, let's set up our project and load the necessary libraries:
(ql:quickload '(:hunchentoot :spinneret))
(defpackage :login-example
(:use :cl :hunchentoot :spinneret))
(in-package :login-example)
Now, let's create functions to start and stop our server:
(defvar *server*)
(defun start-server (&key (port 8080))
(setf *server* (make-instance 'easy-acceptor :port port))
(start *server*))
(defun stop-server ()
(stop *server*))
For this example, we'll use a simple in-memory user database:
(defvar *login* '(:user "foo" :password "bar"))
We'll create a function to check if a user is logged in:
(defun loggedin-p ()
(and (session-value 'user)
(session-value 'loggedin)))
We'll use Spinneret to create HTML templates for our login and welcome pages:
(defun login-page (&key (error nil))
(with-html-string
(:html
(:head (:title "Login"))
(:body
(when error
(:p (:style "color: red;") "Invalid username or password"))
(:form :method "post" :action "/"
(:p "Username: " (:input :type "text" :name "user"))
(:p "Password: " (:input :type "password" :name "password"))
(:p (:input :type "submit" :value "Log In")))))))
(defun welcome-page (username)
(with-html-string
(:html
(:head (:title "Welcome"))
(:body
(:h1 (format nil "Welcome, ~A!" username))
(:p "You are logged in.")
(:a :href "/logout" "Log out")))))
Now, let's create our main handler that will manage both GET and POST requests:
(define-easy-handler (home :uri "/") ()
(start-session)
(ecase (request-method*)
(:get (if (loggedin-p)
(welcome-page (session-value 'user))
(login-page)))
(:post (let ((user (post-parameter "user"))
(password (post-parameter "password")))
(if (and (string= user (getf *login* :user))
(string= password (getf *login* :password)))
(progn
(setf (session-value 'user) user)
(setf (session-value 'loggedin) t)
(welcome-page user))
(login-page :error t))))))
Let's add a handler for logging out:
(define-easy-handler (logout :uri "/logout") ()
(setf (session-value 'user) nil)
(setf (session-value 'loggedin) nil)
(redirect "/"))
To run our application, we simply call:
(start-server)
Now, you can visit http://localhost:8080
in your browser to see the login page.
Let's break down some key points:
Session Management: We use start-session
at the beginning of our main handler. This is safe because if a session already exists, it won't create a new one.
Login Logic: We check the credentials against our simple in-memory database and set session values if they match.
Logout Handling: We clear session values and redirect to the home page.
For a production application, you'd want to implement several security enhancements:
What if we want to create routes that are only accessible to logged-in users? We can create a macro for this:
(defmacro define-protected-handler ((name &key uri) &body body)
`(define-easy-handler (,name :uri ,uri) ()
(if (loggedin-p)
(progn ,@body)
(redirect "/"))))
Now we can easily create protected routes:
(define-protected-handler (user-profile :uri "/profile")
(with-html-string
(:html
(:head (:title "User Profile"))
(:body
(:h1 "Your Profile")
(:p "Welcome to your profile page, " (session-value 'user) "!")
(:a :href "/" "Back to Home")))))
For more flexibility, we can create a middleware-like function:
(defun require-login (next)
(lambda ()
(if (loggedin-p)
(funcall next)
(redirect "/"))))
(defmacro define-protected-handler-with-middleware ((name &key uri) &body body)
`(define-easy-handler (,name :uri ,uri) ()
(funcall (require-login (lambda () ,@body)))))
This approach uses higher-order functions to wrap our handler logic with authentication checks.
funcall
here?In the define-protected-handler-with-middleware
macro, we use funcall
because:
require-login
returns a function, not the result of calling a function.funcall
is used in Lisp to call a function object.This pattern demonstrates the power of Lisp's functional programming features in creating flexible web application structures.
This tutorial introduced you to building a simple login system with Hunchentoot in Common Lisp. We covered setting up a server, managing sessions, creating handlers, and even touched on more advanced concepts like creating protected routes and using higher-order functions for middleware-like functionality.
Remember, this is a basic example and should be enhanced with proper security measures for any production use. Happy Lisp coding!