Enterprise Java

Apache Shiro Part 1 – Basics

Apache Shiro, originally called JSecurity, is Java security framework. It was accepted and became Apache top level project in 2010. It aims to be powerful and easy to be used.

The project is in active development with active both users and developers mailing lists. Most important areas are documented on its web page. However, it has lot of gaps in documentation. It is not possible to learn to use most Shiro features from documentation alone. Luckily, the code is well commented and where I tried it was also easily readable.

Main Shiro features are:

  • authentication,
  • authorization,
  • cryptography,
  • session management.

In this article article we try and demonstrate various Shiro features. We start with simple unsecured web application, then we add security features into it. All code code is available in SimpleShiroSecuredApplication project on Github.
Unsecured Application

Unsecured application code is located in unsecured_application branch. Application represents an internal system for a fictional company. The company has four departments:

  • administrators,
  • repairmen,
  • scientists,
  • sales.

Each department has its own page. Each page contains buttons that are used by users to do their work. When user presses button, the work is done. For example, any repairmen can go to repairmen page and press button “Repair Refrigerator”. The button repairs refrigerator and shows success message.

Each user have his own account page. Account page contains user’s private data. Since unsecured application has no users yet, account page does nothing. Additionally, there is a page which contains all application functions. Everything anybody can do is possible to be done on this page.

Anybody can do any work and see all the pages.Sample application is run in test class RunWaitTest. It is not the best practice to use unit test this way, but it is not important now. If you run the class the application will be available at http://localhost:9180/simpleshirosecuredapplication/ url.

Adding Authentication

First, we have to verify user’s identity. The easiest and most standard authentication is done through user name and password. User fills in his user name and password and system verifies whether supplied values match with some user account.

For simplest applications, it is sufficient to store user names and passwords in plain text files. In more realistic scenario, user name and password are stored in persistent storage or the verification is done through other system such as ldap or active directory. Shiro supports all mentioned authentication methods. If out of the box authentication features are not sufficient, it is possible to extend the framework with own verification implementation.

In this chapter, we add user name and password based authentication into the application. User name and password are stored in static plain-text Shiro ini file.

New requirements: It is possible to log in and log out users. Application is to be accessible only for logged users. Successful log in redirects user into his own account page. All application functions and pages are still accessible to any logged user.

Needed steps:

  • add Apache Shiro,
  • create log in page,
  • configure users and password,
  • create log out page.

Add Apache Shiro

Shiro is integrated into web application through servlet filters. A filter intercepts requests and responses before servlet and performs all necessary tasks (such as identifying currently logged user, attaching logged user to current thread, … ). Default Shiro filters provide basic security features such as:

  • enforcing user log in,
  • enforcing ssl,
  • checking of page access rights.

If you want to learn more about default Shiro filters, good place to start is DefaultFilter enumeration. It lists all Shiro filters available by default. If those are not sufficient for your needs, you may create custom one.

We will use highly configurable IniShiroFilter. It reads Shiro configuration from ini file and initializes security framework. It does not perform any security checks. Permission checks, user login, protocol checking etc. are all delegated to either default or custom filters. IniShiroFilter only initialize them.

Ini configuration is described in both documentation and javadoc. Ini file configuration has four sections:

  • Section [main] contains Shiro initialization. Filters and custom objects are configured here.
  • Section [users] defines users, passwords and roles.
  • Section [roles] associates roles with permissions.
  • Section [urls] specifies access rights to application pages (urls). It is done by binding either default or custom filters to urls.

Add Apache Shiro dependency to pom.xml:

<properties>
    <shiro.version>1.1.0</shiro.version>
</properties>
<dependencies>
    <dependency>
        <groupid>org.apache.shiro</groupid>
        <artifactid>shiro-core</artifactid>
        <version>${shiro.version}</version>
    </dependency>
    <dependency>
        <groupid>org.apache.shiro</groupid>
        <artifactid>shiro-web</artifactid>
        <version>${shiro.version}</version>
    </dependency>
</dependencies>

Create Shiro.ini file and put it on classpath. Configure web.xml to call IniShiroFilter before each request:

<filter>
    <filter-name>ShiroFilter</filter-name>
    <filter-class>org.apache.shiro.web.servlet.IniShiroFilter</filter-class>
    <init-param>
        <param-name>configPath</param-name>
        <param-value>classpath:Shiro.ini</param-value>
    </init-param>
</filter>

<filter-mapping>
    <filter-name>ShiroFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

Create Login Page

Login page is simple html page with submit button, user name and password fields. Login functionality is handled by default Shiro authc filter. Authc filter allows url access only to logged in users. If the user is not logged in, filter will redirect him to login page.

Form on login page must have name ‘loginform’ and its submit method must be ‘post’. Create login.jsp page:

