google-site-verification: google61fe8ba583a51912.html
Simplified Security: Implementing Authentication and Authorization with Apache Shiro in Java


Article

In Java application development, implementing robust security—authentication, authorization, session management, and cryptography—can be complex and time-consuming. Apache Shiro is a powerful, easy-to-use Java security framework that simplifies these concerns while providing comprehensive security capabilities. Unlike the more complex Spring Security, Shiro offers a clean, intuitive API that makes security accessible to Java developers of all experience levels.

What is Apache Shiro?

Apache Shiro is a comprehensive security framework that provides:

  • Authentication: Verify user identity
  • Authorization: Control access to resources
  • Session Management: User session handling, even in non-web applications
  • Cryptography: Simplified encryption and hashing
  • Web Integration: Security for web applications
  • Caching: Performance optimization for security operations

Why Choose Apache Shiro?

  1. Simplicity: Intuitive API and straightforward configuration
  2. Flexibility: Works with any application architecture
  3. Comprehensive: Handles all aspects of security in one framework
  4. Web Agnostic: Can be used in web, desktop, and mobile applications
  5. Lightweight: Minimal dependencies and overhead

Core Shiro Concepts

  • Subject: The current user (human, service, or daemon)
  • SecurityManager: The heart of Shiro, coordinating security operations
  • Realm: Bridge between Shiro and your security data (database, LDAP, etc.)
  • Permission: Atomic privilege to perform an action
  • Role: Named collection of permissions
  • AuthenticationToken: Credentials presented for verification

Setting Up Apache Shiro

1. Add Dependencies:

<!-- pom.xml --> <dependencies> <!-- Shiro Core --> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> <version>1.13.0</version> </dependency> <!-- Shiro Web --> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-web</artifactId> <version>1.13.0</version> </dependency> <!-- Shiro Spring --> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.13.0</version> </dependency> <!-- Optional: Shiro caching --> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-ehcache</artifactId> <version>1.13.0</version> </dependency> </dependencies>

2. Basic Shiro Configuration:

# shiro.ini

[main]

# Configure Realm myRealm = com.example.shiro.CustomJdbcRealm myRealm.permissionsLookupEnabled = true # Configure SecurityManager securityManager.realms = $myRealm # Configure Cache cacheManager = org.apache.shiro.cache.MemoryConstrainedCacheManager securityManager.cacheManager = $cacheManager # Session Management sessionManager = org.apache.shiro.web.session.mgt.DefaultWebSessionManager securityManager.sessionManager = $sessionManager

[urls]

