Shiro Security Framework: Authentication and Authorization

Apache Shiro is a powerful and flexible security framework that provides robust authentication and authorization capabilities for Java applications.

Shiro Core Architecture

  • Subject: Represents the "current user" or actor interacting with the system. This can be a human user, a remote service, or any entity requiring security.
  • SecurityManager: The heart of Shiro, acting as a bridge between the Subject and the underlying security infrastructure. It manages security operations like authentication and authorization.
  • Authentication: The process of verifying the identity of a Subject.
  • Authorizer: Responsible for determining whether a Subject has the necessary permissions to perform an action.
  • SessionManager: Manages user sessions, tracking their state and activity throughout their interaction with the application.
  • CacheManager: Provides caching mechanisms to improve performance by reducing redundant data retrieval for authentication and authorization information.
  • Realm: Shiro's data sources for security information. A Realm is responsible for retrieving Subject identities, credentials, and permissions from a specific data store (e.g., a database, LDAP, or an ini file).
  • Cryptography: Shiro includes utilities for cryptographic operations, such as password hashing and salting, to enhance security.

Authentication in Shiro

Authentication is the process of confirming a Subject's identity. Key concepts include:

  • Subject: The entity attempting to access the system.
  • Principal: An identifier for a Subject, such as a username. A Subject can have multiple principals, but typically has one primary principal.
  • Credentials: Secret information known only to the Subject, such as a password or a private key, used to prove their identity.

Authentication Flow

The authentication process typically involves a Subject attempting to log in with credentials. Shiro then uses a Realm to validate these credentials against a security data store.

Example Authentication Code:


import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.Factory;

public class ShiroAuthenticationTest {
   public static void main(String[] args) {
       // 1. Obtain a SecurityManager factory, initialized with an INI configuration file.
       Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
       // 2. Get the SecurityManager instance.
       SecurityManager securityManager = factory.getInstance();
       // 3. Set the global SecurityManager for the application.
       SecurityUtils.setSecurityManager(securityManager);

       // 4. Get the Subject (current user).
       Subject currentUser = SecurityUtils.getSubject();

       // 5. Create an authentication token with username and password.
       UsernamePasswordToken token = new UsernamePasswordToken("testuser", "password123");

       try {
           // Attempt to log in.
           currentUser.login(token);
           System.out.println("Authentication Status: " + currentUser.isAuthenticated());
       } catch (UnknownAccountException uae) {
           System.err.println("Authentication Failed: User account does not exist.");
       } catch (IncorrectCredentialsException ice) {
           System.err.println("Authentication Failed: Incorrect password.");
       } catch (Exception e) {
           System.err.println("An unexpected error occurred during authentication: " + e.getMessage());
       }
   }
}
   

Shiro's built-in AuthenticatingRealm (or its subclasses like AuthorizingRealm) handles the credential matching process. The doGetAuthenticationInfo method is crucial for retrieving user data and performing the initial validation. Specific implementations like SimpleAccountRealm handle basic username checks, while custom realms can integrate with databases for more complex scenarios.

Custom Realm for Authentication

For more control, you can create a custom Realm that extends AuthorizingRealm and overrides its methods.

Custom Authentication Test Code:


import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.PrincipalCollection;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.util.ByteSource;

public class CustomRealmAuthenticationTest {
   public static void main(String[] args) {
       // Create a custom SecurityManager
       org.apache.shiro.mgt.DefaultSecurityManager securityManager = new org.apache.shiro.mgt.DefaultSecurityManager();
       // Set our custom realm
       CustomUserRealm userRealm = new CustomUserRealm();
       securityManager.setRealm(userRealm);
       SecurityUtils.setSecurityManager(securityManager);

       Subject subject = SecurityUtils.getSubject();
       UsernamePasswordToken token = new UsernamePasswordToken("customUser", "secret123");

       try {
           subject.login(token);
           System.out.println("Custom authentication successful!");
       } catch (IncorrectCredentialsException e) {
           System.err.println("Custom authentication failed: Incorrect password.");
       } catch (UnknownAccountException e) {
           System.err.println("Custom authentication failed: Unknown user.");
       } catch (AuthenticationException e) {
           System.err.println("Custom authentication failed: " + e.getMessage());
       }
   }
}
   

Custom Realm Implementation:


import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.PrincipalCollection;
import org.apache.shiro.realm.AuthorizingRealm;

public class CustomUserRealm extends AuthorizingRealm {

   // Placeholder for authorization logic
   @Override
   protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
       // This method is for authorization, not covered in this authentication example.
       return null;
   }

   // Authentication logic
   @Override
   protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
       String username = (String) authenticationToken.getPrincipal();

       // Simulate looking up user credentials in a database
       if ("customUser".equals(username)) {
           // In a real application, fetch the salt and hashed password from your data store
           String storedHashedPassword = "hashed_password_from_db"; // Replace with actual retrieval
           ByteSource salt = ByteSource.Util.bytes("a_unique_salt_for_this_user"); // Replace with actual retrieval

           // Return SimpleAuthenticationInfo with principal, stored credentials, and salt
           return new SimpleAuthenticationInfo(username, storedHashedPassword, salt, getName());
       }
       // If username not found, Shiro will automatically throw UnknownAccountException
       return null;
   }
}
   

Password Hashing with MD5 and Salt

To securely store passwords, Shiro suppports hashing with salts. Salting adds a random value to the password before hashing, making rainbow table attacks more difficult.

MD5 Hashing Example:


import org.apache.shiro.crypto.hash.Md5Hash;
import org.apache.shiro.util.ByteSource;

public class PasswordHashingExample {
   public static void main(String[] args) {
       String plainPassword = "mysecretpassword";
       String salt = "random_salt_value";
       int hashIterations = 1024; // Number of times to hash the password

       // Hash the password with MD5, salt, and multiple iterations
       Md5Hash md5Hash = new Md5Hash(plainPassword, salt, hashIterations);
       String hashedPassword = md5Hash.toHex();

       System.out.println("Original Password: " + plainPassword);
       System.out.println("Salt: " + salt);
       System.out.println("Hashed Password (MD5 + Salt + " + hashIterations + " iterations): " + hashedPassword);

       // To verify, you would hash the login attempt password with the same salt and iterations
       // and compare the result with the stored hashedPassword.
   }
}
   

Custom Realm with MD5 Hashing and CredentialsMatcher:


import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.PrincipalCollection;
import org.apache.shiro.crypto.hash.Md5Hash;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.ByteSource;

// Custom Realm for MD5 password hashing
class CustomMd5Realm extends AuthorizingRealm {
   @Override
   protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
       return null; // Placeholder for authorization
   }

   @Override
   protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
       String username = (String) token.getPrincipal();

       // Simulate fetching user data from a database
       if ("secureUser".equals(username)) {
           // In a real scenario, fetch the salt and the pre-hashed password from your DB
           String storedHashedPassword = "3a7e6b1f0a4d9c8e2f1a0b9c8d7e6f5a"; // Example: MD5("password123" + "unique_salt").toHex()
           ByteSource salt = ByteSource.Util.bytes("unique_salt"); // The salt used during user registration

           return new SimpleAuthenticationInfo(username, storedHashedPassword, salt, getName());
       }
       return null; // User not found
   }
}

public class ShiroMd5AuthenticationTest {
   public static void main(String[] args) {
       DefaultSecurityManager securityManager = new DefaultSecurityManager();
       CustomMd5Realm realm = new CustomMd5Realm();

       // Configure HashedCredentialsMatcher for password verification
       HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
       credentialsMatcher.setHashAlgorithmName("md5"); // Specify the hashing algorithm
       credentialsMatcher.setHashIterations(1024);      // Specify the number of hash iterations
       realm.setCredentialsMatcher(credentialsMatcher);

       securityManager.setRealm(realm);
       SecurityUtils.setSecurityManager(securityManager);

       Subject subject = SecurityUtils.getSubject();
       UsernamePasswordToken token = new UsernamePasswordToken("secureUser", "password123"); // User's input password

       try {
           subject.login(token);
           System.out.println("MD5 Authentication successful!");
       } catch (UnknownAccountException e) {
           System.err.println("MD5 Authentication failed: Unknown user.");
       } catch (IncorrectCredentialsException e) {
           System.err.println("MD5 Authentication failed: Incorrect password.");
       } catch (AuthenticationException e) {
           System.err.println("MD5 Authentication failed: " + e.getMessage());
       }
   }
}
   

Authorization in Shiro

Authorization, or access control, determines what actions a Subject is allowed to perform after they have been authenticated. Shiro supports two primary models:

  • Role-Based Access Control (RBAC): Users are assigned roles, and permissions are granted to roles. This simplifies management by grouping permissions.
  • Resource-Based Access Control (Permission-Based): Permissions are directly assigned to users, defining specific actions on specific resources.

Permission String Format

Shiro uses a flexible permission string format, commonly following the pattern:

[Resource]:[Action]:[Instance]- Resource: The type of resource (e.g., user, product).

  • Action: The operation to perform (e.g., create, update, delete, view).
  • Instance: A specific identifier for the resource instance (e.g., 001, admin).

Wildcards (*) can be used for broader permissions.

  • user:create:*: Permission to create any user.
  • user:update:001: Permission to update user instance 001.
  • product:*:01: Permission to perform any action on product instance 01.

Authorization Implementation with Custom Realm

The doGetAuthorizationInfo method in your custom Realm is where you define a Subject's roles and permissions.

Custom Realm with Authorization Logic:


import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.PrincipalCollection;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.util.ByteSource;

// Assume this realm also handles authentication as shown previously
public class CustomAuthzRealm extends AuthorizingRealm {

   // Authorization logic
   @Override
   protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
       System.out.println("======== Starting Authorization ========");
       String username = (String) principalCollection.getPrimaryPrincipal();
       System.out.println("Primary Principal: " + username);

       SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();

       // Simulate fetching roles and permissions from a database based on the username
       if ("authorizedUser".equals(username)) {
           // Assign roles
           authorizationInfo.addRole("administrator");
           authorizationInfo.addRole("editor");

           // Assign permissions
           authorizationInfo.addStringPermission("content:create"); // Permission to create content
           authorizationInfo.addStringPermission("content:edit:123"); // Permission to edit content instance 123
           authorizationInfo.addStringPermission("user:view:*:456"); // Permission to view any user related to instance 456
       } else if ("basicUser".equals(username)) {
           authorizationInfo.addRole("viewer");
           authorizationInfo.addStringPermission("content:view");
       }

       return authorizationInfo;
   }

   // Authentication logic (simplified for brevity, assumes user and password match)
   @Override
   protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
       String username = (String) token.getPrincipal();
       char[] passwordChars = (char[]) token.getCredentials();
       String password = new String(passwordChars);

       // Simulate database lookup
       if ("authorizedUser".equals(username) && "securepass1".equals(password)) {
           // For simplicity, no salt used here, but in practice, use hashing and salt
           return new SimpleAuthenticationInfo(username, password, getName());
       } else if ("basicUser".equals(username) && "simplepass2".equals(password)) {
           return new SimpleAuthenticationInfo(username, password, getName());
       }
       return null; // User not found or incorrect credentials
   }
}
   

Testing Authentication and Authorization:


import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.authz.permission.WildcardPermission;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.subject.Subject;
import java.util.Arrays;

public class ShiroAuthorizationTest {
   public static void main(String[] args) {
       // Setup Security Manager and Realm
       DefaultSecurityManager securityManager = new DefaultSecurityManager();
       CustomAuthzRealm realm = new CustomAuthzRealm(); // Using the realm defined above
       securityManager.setRealm(realm);
       SecurityUtils.setSecurityManager(securityManager);

       Subject subject = SecurityUtils.getSubject();
       UsernamePasswordToken token = new UsernamePasswordToken("authorizedUser", "securepass1"); // Login as 'authorizedUser'

       try {
           subject.login(token);
           System.out.println("Login successful!");
       } catch (UnknownAccountException e) {
           System.err.println("Login failed: Unknown account.");
           return;
       } catch (IncorrectCredentialsException e) {
           System.err.println("Login failed: Incorrect credentials.");
           return;
       } catch (AuthenticationException e) {
           System.err.println("Login failed: " + e.getMessage());
           return;
       }

       // Perform Authorization Checks
       if (subject.isAuthenticated()) {
           System.out.println("\n--- Role Checks ---");
           // Check for a specific role
           System.out.println("Has role 'administrator': " + subject.hasRole("administrator"));
           // Check for all specified roles
           System.out.println("Has roles 'administrator' AND 'editor': " + subject.hasAllRoles(Arrays.asList("administrator", "editor")));
           // Check if has any of the specified roles
           boolean[] roleResults = subject.hasRoles(Arrays.asList("administrator", "guest", "editor"));
           System.out.println("Role check results (admin, guest, editor): " + Arrays.toString(roleResults));

           System.out.println("\n--- Permission Checks ---");
           // Check for a specific permission string
           System.out.println("Has permission 'content:create': " + subject.isPermitted("content:create"));
           // Check for permission on a specific instance
           System.out.println("Has permission 'content:edit:123': " + subject.isPermitted("content:edit:123"));
           // Check for wildcard permission
           System.out.println("Has permission 'user:view:*:456': " + subject.isPermitted("user:view:*:456"));
           System.out.println("Has permission 'user:view:007:456': " + subject.isPermitted("user:view:007:456")); // Specific instance check
           System.out.println("Has permission 'product:delete': " + subject.isPermitted("product:delete")); // Should be false

           // Check multiple permissions at once
           boolean[] permResults = subject.isPermitted("content:create", "content:edit:123", "product:delete");
           System.out.println("Permission check results (create, edit:123, prod:delete): " + Arrays.toString(permResults));

           // Check if all specified permissions are granted
           boolean allPermitted = subject.isPermittedAll("content:create", "content:edit:123");
           System.out.println("Has ALL permissions ('content:create', 'content:edit:123'): " + allPermitted);

           // Using WildcardPermission for more complex checks
           WildcardPermission viewSpecificContentPerm = new WildcardPermission("content:view:789");
           System.out.println("Has specific permission 'content:view:789': " + subject.isPermitted(viewSpecificContentPerm));

           WildcardPermission editAnyContentPerm = new WildcardPermission("content:edit:*");
           System.out.println("Has wildcard permission 'content:edit:*': " + subject.isPermitted(editAnyContentPerm));
       }
   }
}
   

Tags: Shiro java Security Authentication Authorization

Posted on Sun, 30 Aug 2026 16:47:32 +0000 by g00fy_m