Spring Boot Shiro Integration: Custom Authentication Mechanisms

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; }
    }
}

Tags: spring-boot apache-shiro Authentication Encryption token-auth

Posted on Sat, 06 Jun 2026 18:45:38 +0000 by DapperDanMan