/login = anon /logout = logout /admin/** = authc, roles[admin] /user/** = authc, perms["user:read"] /api/** = authc /** = authc

Spring Boot Integration

1. Shiro Configuration Class:

@Configuration public class ShiroConfig { @Bean public Realm realm() { return new CustomJdbcRealm(); } @Bean public DefaultWebSecurityManager securityManager(Realm realm) { DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); securityManager.setRealm(realm); // Configure cache securityManager.setCacheManager(new MemoryConstrainedCacheManager()); // Configure session manager securityManager.setSessionManager(sessionManager()); return securityManager; } @Bean public SessionManager sessionManager() { DefaultWebSessionManager sessionManager = new DefaultWebSessionManager(); sessionManager.setGlobalSessionTimeout(1800000); // 30 minutes sessionManager.setSessionValidationSchedulerEnabled(true); return sessionManager; } @Bean public ShiroFilterFactoryBean shiroFilter(DefaultWebSecurityManager securityManager) { ShiroFilterFactoryBean filterFactoryBean = new ShiroFilterFactoryBean(); filterFactoryBean.setSecurityManager(securityManager); // Configure login and unauthorized URLs filterFactoryBean.setLoginUrl("/login"); filterFactoryBean.setUnauthorizedUrl("/unauthorized"); filterFactoryBean.setSuccessUrl("/dashboard"); // Configure URL filters Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>(); // Public endpoints filterChainDefinitionMap.put("/css/**", "anon"); filterChainDefinitionMap.put("/js/**", "anon"); filterChainDefinitionMap.put("/webjars/**", "anon"); filterChainDefinitionMap.put("/login", "anon"); filterChainDefinitionMap.put("/register", "anon"); // Logout filterChainDefinitionMap.put("/logout", "logout"); // Role-based access filterChainDefinitionMap.put("/admin/**", "authc, roles[admin]"); filterChainDefinitionMap.put("/manager/**", "authc, roles[manager]"); // Permission-based access filterChainDefinitionMap.put("/user/create", "authc, perms[user:create]"); filterChainDefinitionMap.put("/user/delete", "authc, perms[user:delete]"); filterChainDefinitionMap.put("/user/**", "authc, perms[user:read]"); // Authenticated access filterChainDefinitionMap.put("/**", "authc"); filterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); return filterFactoryBean; } @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor( DefaultWebSecurityManager securityManager) { AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor(); advisor.setSecurityManager(securityManager); return advisor; } }

Custom Realm Implementation

1. Database-Backed Realm:

@Component public class CustomJdbcRealm extends AuthorizingRealm { @Autowired private UserService userService; @Autowired private PasswordService passwordService; public CustomJdbcRealm() { // Configure credentials matcher setCredentialsMatcher(new BCryptCredentialsMatcher()); // Enable caching setCachingEnabled(true); setAuthenticationCachingEnabled(true); setAuthorizationCachingEnabled(true); } @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { UsernamePasswordToken upToken = (UsernamePasswordToken) token; String username = upToken.getUsername(); // Retrieve user from database User user = userService.findByUsername(username) .orElseThrow(() -> new UnknownAccountException("User not found: " + username)); // Check if account is locked if (!user.isActive()) { throw new LockedAccountException("Account is locked: " + username); } // Return authentication info return new SimpleAuthenticationInfo( new ShiroUserPrincipal(user), // principal user.getPassword(), // hashed password getName() // realm name ); } @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { ShiroUserPrincipal principal = (ShiroUserPrincipal) principals.getPrimaryPrincipal(); User user = principal.getUser(); SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); // Add roles authorizationInfo.setRoles(userService.getUserRoles(user.getId())); // Add permissions authorizationInfo.setStringPermissions(userService.getUserPermissions(user.getId())); return authorizationInfo; } @Override public boolean supports(AuthenticationToken token) { return token instanceof UsernamePasswordToken; } }

2. User Principal Wrapper:

public class ShiroUserPrincipal implements Serializable { private final User user; private final Set<String> roles; private final Set<String> permissions; public ShiroUserPrincipal(User user) { this.user = user; this.roles = Collections.unmodifiableSet(new HashSet<>()); this.permissions = Collections.unmodifiableSet(new HashSet<>()); } public ShiroUserPrincipal(User user, Set<String> roles, Set<String> permissions) { this.user = user; this.roles = Collections.unmodifiableSet(roles); this.permissions = Collections.unmodifiableSet(permissions); } // Getters public User getUser() { return user; } public Set<String> getRoles() { return roles; } public Set<String> getPermissions() { return permissions; } @Override public String toString() { return user.getUsername(); } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; ShiroUserPrincipal that = (ShiroUserPrincipal) o; return Objects.equals(user.getId(), that.user.getId()); } @Override public int hashCode() { return Objects.hash(user.getId()); } }

Service Layer Implementation

1. User Service:

@Service @Transactional public class UserService { @Autowired private UserRepository userRepository; @Autowired private RoleRepository roleRepository; @Autowired private PasswordService passwordService; public Optional<User> findByUsername(String username) { return userRepository.findByUsername(username); } public Set<String> getUserRoles(Long userId) { return roleRepository.findRoleNamesByUserId(userId); } public Set<String> getUserPermissions(Long userId) { return roleRepository.findPermissionNamesByUserId(userId); } public User createUser(User user, Set<String> roleNames) { // Hash password String hashedPassword = passwordService.hashPassword(user.getPassword()); user.setPassword(hashedPassword); user.setActive(true); user.setCreatedAt(LocalDateTime.now()); // Save user User savedUser = userRepository.save(user); // Assign roles assignRoles(savedUser.getId(), roleNames); return savedUser; } public void changePassword(Long userId, String newPassword) { User user = userRepository.findById(userId) .orElseThrow(() -> new RuntimeException("User not found")); String hashedPassword = passwordService.hashPassword(newPassword); user.setPassword(hashedPassword); userRepository.save(user); } private void assignRoles(Long userId, Set<String> roleNames) { // Implementation for role assignment } }

2. Password Service:

@Service public class PasswordService { private final DefaultHashService hashService; private final HashRequest.Builder hashRequestBuilder; public PasswordService() { this.hashService = new DefaultHashService(); hashService.setHashAlgorithmName("SHA-256"); hashService.setGeneratePublicSalt(true); hashService.setHashIterations(500000); this.hashRequestBuilder = new HashRequest.Builder(); } public String hashPassword(String plainPassword) { HashRequest request = hashRequestBuilder .setSource(ByteSource.Util.bytes(plainPassword)) .build(); Hash hash = hashService.computeHash(request); return hash.toHex(); } public boolean verifyPassword(String plainPassword, String hashedPassword) { HashRequest request = hashRequestBuilder .setSource(ByteSource.Util.bytes(plainPassword)) .build(); Hash computedHash = hashService.computeHash(request); return computedHash.toHex().equals(hashedPassword); } }

Controller Implementation

1. Authentication Controller:

@Controller public class AuthController { private static final Logger logger = LoggerFactory.getLogger(AuthController.class); @GetMapping("/login") public String loginForm(@RequestParam(value = "error", required = false) String error, @RequestParam(value = "logout", required = false) String logout, Model model) { if (error != null) { model.addAttribute("error", "Invalid username or password"); } if (logout != null) { model.addAttribute("message", "You have been logged out successfully"); } return "login"; } @PostMapping("/login") public String login(@RequestParam String username, @RequestParam String password, @RequestParam(value = "rememberMe", defaultValue = "false") boolean rememberMe, Model model) { try { Subject currentUser = SecurityUtils.getSubject(); if (!currentUser.isAuthenticated()) { UsernamePasswordToken token = new UsernamePasswordToken(username, password); token.setRememberMe(rememberMe); currentUser.login(token); logger.info("User {} logged in successfully", username); return "redirect:/dashboard"; } } catch (UnknownAccountException e) { model.addAttribute("error", "User not found"); } catch (IncorrectCredentialsException e) { model.addAttribute("error", "Invalid password"); } catch (LockedAccountException e) { model.addAttribute("error", "Account is locked"); } catch (AuthenticationException e) { model.addAttribute("error", "Authentication failed: " + e.getMessage()); } return "login"; } @GetMapping("/logout") public String logout() { Subject currentUser = SecurityUtils.getSubject(); if (currentUser != null) { currentUser.logout(); } return "redirect:/login?logout=true"; } @GetMapping("/unauthorized") public String unauthorized() { return "unauthorized"; } }

2. Secure REST API Controller:

@RestController @RequestMapping("/api") public class UserApiController { @Autowired private UserService userService; @GetMapping("/user/profile") public ResponseEntity<UserProfile> getUserProfile() { Subject currentUser = SecurityUtils.getSubject(); ShiroUserPrincipal principal = (ShiroUserPrincipal) currentUser.getPrincipal(); UserProfile profile = UserProfile.builder() .username(principal.getUser().getUsername()) .email(principal.getUser().getEmail()) .roles(principal.getRoles()) .permissions(principal.getPermissions()) .build(); return ResponseEntity.ok(profile); } @RequiresAuthentication @GetMapping("/users") public ResponseEntity<List<User>> listUsers() { List<User> users = userService.findAllUsers(); return ResponseEntity.ok(users); } @RequiresRoles("admin") @PostMapping("/users") public ResponseEntity<User> createUser(@RequestBody CreateUserRequest request) { User user = userService.createUser(request.toUser(), request.getRoles()); return ResponseEntity.status(HttpStatus.CREATED).body(user); } @RequiresPermissions("user:delete") @DeleteMapping("/users/{userId}") public ResponseEntity<Void> deleteUser(@PathVariable Long userId) { userService.deleteUser(userId); return ResponseEntity.noContent().build(); } @RequiresUser @PutMapping("/user/password") public ResponseEntity<Void> changePassword(@RequestBody ChangePasswordRequest request) { Subject currentUser = SecurityUtils.getSubject(); ShiroUserPrincipal principal = (ShiroUserPrincipal) currentUser.getPrincipal(); userService.changePassword(principal.getUser().getId(), request.getNewPassword()); return ResponseEntity.ok().build(); } }

3. Web MVC Controller with Annotations:

@Controller @RequestMapping("/admin") public class AdminController { @RequiresAuthentication @GetMapping("/dashboard") public String adminDashboard(Model model) { Subject currentUser = SecurityUtils.getSubject(); model.addAttribute("user", currentUser.getPrincipal()); return "admin/dashboard"; } @RequiresRoles("admin") @GetMapping("/users") public String userManagement(Model model) { model.addAttribute("users", userService.findAllUsers()); return "admin/users"; } @RequiresPermissions("user:create") @GetMapping("/users/create") public String createUserForm(Model model) { model.addAttribute("user", new User()); return "admin/create-user"; } @RequiresPermissions("user:create") @PostMapping("/users") public String createUser(@ModelAttribute User user, @RequestParam Set<String> roles) { userService.createUser(user, roles); return "redirect:/admin/users"; } }

Advanced Shiro Features

1. Custom Permission Implementation:

public class DomainObjectPermission extends WildcardPermission { private final String domain; private final Long targetId; public DomainObjectPermission(String permissionString) { super(permissionString); String[] parts = permissionString.split(":"); if (parts.length >= 3) { this.domain = parts[0]; this.targetId = parts.length > 3 ? Long.parseLong(parts[2]) : null; } else { this.domain = null; this.targetId = null; } } public DomainObjectPermission(String domain, String actions, Long targetId) { super(domain + ":" + actions + (targetId != null ? ":" + targetId : "")); this.domain = domain; this.targetId = targetId; } // Getters public String getDomain() { return domain; } public Long getTargetId() { return targetId; } } // Usage in realm @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); // Add domain-specific permissions authorizationInfo.addObjectPermission( new DomainObjectPermission("project", "read,write", 123L) ); return authorizationInfo; }

2. Session Management:

@Service public class SessionService { public List<Session> getActiveSessions() { DefaultSecurityManager securityManager = (DefaultSecurityManager) SecurityUtils.getSecurityManager(); SessionManager sessionManager = securityManager.getSessionManager(); Collection<Session> activeSessions = ((DefaultSessionManager) sessionManager).getSessionDAO().getActiveSessions(); return new ArrayList<>(activeSessions); } public void expireSession(String sessionId) { DefaultSecurityManager securityManager = (DefaultSecurityManager) SecurityUtils.getSecurityManager(); SessionManager sessionManager = securityManager.getSessionManager(); Session session = sessionManager.getSession(new DefaultSessionKey(sessionId)); if (session != null) { session.stop(); } } public Session getCurrentSession() { return SecurityUtils.getSubject().getSession(); } }

3. Remember Me Service:

@Component public class CustomRememberMeManager extends CookieRememberMeManager { @Autowired private UserService userService; @Override protected byte[] getRememberedSerializedIdentity(SubjectContext subjectContext) { byte[] bytes = super.getRememberedSerializedIdentity(subjectContext); if (bytes != null) { // Additional validation for remembered users validateRememberedUser(bytes); } return bytes; } private void validateRememberedUser(byte[] serialized) { try { String username = new String(serialized, StandardCharsets.UTF_8); User user = userService.findByUsername(username) .orElseThrow(() -> new AuthenticationException("Remembered user not found")); if (!user.isActive()) { throw new AuthenticationException("Remembered account is disabled"); } } catch (Exception e) { forgetIdentity(SecurityUtils.getSubject()); throw new AuthenticationException("Invalid remembered user", e); } } }

Testing Shiro Security

1. Unit Test Configuration:

@ExtendWith(MockitoExtension.class) public class ShiroSecurityTest { @Mock private UserService userService; private CustomJdbcRealm realm; @BeforeEach void setUp() { realm = new CustomJdbcRealm(); realm.setUserService(userService); // Set up minimal Shiro environment for testing DefaultSecurityManager securityManager = new DefaultSecurityManager(); securityManager.setRealm(realm); SecurityUtils.setSecurityManager(securityManager); } @Test void testSuccessfulAuthentication() { // Given User user = new User("testuser", "hashedPassword"); when(userService.findByUsername("testuser")).thenReturn(Optional.of(user)); // When Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken("testuser", "password"); // Then assertDoesNotThrow(() -> subject.login(token)); assertTrue(subject.isAuthenticated()); } @Test void testAuthorization() { // Given User user = new User("testuser", "hashedPassword"); when(userService.findByUsername("testuser")).thenReturn(Optional.of(user)); when(userService.getUserRoles(1L)).thenReturn(Set.of("admin", "user")); when(userService.getUserPermissions(1L)).thenReturn(Set.of("user:read", "user:write")); // When Subject subject = SecurityUtils.getSubject(); subject.login(new UsernamePasswordToken("testuser", "password")); // Then assertTrue(subject.hasRole("admin")); assertTrue(subject.isPermitted("user:read")); assertFalse(subject.isPermitted("user:delete")); } }

Best Practices for Production

  1. Secure Session Management: Use secure cookies and session timeouts
  2. Password Hashing: Use strong hashing algorithms with sufficient iterations
  3. Role and Permission Design: Design a clear role and permission hierarchy
  4. Caching Strategy: Implement appropriate caching for performance
  5. Security Monitoring: Log authentication and authorization events
@Aspect @Component public class SecurityAuditAspect { private static final Logger logger = LoggerFactory.getLogger(SecurityAuditAspect.class); @AfterReturning("execution(* org.apache.shiro.realm..*.*(..))") public void auditRealmOperations(JoinPoint joinPoint) { logger.info("Shiro realm operation: {}", joinPoint.getSignature().getName()); } @AfterThrowing(pointcut = "execution(* org.apache.shiro.realm..*.*(..))", throwing = "ex") public void auditAuthenticationFailures(JoinPoint joinPoint, Exception ex) { logger.warn("Authentication failure in {}: {}", joinPoint.getSignature().getName(), ex.getMessage()); } }

Conclusion

Apache Shiro provides a clean, intuitive approach to Java application security that balances power with simplicity. Its comprehensive feature set—covering authentication, authorization, session management, and cryptography—makes it suitable for applications of all sizes, from simple web apps to complex enterprise systems.

The framework's flexibility allows it to integrate seamlessly with various data sources and application architectures, while its annotation-based security and URL filtering provide multiple layers of protection. For Java teams looking for a security solution that's both powerful and approachable, Apache Shiro offers an excellent balance of capability and developer productivity.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper