https://github.com/inventiLT/Pocheshiro.git
git clone 'https://github.com/inventiLT/Pocheshiro.git'
(ql:quickload :inventiLT.Pocheshiro)
Pocheshiro is a Clojure wrapper for the Apache Shiro security library tailored for use with Ring and Compojure.
Pocheshiro must be run in a servlet container (sorry, no http-kit because Shiro depends on the servlet specification).
If you're coming from Java-land, you've probably used either Spring Security or Apache Shiro.
Jumping from one of these straight to Friend, which is the mainstream Clojure option for securing web applications, may be too big of a leap. After all, security is a sensitive part of any application.
[pocheshiro "0.1.1"]
Pocheshiro is a thin wrapper so you will need to use some of the Shiro classes directly. Let's see what we're going to need in order to provided a username/password authentication with role and permission authorization capabilities.
In order to register new users you will have to store their identifying attributes (principal in Shiro parlance) and passwords (credentials). You can choose either Bcrypt or salted and iterated SHA for your password hashing needs.
(require '[pocheshiro.core :as shiro])
(def users (atom {}))
(def bcrypted-passwords (shiro/bcrypt-passwords {}))
(defn register-user! [{:keys [username password
roles permissions]}]
(let [hashed-pwd (.encryptPassword bcrypted-passwords password)]
(swap! users assoc username {:username username
:roles (set roles)
:permissions (set permissions)
:password-hash hashed-pwd})))
(register-user! {:username "john"
:password "secret"
:roles [:manager]
:permissions [:fire-underlings]})
Realms are the core part of Shiro. According to Shiro docs, a Realm is a
component that can access application-specific security data such as users, roles, and permissions. The Realm translates this application-specific data into a format that Shiro understands so Shiro can in turn provide a single easy-to-understand Subject programming API no matter how many data sources exist or how application-specific your data might be.
Basically, a realm takes user attributes and credentials and looks up the authentication/authorization info for the user.
Pocheshiro provides a way to concisely define realms via the defrealm-fn
function together with a stub handling username/password authentication -
username-password-realm
.
(def inmemory-realm
(shiro/username-password-realm
:passwords bcrypted-passwords
:get-authentication
#(if-let [user (get @users (.getPrincipal %))]
{:principal (:username user)
:credentials (:password-hash user)})
:get-authorization
#(if-let [user (get @users (.getPrimaryPrincipal %))]
(select-keys user [:roles :permissions]))))
The wiring of the Shiro library is concentrated in the SecurityManager. Here you will set all of the settings, add listeners and other options provided by Shiro.
Web applications will need an instance of WebSecurityManager
.
(import 'org.apache.shiro.web.mgt.DefaultWebSecurityManager)
(require '[compojure.handler :as handler])
(def security-manager (DefaultWebSecurityManager. [inmemory-realm]))
(declare main-routes)
(def app
(shiro/wrap-security
(shiro/wrap-principal (handler/site #'main-routes))
{:security-manager-retriever (constantly security-manager)}))
Security manager is retrieved in a function in order to facilitate extracting
it from the system
contained in the request, Stuart Sierra
style.
First, import the type of token we'll be using for authentication.
UsernamePasswordToken
is supported by the username-password-realm
by
default.
(import 'org.apache.shiro.authc.UsernamePasswordToken)
We'll need to define routes for logging in and out. Notice the
redirect-after-login!
call which happens after login!
. This call will send
a redirect to the URI visited before being redirected to the login page (or the
/index page if you went straight to the login):
(require '[compojure.core :refer [GET POST context defroutes]])
(defroutes main-routes
(POST "/login" {:keys [params] :as request}
(shiro/login! (UsernamePasswordToken. (:username params)
(:password params)))
(shiro/redirect-after-login! request "/index"))
(GET "/logout" request
(shiro/logout!)
{:status 200, :body "You have been logged out!"})
...
(require '[pocheshiro.core :as shiro :refer
[enforce wrap-enforce authorized authenticated or*]])
(defroutes manager-routes ... )
(defroutes main-routes
...
(GET "/managers-only" request
(wrap-enforce (authorized {:roles #{:manager}})
manager-routes)
; We can extract the primary principal from the request if the
; `wrap-principal` middleware was used when defining the Ring handler.
; In this case `user` is equal to the username provided to the
; `register-user!` above (as that's what we return from the realm).
(GET "/anonymous-only-at-midnight" [{:keys [user]}]
(enforce (or* authenticated
(authorized #(= dates/midnight (dates/now))))
(views/at-midnight user))))
In order to use Pocheshiro you will need to run your Ring/Compojure app as a
servlet. If you already produce a WAR artifact, you don't need to modify
anything, but if you run in a Jetty
, you will need to configure it properly:
(import '[org.eclipse.jetty.servlet ServletContextHandler ServletHolder])
(require '[ring.adapter.jetty :as jetty]
'[ring.util.servlet :as servlet])
(defn run [handler]
(jetty/run-jetty handler
{:port 8080
:configurator
#(.setHandler %
(doto (ServletContextHandler.)
(.addServlet (ServletHolder. (servlet/servlet handler)) "/*")))})))
Licensed under MIT License.