Spring Boot Shiro Integration: Custom Authentication Mechanisms
Introduction to Apache Shiro
Apache Shiro is a powerful and user-friendly Java security framework designed to handle authentication, authorization, session management, and cryptography. Its straightforward architecture makes it suitable for applications of all sizes, from mobile apps to enterprise systems.
Shiro Architecture
Understanding Shiro's core components is essential for effective implementation:
- Subject: Represents any user interacting with the application.
- SecurityManager: The heart of Shiro, managing all security operations.
- Realm: Acts as a security data source, providing authentication and authorization data.
- Authenticator: Handles user authentication processes.
- Authorizer: Determines user permissions and access rights.
- SessionManager: Manages user sessions across different environments.
- Cryptography: Provides data encryption capabilities.
Shiro vs. Spring Security
When choosing a security framework, consider these key differences:
- Shiro offers simpler configuration and easier integration with non-Spring applications.
- Spring Security provides deeper integration with the Spring ecosystem.
- Shiro has fewer dependencies and can run independently of containers.
- Spring Security has a larger community and more extensive documentation.
Advantages and Disadvantages
Advantages:
- Simple and intuitive API
- Lightweight with minimal dependencies
- Flexible and extensible architecture
- Comprehensive security features
Disadvantages:
- Smaller community compared to Spring Security
- Less seamless integration with Spring framework
Spring Boot Integration
To integrate Shiro with Spring Boot, follow these steps:
1. Maven Dependencies
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Shiro Spring Boot integration -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-web-starter</artifactId>
<version>1.7.1</version>
</dependency>
</dependencies>
2. Custom Password Encryption
package com.example.security.encryption;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.credential.SimpleCredentialsMatcher;
import org.springframework.util.DigestUtils;
public class CustomCredentialsMatcher extends SimpleCredentialsMatcher {
@Override
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
// Extract credentials from token
String providedCredentials = extractCredentials(token);
// Get stored credentials
String storedCredentials = getStoredCredentials(info);
// Compare credentials
return equals(providedCredentials, storedCredentials);
}
private String extractCredentials(AuthenticationToken token) {
String rawPassword = new String((char[]) token.getCredentials());
// Apply custom encryption
return encryptPassword(rawPassword);
}
private String getStoredCredentials(AuthenticationInfo info) {
return (String) getCredentials(info);
}
private String encryptPassword(String rawPassword) {
// Apply MD5 encryption with salt
String salt = "security-salt";
String encrypted = DigestUtils.md5DigestAsHex((salt + rawPassword).getBytes());
// Apply multiple iterations
for (int i = 0; i < 2; i++) {
encrypted = DigestUtils.md5DigestAsHex(encrypted.getBytes());
}
return encrypted;
}
}
3. Custom Realm Implementation
package com.example.security.realm;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.springframework.stereotype.Component;
@Component
public class ApplicationRealm extends AuthorizingRealm {
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
// Extract user information from principals
String username = (String) principals.getPrimaryPrincipal();
// Create authorization info
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
// Add permissions and roles based on user data
// authorizationInfo.addStringPermission("permission:read");
// authorizationInfo.addRole("user");
return authorizationInfo;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
// Extract username from token
UsernamePasswordToken userToken = (UsernamePasswordToken) token;
String username = userToken.getUsername();
// Retrieve user from database
// User user = userRepository.findByUsername(username);
// Mock user for demonstration
if ("admin".equals(username)) {
String encryptedPassword = "a8a3534fb460598459bfb4d48b7f0e8a"; // MD5 with salt, 3 iterations
return new SimpleAuthenticationInfo(username, encryptedPassword, getName());
}
return null; // User not found
}
}
4. Shiro Configuration
package com.example.config;
import com.example.security.encryption.CustomCredentialsMatcher;
import com.example.security.realm.ApplicationRealm;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.LinkedHashMap;
import java.util.Map;
@Configuration
public class ShiroConfiguration {
@Bean
public ApplicationRealm applicationRealm() {
ApplicationRealm realm = new ApplicationRealm();
// Set custom credentials matcher
realm.setCredentialsMatcher(new CustomCredentialsMatcher());
return realm;
}
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(applicationRealm());
return securityManager;
}
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
shiroFilter.setSecurityManager(securityManager);
// Define filter chains
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
filterChainDefinitionMap.put("/login", "anon"); // No authentication required
filterChainDefinitionMap.put("/**", "authc"); // Authentication required
shiroFilter.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilter;
}
}
5. Login Controller
package com.example.controller;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.subject.Subject;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/auth")
public class AuthenticationController {
@PostMapping("/login")
public String login(@RequestBody LoginRequest request) {
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(
request.getUsername(),
request.getPassword()
);
try {
subject.login(token);
return "Login successful";
} catch (Exception e) {
return "Authentication failed: " + e.getMessage();
}
}
@GetMapping("/logout")
public String logout() {
SecurityUtils.getSubject().logout();
return "Logged out successfully";
}
static class LoginRequest {
private String username;
private String password;
// Getters and setters
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getPassword() { return password; }
public void setPassword(String password) { this.password = password; }
}
}
Custom Token Authentication
To implement custom token authentication, follow these additional steps:
1. Custom Token Class
package com.example.security.token;
import lombok.Data;
import org.apache.shiro.authc.AuthenticationToken;
@Data
public class CustomToken implements AuthenticationToken {
private final String token;
public CustomToken(String token) {
this.token = token;
}
@Override
public Object getPrincipal() {
return token;
}
@Override
public Object getCredentials() {
return token;
}
}
2. Token Authentication Filter
package com.example.security.filter;
import com.example.security.token.CustomToken;
import org.apache.shiro.web.filter.authc.AuthenticatingFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.util.StringUtils;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.util.concurrent.TimeUnit;
public class TokenFilter extends AuthenticatingFilter {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
private static final String TOKEN_HEADER = "Authorization";
@Override
protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String token = httpRequest.getHeader(TOKEN_HEADER);
if (StringUtils.isEmpty(token)) {
return null;
}
return new CustomToken(token);
}
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
try {
return executeLogin(request, response);
} catch (Exception e) {
return false;
}
}
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) {
// Handle unauthorized access
return false;
}
private boolean validateToken(String token) {
// Check if token exists in Redis
Object storedToken = redisTemplate.opsForValue().get(token);
return storedToken != null;
}
}
3. Updated Shiro Configuration for Token Authentication
package com.example.config;
import com.example.security.filter.TokenFilter;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.LinkedHashMap;
import java.util.Map;
@Configuration
public class ShiroTokenConfiguration {
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(applicationRealm());
return securityManager;
}
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
shiroFilter.setSecurityManager(securityManager);
// Add custom filter
Map<String, Filter> filters = new HashMap<>();
filters.put("token", new TokenFilter());
shiroFilter.setFilters(filters);
// Define filter chains
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
filterChainDefinitionMap.put("/login", "anon");
filterChainDefinitionMap.put("/**", "token");
shiroFilter.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilter;
}
}
4. Updated Realm for Token Authentication
package com.example.security.realm;
import com.example.security.token.CustomToken;
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.realm.AuthorizingRealm;
import org.springframework.stereotype.Component;
@Component
public class TokenRealm extends AuthorizingRealm {
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof CustomToken;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
CustomToken customToken = (CustomToken) token;
String authToken = customToken.getToken();
// Validate token (e.g., check in Redis or JWT validation)
// if (!tokenService.validateToken(authToken)) {
// throw new AuthenticationException("Invalid token");
// }
// Return authentication info
return new SimpleAuthenticationInfo(authToken, authToken, getName());
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
// Implement authorization logic based on token
return null;
}
}
5. Token-Based Login Controller
package com.example.controller;
import com.example.security.token.CustomToken;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.subject.Subject;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/auth")
public class TokenAuthenticationController {
@PostMapping("/token-login")
public String tokenLogin(@RequestBody TokenLoginRequest request) {
// Validate credentials and generate token
String token = generateToken(request.getUsername(), request.getPassword());
Subject subject = SecurityUtils.getSubject();
try {
subject.login(new CustomToken(token));
return token;
} catch (Exception e) {
return "Authentication failed: " + e.getMessage();
}
}
private String generateToken(String username, String password) {
// Implement token generation (e.g., JWT)
return "generated-token-" + System.currentTimeMillis();
}
static class TokenLoginRequest {
private String username;
private String password;
// Getters and setters
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getPassword() { return password; }
public void setPassword(String password) { this.password = password; }
}
}