Course – LSS – NPI (cat=Security/Spring Security)
announcement - icon

I just announced the new Learn Spring Security course, including the full material focused on the new OAuth2 stack in Spring Security:

>> CHECK OUT THE COURSE

1. Overview

In this article, we’ll look at Apache Shiro, a versatile Java security framework.

The framework is highly customizable and modular, as it offers authentication, authorization, cryptography and session management.

2. Dependency

Apache Shiro has many modules. However, in this tutorial, we use the shiro-core artifact only.

Let’s add it to our pom.xml:

<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-core</artifactId>
    <version>1.4.0</version>
</dependency>

The latest version of the Apache Shiro modules can be found on Maven Central.

3. Configuring Security Manager

The SecurityManager is the center piece of Apache Shiro’s framework. Applications will usually have a single instance of it running.

In this tutorial, we explore the framework in a desktop environment. To configure the framework, we need to create a shiro.ini file in the resource folder with the following content:

[users]
user = password, admin
user2 = password2, editor
user3 = password3, author

[roles]
admin = *
editor = articles:*
author = articles:compose,articles:save

The [users] section of the shiro.ini config file defines the user credentials that are recognized by the SecurityManager. The format is: principal (username) = password, role1, role2, …, role.

The roles and their associated permissions are declared in the [roles] section. The admin role is granted permission and access to every part of the application. This is indicated by the wildcard (*) symbol.

The editor role has all permissions associated with articles while the author role can only compose and save an article.

The SecurityManager is used to configure the SecurityUtils class. From the SecurityUtils we can obtain the current user interacting with the system and perform authentication and authorization operations.

Let’s use the IniRealm to load our user and role definitions from the shiro.ini file and then use it to configure the DefaultSecurityManager object:

IniRealm iniRealm = new IniRealm("classpath:shiro.ini");
SecurityManager securityManager = new DefaultSecurityManager(iniRealm);

SecurityUtils.setSecurityManager(securityManager);
Subject currentUser = SecurityUtils.getSubject();

Now that we have a SecurityManager that is aware of user credentials and roles defined in the shiro.ini file, let’s proceed to user authentication and authorization.

4. Authentication

In Apache Shiro’s terminologies, a Subject is any entity interacting with the system. It may either be a human, a script, or a REST Client.

Calling SecurityUtils.getSubject() returns an instance of the current Subject, that is, the currentUser.

Now that we have the currentUser Object, we can perform authentication on the supplied credentials:

if (!currentUser.isAuthenticated()) {               
  UsernamePasswordToken token                       
    = new UsernamePasswordToken("user", "password");
  token.setRememberMe(true);                        
  try {                                             
      currentUser.login(token);                       
  } catch (UnknownAccountException uae) {           
      log.error("Username Not Found!", uae);        
  } catch (IncorrectCredentialsException ice) {     
      log.error("Invalid Credentials!", ice);       
  } catch (LockedAccountException lae) {            
      log.error("Your Account is Locked!", lae);    
  } catch (AuthenticationException ae) {            
      log.error("Unexpected Error!", ae);           
  }                                                 
}

First, we check if the current user has not been authenticated already. Then we create an authentication token with the user’s principal (username) and credential (password).

Next, we attempt to login in with the token. If the supplied credentials are correct, everything should go fine.

There are different exceptions for different cases. It’s also possible to throw a custom exception that better suits the application requirement. This can be done by subclassing the AccountException class.

5. Authorization

Authentication is trying to validate the identity of a user while authorization is trying to control access to certain resources in the system.

Recall that we assign one or more roles to each user we have created in the shiro.ini file. Furthermore, in the roles section, we define different permissions or access levels for each role.

Now let’s see how we can use that in our application to enforce user access control.

In the shiro.ini file, we give the admin total access to every part of the system.

The editor has total access to every resource/operation regarding articles, and an author is restricted to just composing and saving articles only.

Let’s welcome the current user based on role:

if (currentUser.hasRole("admin")) {       
    log.info("Welcome Admin");              
} else if(currentUser.hasRole("editor")) {
    log.info("Welcome, Editor!");           
} else if(currentUser.hasRole("author")) {
    log.info("Welcome, Author");            
} else {                                  
    log.info("Welcome, Guest");             
}

Now, let’s see what the current user is permitted to do in the system:

if(currentUser.isPermitted("articles:compose")) {            
    log.info("You can compose an article");                    
} else {                                                     
    log.info("You are not permitted to compose an article!");
}                                                            
                                                             
if(currentUser.isPermitted("articles:save")) {               
    log.info("You can save articles");                         
} else {                                                     
    log.info("You can not save articles");                   
}                                                            
                                                             
