Java Web Application Security - Part III: Apache Shiro Login Demo
A couple weeks ago, I wrote a tutorial on how to implement security with Spring Security. The week prior, I wrote a similar tutorial for Java EE 6. This week, I'd like to show you how to implement the same features using Apache Shiro. As I mentioned in previous articles, I'm writing this because I told the audience at April's UJUG that I would publish screencasts of the demos.
Today, I've finished the third screencast showing how to implement security with Apache Shiro. Below is the presentation (with the screencast embedded on slide 22) as well as a step-by-step tutorial.
Apache Shiro Login Tutorial
- Download and Run the Application
- Implement Basic Authentication
- Force SSL
- Implement Form-based Authentication
- Store Users in a Database
- Summary
Download and Run the Application
To begin, download the application you'll be implementing security in. This app is a stripped-down version of the Ajax Login application I wrote for my article on Implementing Ajax Authentication using jQuery, Spring Security and HTTPS. You'll need Java 6 and Maven installed to run the app. Run it using mvn jetty:run and open http://localhost:8080 in your browser. You'll see it's a simple CRUD application for users and there's no login required to add or delete users.
Implement Basic Authentication
The first step is to protect the list screen so people have to login to view users. To do this, you'll need to create a shiro.ini file Shiro's configuration. Create src/main/resources/shiro.ini and populate it with the contents below:
[main] [users] admin = admin, ROLE_ADMIN [roles] ROLE_ADMIN = * [urls] /app/users = authcBasic
You can see this file has four sections and is pretty simple to read and understand. For more information about what each section is for, check out Shiro's configuration documentation.
Next, open src/main/webapp/WEB-INF/web.xml and add Shiro's IniShiroFilter:
<filter> <filter-name>securityFilter</filter-name> <filter-class>org.apache.shiro.web.servlet.IniShiroFilter</filter-class> <!-- no init-param means load the INI config from classpath:shiro.ini --> </filter>
And add its filter-mapping just after the rewriteFilter in the filter-mappings section (order is important!):
<filter-mapping> <filter-name>rewriteFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <filter-mapping> <filter-name>securityFilter</filter-name> <url-pattern>/*</url-pattern> <dispatcher>REQUEST</dispatcher> <dispatcher>FORWARD</dispatcher> <dispatcher>INCLUDE</dispatcher> </filter-mapping>
Then add Shiro's core and web dependencies to your pom.xml:
<dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> <version>1.1.0</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-web</artifactId> <version>1.1.0</version> </dependency>
At this point, if you restart Jetty (Ctrl+C and jetty:run again), you should be prompted to login when you click on the "Users" tab. Enter admin/admin to login. Apache Shiro is easier to configure than Spring Security out-of-the-box, mostly because it doesn't require XML.
After logging in, you can try to logout by clicking the "Logout" link in the top-right corner. This calls a LogoutController with the following code that logs the user out.
public void logout(HttpServletResponse response) throws ServletException, IOException { request.getSession().invalidate(); response.sendRedirect(request.getContextPath()); }
NOTE: Shiro doesn't currently have a way to logout with its API. However, it will be added in the 1.2 release.
You'll notice that clicking this link doesn't log you out, even though the session is invalidated. The only way to logout with basic authentication is to close the browser. In order to get the ability to logout, as well as to have more control over the look-and-feel of the login, you can implement form-based authentication. Before you implement form-based authentication, I'd like to show you how easy it is to force SSL with Apache Shiro.
Force SSL
Apache Shiro allows you to force SSL on a URL by simply adding "ssl[port]" to a URL in the [urls] section. If you don't specify the port, it will use the default port (443). I'm not sure if it allows you to switch back to http like Spring Security's requires-channel, but I don't think it does. Modify the URLs section of your shiro.ini to have the following:
[urls] /app/users = ssl[8443],authc
In order for this to work, you have to configure Jetty to listen on an SSL port. Add the following just after the jetty-maven-plugin's </webAppConfig> element in your pom.xml:
<connectors> <connector implementation="org.eclipse.jetty.server.nio.SelectChannelConnector"> <forwarded>true</forwarded> <port>8080</port> </connector> <connector implementation="org.eclipse.jetty.server.ssl.SslSelectChannelConnector"> <forwarded>true</forwarded> <port>8443</port> <maxIdleTime>60000</maxIdleTime> <keystore>${project.build.directory}/ssl.keystore</keystore> <password>appfuse</password> <keyPassword>appfuse</keyPassword> </connector> </connectors>
The keystore must be generated for Jetty to start successfully, so add the keytool-maven-plugin just above the jetty-maven-plugin in pom.xml.
<plugin> <groupId>org.codehaus.mojo</groupId> <artifactId>keytool-maven-plugin</artifactId> <version>1.0</version> <executions> <execution> <phase>generate-resources</phase> <id>clean</id> <goals> <goal>clean</goal> </goals> </execution> <execution> <phase>generate-resources</phase> <id>genkey</id> <goals> <goal>genkey</goal> </goals> </execution> </executions> <configuration> <keystore>${project.build.directory}/ssl.keystore</keystore> <dname>cn=localhost</dname> <keypass>appfuse</keypass> <storepass>appfuse</storepass> <alias>appfuse</alias> <keyalg>RSA</keyalg> </configuration> </plugin>
Now if you restart Jetty, go to http://localhost:8080 and click on the "Users" tab, you'll be prompted to accept the Untrusted Certificate and then redirected to https://localhost:8443/users after logging in.
Now let's look at how to have more control over the look-and-feel of the login screen, as well as how to make logout work with form-based authentication.
Implement Form-based Authentication
To change from basic to form-based authentication, you simply have to add a few lines to shiro.ini. First of all, since I'd rather not change the name of the input elements in login.jsp, override the default names in the [main] section:
# name of request parameter with username; if not present filter assumes 'username' authc.usernameParam = j_username # name of request parameter with password; if not present filter assumes 'password' authc.passwordParam = j_password authc.failureKeyAttribute = shiroLoginFailure
Then change the [urls] section to filter on login.jsp and use "authc" instead of "authcBasic":
[urls] # The /login.jsp is not restricted to authenticated users (otherwise no one could log in!), but # the 'authc' filter must still be specified for it so it can process that url's # login submissions. It is 'smart' enough to allow those requests through as specified by the # shiro.loginUrl above. /login.jsp = authc /app/users = ssl[8443],authc
Then change login.jsp so the form's action is blank (causing it to submit to itself) instead of j_security_check:
<form action="" id="loginForm" method="post">
Now, restart Jetty and you should be prompted to login with this JSP instead of the basic authentication dialog.
Store Users in a Database
To store your users in a database instead of file, you'll need to add a few settings to shiro.ini to define your database and tables to use. Open src/main/resources/shiro.ini and add the following lines under the [main] section.
jdbcRealm=org.apache.shiro.realm.jdbc.JdbcRealm #jdbcRealm.permissionsLookupEnabled=false # If not filled, subclasses of JdbcRealm assume "select password from users where username = ?" jdbcRealm.authenticationQuery = select user_pass from users where user_name = ? # If not filled, subclasses of JdbcRealm assume "select role_name from user_roles where username = ?" jdbcRealm.userRolesQuery = select role_name from users_roles where user_name = ? ds = com.mysql.jdbc.jdbc2.optional.MysqlDataSource ds.serverName = localhost ds.user = root ds.databaseName = appfuse jdbcRealm.dataSource = $ds
This configuration is similar to what I did with the Java EE 6 tutorial where I'm pointing to a database other than the H2 instance that's used by the application. I believe Shiro can talk to a DAO like Spring Security, but I have yet to explore that option.
While you're at it, add the following lines to enable password encryption.
sha256Matcher = org.apache.shiro.authc.credential.Sha256CredentialsMatcher jdbcRealm.credentialsMatcher = $sha256Matcher
You'll need to install MySQL for this to work. After installing it, you should be able to create an "appfuse" database using the following command:
mysql -u root -p -e 'create database appfuse'
Then create the tables necessary and populate it with an 'admin' user. Login using "mysql -u root -p appfuse" and execute the following SQL statements:
create table users ( user_name varchar(30) not null primary key, user_pass varchar(100) not null ); create table user_roles ( user_name varchar(30) not null, role_name varchar(30) not null, primary key (user_name, role_name) ); insert into users values ('admin', '22f256eca1f336a97eef2b260773cb0d81d900c208ff26e94410d292d605fed8'); insert into user_roles values ('admin', 'ROLE_ADMIN');
Now if you restart Jetty, you should be able to login with admin/adminjdbc and view the list of users.
Summary
In this tutorial, you learned how to implement authentication using Apache Shiro 1.1.0. I don't have a lot of experience with Apache Shiro, but I was able to get the basics working without too much effort. This tutorial doesn't show how to do Remember Me because I couldn't figure it out in 5 minutes, which means I have 5 more minutes before it fails the 10-minute test.
Shiro was formerly named JSecurity and has been an Apache project for less than a year. It seems to be more targeted towards non-web use, so its certainly something to look at if you're more interested in cryptography or non-web apps. I think there's a good chance this project will continue to grow and be used more as more developers learn about it. The Apache brand certainly doesn't hurt.
I didn't include a slide about the limitations I found with Shiro, mostly because I haven't used it much. I've used Java EE and Spring Security for several years. The main limitation I found was the lack of documentation, but I've heard it's improving rapidly.
In the next couple weeks, I'll post a Part IV on implementing programmatic login using the APIs of Java EE 6, Spring Security and Apache Shiro. I'll be presenting this topic at Jazoon as well as the long-form version (with hacking) at ÜberConf. Hopefully I'll see you at one of those conferences.
Update: Thanks to help from Les Hazlewood, I've figured out how to implement Remember Me with Apache Shiro. In the [urls] section of shiro.ini, the second url (shown below) says to Shiro "In order to visit the /app/users URL, you must be connecting via SSL on port 8443 and you must also be authenticated."
/app/users = ssl[8443],authc
Remembered users are not authenticated because their identity hasn't been proven during the current session. What I want Shiro to say is "In order to visit the /app/users URL, you must be connecting via SSL on 8443 and you must also be a known user. If you're not, you should login first." Where a known user is someone who has a recognized identity and has either authenticated during the current session or is known via RememberMe from a previous session. The documentation gives a good example with Amazon.com for why Shiro makes this distinction. It allows more control (usually necessary), but you can relax the control as you see fit.
So, to relax my configuration a bit to match what I want (known users), I updated shiro.ini's [urls] section to be as follows:
/app/users = ssl[8443],user
The key is that the /app/users url is now protected with the more relaxed user filter instead of the authc filter. However, you would typically want an account profile page (or credit card information page, or similar) protected with the authc filter instead to guarantee proof of identity for those sensitive operations.
Hi Matt!
This is a great write up and video - thanks so much for taking the time to do this, as I'm sure it will only continue to help the JVM community further.
I wanted to clarify just a couple of things:
Apache Shiro not more targeted at non-web use. We support web apps as first-class citizens equally as well as non-web apps, and actually, probably more work goes into our web support code than the core code base due to the variety of web scenarios that must be supported. In fact, most of Shiro deployments are in web applications (90%? 80%?). I don't think there is a single Shiro team member that does not deploy Shiro in a web app.
The reason is that, if we really wanted to 'get it right' where things 'just work', the fundamental design should work equally well in both environments. Once the thought had been put into a transport-orthogonal foundation, adding a web layer was really easy.
This is why Shiro integrates so well into any web framework, be it standard JSP/JSTL all the way to Spring MVC, Tapestry, Wicket, Vaadin, Flex, or other 'robust' web MVC frameworks. It was very easy to support full web-app use cases because the founding design was so strong. If there are any deficiencies whatsoever, or if people would like a certain feature here and there, they'll find the Shiro dev team very receptive for this type of feedback. We're quite happy to help where we can!
Apache Shiro is essentially a direct parallel to Spring's breadth of scope (but for security, not DI): Spring is, at its core, a DI container that works equally well in non-web and web environments. But the very large majority of Spring apps are web apps. This is the same for Apache Shiro, but we're obviously focused on security and not DI/container use cases.
Second, a minor point - Apache Shiro has been a top-level project since September 2010, but we were in the incubator prior to that. We've been an Apache project for about 3 years now, with JSecurity existing another 5 before that.
And finally, a lot of work has been done this week to round-out the Shiro Reference Manual, with more work coming next week. Hopefully people will find it useful.
Anyway, once again, you've made a great video and write-up (in short time too! I wish I could create content this quickly! ;) ), so thanks for taking the time to show off Shiro!
Best regards,
Les HazlewoodApache Shiro PMC Chair
Posted by Les Hazlewood on May 27, 2011 at 11:19 PM MDT #
Hi Matt - thanks for the great post. I am just beginning a new Shiro project and this was an excellent kick-start!
I have some code to add to the logout controller. It should drop the remember-me cookie if one exists.
Posted by Eric P on July 19, 2011 at 08:28 PM MDT #
Posted by Kiran Tambe on January 31, 2012 at 12:52 PM MST #
Hello,I have a question about shiro:
and Permissions different I do not judge those different, Can you help me? thank you
Posted by cocoon on May 30, 2012 at 04:31 PM MDT #
@Kiran - I would checkout Shiro's 10 Minute Tutorial.
@cocoon - The 2nd permission (with ssl[8443]) forces SSL for the /app/users resource. If you have additional questions, I'd ask them on the Shiro Mailing Lists.
Posted by Matt Raible on June 06, 2012 at 12:05 PM MDT #
Posted by Mircea on March 25, 2013 at 12:37 AM MDT #
Posted by Richard Lee on March 29, 2014 at 02:15 PM MDT #
Hi,
Thanks a lot for such post on apache Shiro. I'm following this and created one sample application and implement Shiro on user login. But I could not successfully login as following error is coming everytime -
Can you please guide where the mistake could be there and how to resolve ?
Many thanks,
Jayanta P.
Posted by Jayanta Pramanik on August 11, 2014 at 02:11 PM MDT #
Posted by anant rao on September 05, 2014 at 06:11 PM MDT #
I am truly disappointed that I have to scratch information from google about the Remember Me and the UserFilter functionality.
In the documentation Les Hazlewood made a really BIG LONG point on the difference between authenticated and remembered, but not a single useful mention, that there is a UserFilter ... He even claimed that the functionality must implemented by the developer by accessing the RememberMe cookie :( I am really starting to get fed up with Shiro and this is my second project with it ...
Posted by Yos on December 05, 2014 at 07:15 PM MST #