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 instance001.product:*:01: Permission to perform any action on product instance01.
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));
}
}
}