if(currentUser.isPermitted("articles:publish")) {            
    log.info("You can publish articles");                      
} else {                                                     
    log.info("You can not publish articles");                
}

6. Realm Configuration

In real applications, we’ll need a way to get user credentials from a database rather than from the shiro.ini file. This is where the concept of Realm comes into play.

In Apache Shiro’s terminology, a Realm is a DAO that points to a store of user credentials needed for authentication and authorization.

To create a realm, we only need to implement the Realm interface. That can be tedious; however, the framework comes with default implementations that we can subclass from. One of these implementations is JdbcRealm.

We create a custom realm implementation that extends JdbcRealm class and overrides the following methods: doGetAuthenticationInfo(), doGetAuthorizationInfo(), getRoleNamesForUser() and getPermissions().

Let’s create a realm by subclassing the JdbcRealm class:

public class MyCustomRealm extends JdbcRealm {
    //...
}

For the sake of simplicity, we use java.util.Map to simulate a database:

private Map<String, String> credentials = new HashMap<>();
private Map<String, Set<String>> roles = new HashMap<>();
private Map<String, Set<String>> perm = new HashMap<>();

{
    credentials.put("user", "password");
    credentials.put("user2", "password2");
    credentials.put("user3", "password3");
                                          
    roles.put("user", new HashSet<>(Arrays.asList("admin")));
    roles.put("user2", new HashSet<>(Arrays.asList("editor")));
    roles.put("user3", new HashSet<>(Arrays.asList("author")));
                                                             
    perm.put("admin", new HashSet<>(Arrays.asList("*")));
    perm.put("editor", new HashSet<>(Arrays.asList("articles:*")));
    perm.put("author", 
      new HashSet<>(Arrays.asList("articles:compose", 
      "articles:save")));
}

Let’s proceed and override the doGetAuthenticationInfo():

protected AuthenticationInfo 
  doGetAuthenticationInfo(AuthenticationToken token)
  throws AuthenticationException {
                                                                 
    UsernamePasswordToken uToken = (UsernamePasswordToken) token;
                                                                
    if(uToken.getUsername() == null
      || uToken.getUsername().isEmpty()
      || !credentials.containsKey(uToken.getUsername())) {
          throw new UnknownAccountException("username not found!");
    }
                                        
    return new SimpleAuthenticationInfo(
      uToken.getUsername(), 
      credentials.get(uToken.getUsername()), 
      getName()); 
}

We first cast the AuthenticationToken provided to UsernamePasswordToken. From the uToken, we extract the username (uToken.getUsername()) and use it to get the user credentials (password) from the database.

If no record is found – we throw an UnknownAccountException, else we use the credential and username to construct a SimpleAuthenticatioInfo object that’s returned from the method.

If the user credential is hashed with a salt, we need to return a SimpleAuthenticationInfo with the associated salt:

return new SimpleAuthenticationInfo(
  uToken.getUsername(), 
  credentials.get(uToken.getUsername()), 
  ByteSource.Util.bytes("salt"), 
  getName()
);

We also need to override the doGetAuthorizationInfo(), as well as getRoleNamesForUser() and getPermissions().

Finally, let’s plug the custom realm into the securityManager. All we need to do is replace the IniRealm above with our custom realm, and pass it to the DefaultSecurityManager‘s constructor:

Realm realm = new MyCustomRealm();
SecurityManager securityManager = new DefaultSecurityManager(realm);

Every other part of the code is the same as before. This is all we need to configure the securityManager with a custom realm properly.

Now the question is – how does the framework match the credentials?

By default, the JdbcRealm uses the SimpleCredentialsMatcher, which merely checks for equality by comparing the credentials in the AuthenticationToken and the AuthenticationInfo.

If we hash our passwords, we need to inform the framework to use a HashedCredentialsMatcher instead. The INI configurations for realms with hashed passwords can be found here.

7. Logging Out

Now that we’ve authenticated the user, it’s time to implement log out. That’s done simply by calling a single method – which invalidates the user session and logs the user out:

currentUser.logout();

8. Session Management

The framework naturally comes with its session management system. If used in a web environment, it defaults to the HttpSession implementation.

For a standalone application, it uses its enterprise session management system. The benefit is that even in a desktop environment you can use a session object as you would do in a typical web environment.

Let’s have a look at a quick example and interact with the session of the current user:

Session session = currentUser.getSession();                
session.setAttribute("key", "value");                      
String value = (String) session.getAttribute("key");       
if (value.equals("value")) {                               
    log.info("Retrieved the correct value! [" + value + "]");
}

9. Shiro for a Web Application With Spring

So far we’ve outlined the basic structure of Apache Shiro and we have implemented it in a desktop environment. Let’s proceed by integrating the framework into a Spring Boot application.

Note that the main focus here is Shiro, not the Spring application – we’re only going to use that to power a simple example app.

9.1. Dependencies

First, we need to add the Spring Boot parent dependency to our pom.xml:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
</parent>

Next, we have to add the following dependencies to the same pom.xml file:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring-boot-web-starter</artifactId>
    <version>${apache-shiro-core-version}</version>
</dependency>

9.2. Configuration

Adding the shiro-spring-boot-web-starter dependency to our pom.xml will by default configure some features of the Apache Shiro application such as the SecurityManager.

However, we still need to configure the Realm and Shiro security filters. We will be using the same custom realm defined above.

And so, in the main class where the Spring Boot application is run, let’s add the following Bean definitions:

@Bean
public Realm realm() {
    return new MyCustomRealm();
}
    
@Bean
public ShiroFilterChainDefinition shiroFilterChainDefinition() {
    DefaultShiroFilterChainDefinition filter
      = new DefaultShiroFilterChainDefinition();

    filter.addPathDefinition("/secure", "authc");
    filter.addPathDefinition("/**", "anon");

    return filter;
}

In the ShiroFilterChainDefinition, we applied the authc filter to /secure path and applied the anon filter on other paths using the Ant pattern.

Both authc and anon filters come along by default for web applications. Other default filters can be found here.

If we did not define the Realm bean, ShiroAutoConfiguration will, by default, provide an IniRealm implementation that expects to find a shiro.ini file in src/main/resources or src/main/resources/META-INF.

If we do not define a ShiroFilterChainDefinition bean, the framework secures all paths and sets the login URL as login.jsp.

We can change this default login URL and other defaults by adding the following entries to our application.properties:

shiro.loginUrl = /login
shiro.successUrl = /secure
shiro.unauthorizedUrl = /login

Now that the authc filter has been applied to /secure, all requests to that route will require a form authentication.

9.3. Authentication and Authorization

Let’s create a ShiroSpringController with the following path mappings: /index, /login, /logout and /secure.

The login() method is where we implement actual user authentication as described above. If authentication is successful, the user is redirected to the secure page:

Subject subject = SecurityUtils.getSubject();

if(!subject.isAuthenticated()) {
    UsernamePasswordToken token = new UsernamePasswordToken(
      cred.getUsername(), cred.getPassword(), cred.isRememberMe());
    try {
        subject.login(token);
    } catch (AuthenticationException ae) {
        ae.printStackTrace();
        attr.addFlashAttribute("error", "Invalid Credentials");
        return "redirect:/login";
    }
}

return "redirect:/secure";

And now in the secure() implementation, the currentUser was obtained by invoking the SecurityUtils.getSubject(). The role and permissions of the user are passed on to the secure page, as well the user’s principal:

Subject currentUser = SecurityUtils.getSubject();
String role = "", permission = "";

if(currentUser.hasRole("admin")) {
    role = role  + "You are an Admin";
} else if(currentUser.hasRole("editor")) {
    role = role + "You are an Editor";
} else if(currentUser.hasRole("author")) {
    role = role + "You are an Author";
}

if(currentUser.isPermitted("articles:compose")) {
    permission = permission + "You can compose an article, ";
} else {
    permission = permission + "You are not permitted to compose an article!, ";
}

if(currentUser.isPermitted("articles:save")) {
    permission = permission + "You can save articles, ";
} else {
    permission = permission + "\nYou can not save articles, ";
}

if(currentUser.isPermitted("articles:publish")) {
    permission = permission  + "\nYou can publish articles";
} else {
    permission = permission + "\nYou can not publish articles";
}

modelMap.addAttribute("username", currentUser.getPrincipal());
modelMap.addAttribute("permission", permission);
modelMap.addAttribute("role", role);

return "secure";

And we’re done. That’s how we can integrate Apache Shiro into a Spring Boot Application.

Also, note that the framework offers additional annotations that can be used alongside filter chain definitions to secure our application.

10. JEE Integration

Integrating Apache Shiro into a JEE application is just a matter of configuring the web.xml file. As usual, the configuration expects shiro.ini to be in the class path. A detailed example configuration is available here. Also, the JSP tags can be found here.

11. Conclusion

In this tutorial, we looked at the Apache Shiro’s authentication and authorization mechanisms. We also focused on how to define a custom realm and plug it into the SecurityManager.

As always, the complete source code is available over on GitHub.

Course – LSS (cat=Security/Spring Security)

I just announced the new Learn Spring Security course, including the full material focused on the new OAuth2 stack in Spring Security:

>> CHECK OUT THE COURSE
res – Security (video) (cat=Security/Spring Security)
Comments are closed on this article!