Information Security 12 min read

Integrating Apache Shiro with Spring Boot: Core Components, Configuration, and Authentication Flow

This article provides a comprehensive guide on integrating Apache Shiro into a Spring Boot application, covering core components, Maven configuration, bean definitions, security manager setup, custom realms, filter chain configuration, and the complete login authentication flow with code examples.

360 Quality & Efficiency
360 Quality & Efficiency
360 Quality & Efficiency
Integrating Apache Shiro with Spring Boot: Core Components, Configuration, and Authentication Flow

In Java projects, security frameworks such as Apache Shiro and Spring Security are commonly used; Shiro is a lightweight, intuitive framework that offers authentication, authorization, password and cache management. For Spring Boot, the shiro-spring-boot-web-starter starter simplifies integration without manual configuration.

Shiro’s three core components are Subject , SecurityManager , and Realm . These components work together to handle authentication and authorization within the application.

Version management is handled via Maven properties so that the Shiro version can be referenced throughout the pom.xml file.

<properties>
    <shiro.version>1.6.0</shiro.version>
    <java.version>1.8</java.version>
    <jmeter.version>5.4.1</jmeter.version>
</properties>

The Shiro starter dependency is then added using the defined property:

<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring-boot-web-starter</artifactId>
    <version>${shiro.version}</version>
</dependency>

A ShiroConfig class is created and annotated with @Configuration . Inside this class three beans are defined: ShiroFilterFactoryBean , DefaultWebSecurityManager , and a custom Realm .

SecurityManager bean configuration:

@Bean(name = "securityManager")
public DefaultWebSecurityManager securityManager(SessionManager sessionManager, MemoryConstrainedCacheManager memoryConstrainedCacheManager) {
    DefaultWebSecurityManager dwsm = new DefaultWebSecurityManager();
    dwsm.setSessionManager(sessionManager);
    dwsm.setCacheManager(memoryConstrainedCacheManager);
    dwsm.setAuthenticator(modularRealmAuthenticator());
    return dwsm;
}

ModularRealmAuthenticator bean (first‑successful strategy):

@Bean
public ModularRealmAuthenticator modularRealmAuthenticator() {
    UserModularRealmAuthenticator modularRealmAuthenticator = new UserModularRealmAuthenticator();
    modularRealmAuthenticator.setAuthenticationStrategy(new FirstSuccessfulStrategy());
    return modularRealmAuthenticator;
}

ShiroFilterFactoryBean bean, which defines login, unauthorized, and success URLs and registers custom filters:

@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager sessionManager) {
    // Construct ShiroFilterFactoryBean
    ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
    // Set login path
    shiroFilterFactoryBean.setLoginUrl("/login");
    // Must set SecurityManager
    shiroFilterFactoryBean.setSecurityManager(sessionManager);
    // Set unauthorized path
    shiroFilterFactoryBean.setUnauthorizedUrl("/403");
    // Set success path
    shiroFilterFactoryBean.setSuccessUrl("/");
    // Add custom filters
    shiroFilterFactoryBean.getFilters().put("apikey", new ApiKeyFilter());
    shiroFilterFactoryBean.getFilters().put("csrf", new CsrfFilter());
    shiroFilterFactoryBean.getFilters().put("user", new UserAuthcFilter());
    // Define filter chain map
    Map
filterChainDefinitionMap = shiroFilterFactoryBean.getFilterChainDefinitionMap();
    ShiroUtils.loadBaseFilterChain(filterChainDefinitionMap);
    ShiroUtils.ignoreCsrfFilter(filterChainDefinitionMap);
    filterChainDefinitionMap.put("/**", "apikey, csrf, authc");
    return shiroFilterFactoryBean;
}

Base filter chain (anonymous access) and CSRF‑ignore filter (authenticated access) are defined as static helper methods:

public static void loadBaseFilterChain(Map
filterChainDefinitionMap) {
    filterChainDefinitionMap.put("/resource/**", "anon");
    filterChainDefinitionMap.put("/*.worker.js", "anon");
    filterChainDefinitionMap.put("/login", "anon");
    filterChainDefinitionMap.put("/signin", "anon");
}
public static void ignoreCsrfFilter(Map
filterChainDefinitionMap) {
    filterChainDefinitionMap.put("/", "apikey, authc"); // no CSRF check
    filterChainDefinitionMap.put("/language", "apikey, authc");
    filterChainDefinitionMap.put("/test/case/file/preview/**", "apikey, authc");
}

Because directly injecting a Realm into the SecurityManager may cause transaction issues, an @EventListener method is used to set realms after the application context is refreshed:

@EventListener
public void handleContextRefresh(ContextRefreshedEvent event) {
    ApplicationContext context = event.getApplicationContext();
    List
realmList = new ArrayList<>();
    LocalRealm localRealm = context.getBean(LocalRealm.class);
    LdapRealm ldapRealm = context.getBean(LdapRealm.class);
    realmList.add(localRealm);
    realmList.add(ldapRealm);
    context.getBean(DefaultWebSecurityManager.class).setRealms(realmList);
}

A custom LdapRealm extends AuthorizingRealm and implements authentication and authorization logic.

Authentication method (login verification):

@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
    // Construct UsernamePasswordToken
    UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
    String userId = token.getUsername();
    String password = String.valueOf(token.getPassword());
    return loginLdapMode(userId, password);
}

Helper method that retrieves the user and returns a SimpleAuthenticationInfo object:

private AuthenticationInfo loginLdapMode(String userId, String password) {
    String email = (String) SecurityUtils.getSubject().getSession().getAttribute("email");
    UserDTO user = userService.getLoginUser(userId, Arrays.asList(UserSource.LDAP.name(), UserSource.LOCAL.name()));
    if (user == null) {
        user = userService.getUserDTOByEmail(email, UserSource.LDAP.name(), UserSource.LOCAL.name());
        if (user == null) {
            throw new UnknownAccountException(Translator.get("user_not_exist") + userId);
        }
        userId = user.getId();
    }
    SessionUser sessionUser = SessionUser.fromUser(user);
    SessionUtils.putUser(sessionUser);
    return new SimpleAuthenticationInfo(userId, password, getName());
}

Authorization method that loads roles from the user service:

@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
    String userId = (String) principals.getPrimaryPrincipal();
    return getAuthorizationInfo(userId, userService);
}

public static AuthorizationInfo getAuthorizationInfo(String userId, UserService userService) {
    SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
    UserDTO userDTO = userService.getUserDTO(userId);
    Set
roles = userDTO.getRoles().stream().map(Role::getId).collect(Collectors.toSet());
    authorizationInfo.setRoles(roles);
    return authorizationInfo;
}

Login controller demonstrates the authentication flow: the client sends a POST request, the controller builds a UsernamePasswordToken , calls subject.login(token) , and checks subject.isAuthenticated() to confirm success.

@PostMapping(value = "/signin")
public ResultHolder login(@RequestBody LoginRequest request) {
    SessionUser sessionUser = SessionUtils.getUser();
    if (sessionUser != null) {
        if (!StringUtils.equals(sessionUser.getId(), request.getUsername())) {
            return ResultHolder.error(Translator.get("please_logout_current_user"));
        }
    }
    SecurityUtils.getSubject().getSession().setAttribute("authenticate", UserSource.LOCAL.name());
    return userService.login(request);
}
public ResultHolder login(LoginRequest request) {
    String login = (String) SecurityUtils.getSubject().getSession().getAttribute("authenticate");
    String username = StringUtils.trim(request.getUsername());
    String password = "";
    if (!StringUtils.equals(login, UserSource.LDAP.name())) {
        password = StringUtils.trim(request.getPassword());
        // ...
    }
    UsernamePasswordToken token = new UsernamePasswordToken(username, password, login);
    Subject subject = SecurityUtils.getSubject();
    try {
        subject.login(token);
        if (subject.isAuthenticated()) {
            UserDTO user = (UserDTO) subject.getSession().getAttribute(ATTR_USER);
            // ...
            return ResultHolder.success(subject.getSession().getAttribute("user"));
        } else {
            return ResultHolder.error(Translator.get("login_fail"));
        }
    } catch (ExcessiveAttemptsException e) {
        throw new ExcessiveAttemptsException(Translator.get("excessive_attempts"));
    }
    // ...
}

In summary, Apache Shiro is a powerful and flexible open‑source security framework that handles authentication, authorization, and session management. This article demonstrates how to integrate Shiro into a Spring Boot project, configure core components, define custom realms, set up filter chains, and implement a complete login authentication process.

JavaSpring BootsecurityauthenticationauthorizationApache Shiro
360 Quality & Efficiency
Written by

360 Quality & Efficiency

360 Quality & Efficiency focuses on seamlessly integrating quality and efficiency in R&D, sharing 360’s internal best practices with industry peers to foster collaboration among Chinese enterprises and drive greater efficiency value.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.