<form name="loginform" action="" method="post">
<table align="left" border="0" cellspacing="0" cellpadding="3">
    <tr>
        <td>Username:</td>
        <td><input type="text" name="user" maxlength="30"></td>
    </tr>
    <tr>
        <td>Password:</td>
        <td><input type="password" name="pass" maxlength="30"></td>
    </tr>
    <tr>
        <td colspan="2" align="left"><input type="checkbox" name="remember"><font size="2">Remember Me</font></td>
    </tr>
    <tr>
        <td colspan="2" align="right"><input type="submit" name="submit" value="Login"></td>
    </tr>
</table> 
</form>

Enable authc filter for all application pages:

[main] 
# specify login page
authc.loginUrl = /simpleshirosecuredapplication/account/login.jsp

# name of request parameter with username; if not present filter assumes 'username'
authc.usernameParam = user
# name of request parameter with password; if not present filter assumes 'password'
authc.passwordParam = pass
# does the user wish to be remembered?; if not present filter assumes 'rememberMe'
authc.rememberMeParam = remember

# redirect after successful login
authc.successUrl  = /simpleshirosecuredapplication/account/personalaccountpage.jsp

[urls]
# enable authc filter for all application pages
/simpleshirosecuredapplication/**=authc

Update: Shiro automatically performs context-relative path matching. As SimpleShiroSecuredApplication does not have context path set, full paths in Shiro.ini are necessary. However, if application context path would be /simpleshirosecuredapplication, then paths could be relative: e.g. simple /**=authc or /account/personalaccountpage.jsp.

As it is not safe to send non-encrypted user name and password through network, we should force ssl logins. Ssl filter does exactly that. It has an optional parameter: ssl port number. If the port parameter is omitted, it uses default ssl port 443.

Before configuring ssl in Shiro, we have to enable it on web server. How to do that depends on web server. We show how to enable it in Jetty. First, create keystore with self signed certificate:

keytool -genkey -keyalg RSA -alias jetty -keystore keystore -storepass secret -validity 360 -keysize 2048

Answer all questions and in the end press enter so the keystore password and key passwords are the same.

Second, add the keystore to the project and configure Jetty to use ssl. Java code is available in AbstractContainerTest class.

Now, it is possible to configure ssl filter in Shiro.ini:

[urls]
# force ssl for login page
/simpleshirosecuredapplication/account/login.jsp=ssl[8443],authc
# enable authc filter for the all application pages; as Shiro reads urls from up to down, must be last
/simpleshirosecuredapplication/**=authc

Configure Users and Password

SimpleShiroSecuredApplication is now available only for logged users. We now need to add some users so people can log in. Configuration is done in [users] section of Shiro.ini file. Format of section entries is:

username = password, roleName1, roleName2, ..., roleNameN

Following section creates seven users, all have the same password ‘heslo’:

[users]
administrator=heslo,Administrator
friendlyrepairmen=heslo,repairmen
unfriendlyrepairmen=heslo,repairmen
mathematician=heslo,scientist
physicien=heslo,scientist
productsales=heslo,sales
servicessales=heslo,sales

It is now possible to log in into the application. However, no reasonable error message is shown if the user makes a mistake. Moreover, passwords are stored in plain text file.

Error Handling

If user makes an error while logging in, Shiro redirects him back to the login page. The page looks exactly the same as before, which may confuse the user.

New requirement: show error message after each unsuccessful log in attempt.

Any time authentication error occurs, an exception is thrown. By default, form authentication filter catches the exception and stores its class name in request parameter. As we wish to customize data send to page, we have to extend FormAuthenticationFilter and override setFailureAttribute method:

@Override
protected void setFailureAttribute(ServletRequest request, AuthenticationException ae) {
  String message = ae.getMessage();
  request.setAttribute(getFailureKeyAttribute(), message);
}

Replace form authorization filter with VerboseFormAuthenticationFilter and configure it to use ‘simpleShiroApplicationLoginFailure’ request attribute to hold error information:

[main]
# replace form authentication filter with verbose filter 
authc = org.meri.simpleshirosecuredapplication.servlet.VerboseFormAuthenticationFilter
# request parameter with login error information; if not present filter assumes 'shiroLoginFailure'
authc.failureKeyAttribute=simpleShiroApplicationLoginFailure

Show error in login.jsp page:

<% 
  String errorDescription = (String) request.getAttribute("simpleShiroApplicationLoginFailure");
  if (errorDescription!=null) {
%>
Login attempt was unsuccessful: <%=errorDescription%>
<% 
  }
%>

Beware: real application should not show too much login error information. Message ‘Login attempt was unsuccessful.’ with no further info is usually enough.

Hashing Passwords

Current application version has all passwords stored in plain text. It is better to store and compare only password hashes.

Objects responsible for authentication are called realms. By default, Shiro uses IniRealm with pluggable password matcher to compare passwords. We will replace passwords in ini by their SHA-256 hashes and configure IniRealm to use SHA-256 hashing matcher.

Generate SHA-256 hash of password:

import org.apache.shiro.crypto.hash.Sha256Hash;

public static void main(String[] args) {
    Sha256Hash sha256Hash = new Sha256Hash("heslo");
    System.out.println(sha256Hash.toHex());
}

Configure Shiro to compare password hashes instead of password itself:

[main] 
# define matcher matching hashes instead of passwords
sha256Matcher = org.apache.shiro.authc.credential.HashedCredentialsMatcher
sha256Matcher.hashAlgorithmName=SHA-256

# enable matcher in iniRealm (object responsible for authentication)
iniRealm.credentialsMatcher = $sha256Matcher

Replace users passwords by password hashes:

[users]
administrator=56b1db8133d9eb398aabd376f07bf8ab5fc584ea0b8bd6a1770200cb613ca005, Administrator
friendlyrepairmen=56b1db8133d9eb398aabd376f07bf8ab5fc584ea0b8bd6a1770200cb613ca005, repairmen
unfriendlyrepairmen=56b1db8133d9eb398aabd376f07bf8ab5fc584ea0b8bd6a1770200cb613ca005, repairmen
mathematician=56b1db8133d9eb398aabd376f07bf8ab5fc584ea0b8bd6a1770200cb613ca005, scientist
physicien=56b1db8133d9eb398aabd376f07bf8ab5fc584ea0b8bd6a1770200cb613ca005,  scientist
productsales=56b1db8133d9eb398aabd376f07bf8ab5fc584ea0b8bd6a1770200cb613ca005,        sales
servicessales=56b1db8133d9eb398aabd376f07bf8ab5fc584ea0b8bd6a1770200cb613ca005,  sales

Note: it is not possible to specify salt in ini configuration.

Create Logout Page

Any application that have login feature should have also logout feature. Logging out current user with Shiro is easy, use command:

//acquire currently logged user and log him out
SecurityUtils.getSubject().logout();

Logout page then looks like this:

<%@ page import="org.apache.shiro.SecurityUtils" %>
<% SecurityUtils.getSubject().logout();%>
You have succesfully logged out. 

Adding Authorization

We conclude first part by adding authorization to the application. We begin by limiting users access to pages. No user should be able to see pages of other departments. This provides only partial security to the project, since the user is still able to use ‘all application functions’ page or edit URL in the browser to do any action. We will call it page level authorization.

Then, we limit users ability to perform actions themselves. Even if user opens ‘all application functions’ page or edits url in the browser, he will be allowed to perform only functions specific to his department. We will call it function level authorization.

New requirements: user is not able to see pages of departments he does not belong to. User is able to perform only his departmental functions. Only exception to previous rules is administrator, who can perform both administrative and repair functions.

Page Authorization

Page level authorization is done with roles filter. Parameter part of the filter may contain any number of roles. Logged user can access page only if he has all supplied roles.

As usual, roles filter is configured in Shiro.ini file:

[urls]
# force ssl for login page
/simpleshirosecuredapplication/account/login.jsp=ssl[8443],authc

# only users with some roles are allowed to use role-specific pages 
/simpleshirosecuredapplication/repairmen/**=authc, roles[repairman]
/simpleshirosecuredapplication/sales/**=authc, roles[sales]
/simpleshirosecuredapplication/scientists/**=authc, roles[scientist]
/simpleshirosecuredapplication/adminarea/**=authc, roles[Administrator]

# enable authc filter for the all application pages; as Shiro reads urls from up to down, must be last
/simpleshirosecuredapplication/**=authc

Test whether security works: log in as any sales user, click home, click ‘repairmen page’ link. You will see an ugly error.

We finish page authorization and replace error with redirect to an error page. Default Shiro filters have property unauthorizedUrl. In case of unauthorized access, the filter will redirect user to specified url.

[main]
# redirect to an error page if user does not have access rights
roles.unauthorizedUrl = /simpleshirosecuredapplication/account/accessdenied.jsp

accessdenied.jsp:

<body>
Sorry, you do not have access rights to that area.
</body>

Functions Authorization

All departmental pages are secured now. However, any user can still perform any function on ‘all application functions’ page. Moreover, any logged user can edit url and thus do any action. For example, if you log in as sales and put https://localhost:8443/simpleshirosecuredapplication/masterservlet?action=MANAGE_REPAIRMEN into url, the application will perform manage repairmen function too (then it will throw null pointer exception, but security breach was already done).

We assign unique permission to each function. They are in split into groups:

  • all permissions are in “functions” group,
  • all administrative permissions are in “manage” group,
  • all repair permissions are in “repair” group,
  • all sale permissions are in “sale” group,
  • all science permissions are in “science” group.

Shiro supports multi-level permissions represented as strings. Levels are separated with symbol ‘:’. E.g. “functions:manage:repairmen” has three levels: “functions”, “manage” and “repairman”. Multi-level permissions allow for easy permissions grouping. For example, science group belongs to functions group and contains three permissions:

  • functions:science:research,
  • functions:science:writearticle,
  • functions:science:preparetalk.

Actions verify logged user permissions before doing their job:

public String doIt() {
    String neededPermission = getNeededPermission();
    // acquire logged user and check permission
    if (SecurityUtils.getSubject().isPermitted(neededPermission))
        return "Function " + getName() + " run succesfully.";

    throw new UnauthorizedException("Logged user does not have " + neededPermission + " permission");
}

NOTE: Another way how to achieve the same goal is through annotations.

The PerformFunctionAndGoBackServlet servlet catches authorization exception and converts it into error message:

private String performAction(String actionName) {
    try {
        Actions action = findAction(actionName);
        String result = action == null ? null : action.doIt();
        log.debug("Performed function with result: " + result);
        return result;
    } catch (ShiroException ex) {
        log.debug("Function failed with " + ex.getMessage() + " message.");
        return "Error: " + ex.getMessage();
    }
}

Finally, we need to configure permissions to roles in Shiro.ini file. Shiro supports wildcards for multi-level permissions. Thus, we do not have to specify each departmental permission separately:

[roles]
# members of departments should be able to perform all departmental functions
sales=functions:sale:*
scientist=functions:science:*
repairman=functions:repair:*

# administrators are able to do all management functions and repair functions
Administrator=functions:manage:*,functions:repair:*

You can now try functions on ‘all application functions’ page. If logged user does not have required permission, an error message appears on top of the page. Moreover, if you log in as sales and try hacking https://localhost:8443/simpleshirosecuredapplication/masterservlet?action=MANAGE_REPAIRMEN, you will see an error message in the console (instead of success message).

End

Final application is available in ‘static_authentication_and_authorization’ branch on Github.

In the second part we will create custom realm and move users, passwords, roles and permissions from ini file to database. The third part is dedicated to Apache Shiro cryptography package.

Reference: Apache Shiro Part 1 – Basics from our JCG partner Maria Jurcovicova at the This is Stuff blog.

Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

3 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Akram
Akram
10 years ago

Hi , I downloaded this example and imported it in my STS, but when run it I got this error : SEVERE: Exception sending context initialized event to listener instance of class liquibase.integration.servlet.LiquibaseServletListener java.lang.RuntimeException: javax.naming.NameNotFoundException: Name jdbc is not bound in this Context at liquibase.integration.servlet.LiquibaseServletListener.contextInitialized(LiquibaseServletListener.java:159) at org.apache.catalina.core.StandardContext.listenerStart(StandardContext.java:4206) at org.apache.catalina.core.StandardContext.start(StandardContext.java:4705) at org.apache.catalina.core.ContainerBase.start(ContainerBase.java:1057) at org.apache.catalina.core.StandardHost.start(StandardHost.java:840) at org.apache.catalina.core.ContainerBase.start(ContainerBase.java:1057) at org.apache.catalina.core.StandardEngine.start(StandardEngine.java:463) at org.apache.catalina.core.StandardService.start(StandardService.java:525) at org.apache.catalina.core.StandardServer.start(StandardServer.java:754) at org.apache.catalina.startup.Catalina.start(Catalina.java:595) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:601) at org.apache.catalina.startup.Bootstrap.start(Bootstrap.java:289) at org.apache.catalina.startup.Bootstrap.main(Bootstrap.java:414) Caused by: javax.naming.NameNotFoundException: Name jdbc is not bound in this Context at org.apache.naming.NamingContext.lookup(NamingContext.java:770) at org.apache.naming.NamingContext.lookup(NamingContext.java:153) at org.apache.naming.SelectorContext.lookup(SelectorContext.java:152) at javax.naming.InitialContext.lookup(InitialContext.java:411) at liquibase.integration.servlet.LiquibaseServletListener.contextInitialized(LiquibaseServletListener.java:123) … 15 more… Read more »

roimekoi
roimekoi
10 years ago

I was able to deploy and the site is working
but after adding shiro.ini, all links will points to 404

Ali
Ali
8 years ago

Hi,
I downloaded the unsecured code, the problem is that when i run it this error occurs:

[ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.
1:compile (default-compile) on project SimpleSecuredApplication: Compilation fai
lure: Compilation failure:

can anybody help me with it?
thank you in advance.

Back to top button