Implementing Role-Based Access Control in Spring Boot with Apache Shiro and MyBatis

Project Dependencies

To integrate Apache Shiro into a Spring Boot environment using Gradle, include the necessary starters for web support, MyBatis, and the Shiro core libraries. Below is the configuration for build.gradle:

plugins {
    id 'org.springframework.boot' version '2.7.5'
    id 'io.spring.dependency-management' version '1.0.15.RELEASE'
    id 'java'
}

group = 'com.example'
version = '1.0.0'
sourceCompatibility = '11'

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.mybatis.spring.boot:mybatis-spring-boot-starter:2.2.2'
    implementation 'org.apache.shiro:shiro-spring:1.10.0'
    implementation 'org.apache.shiro:shiro-ehcache:1.10.0'
    implementation 'mysql:mysql-connector-java'
    implementation 'org.apache.commons:commons-lang3:3.12.0'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

Database Schema Design

The permission system follows the RBAC (Role-Based Access Control) model, involving users, roles, and permissions (modules).

CREATE TABLE `account` (
  `id` INT AUTO_INCREMENT PRIMARY KEY,
  `username` VARCHAR(50) NOT NULL,
  `password` VARCHAR(100) NOT NULL
);

CREATE TABLE `access_role` (
  `id` INT AUTO_INCREMENT PRIMARY KEY,
  `role_name` VARCHAR(50) DEFAULT NULL
);

CREATE TABLE `permission` (
  `id` INT AUTO_INCREMENT PRIMARY KEY,
  `perm_name` VARCHAR(50) DEFAULT NULL
);

CREATE TABLE `account_role_link` (
  `user_id` INT,
  `role_id` INT,
  FOREIGN KEY (`user_id`) REFERENCES `account` (`id`),
  FOREIGN KEY (`role_id`) REFERENCES `access_role` (`id`)
);

CREATE TABLE `role_permission_link` (
  `role_id` INT,
  `perm_id` INT,
  FOREIGN KEY (`role_id`) REFERENCES `access_role` (`id`),
  FOREIGN KEY (`perm_id`) REFERENCES `permission` (`id`)
);

Data Models and Mapper

Define the entity classes to represent the security structure. Use a many-to-many relationship mapping between accounts, roles, and permissions.

public class Account implements Serializable {
    private Integer id;
    private String username;
    private String password;
    private Set<AccessRole> roles;
    // Getters and Setters omitted for brevity
}

public interface AccountMapper {
    Account selectAccountWithSecurityDetails(String username);
}

In the AccountMapper.xml, perform a nested join to fetch the full permisssion tree for a user:

<mapper namespace="com.example.repository.AccountMapper">
    <resultMap id="AccountResultMap" type="com.example.entity.Account">
        <id property="id" column="id"/>
        <result property="username" column="username"/>
        <result property="password" column="password"/>
        <collection property="roles" ofType="com.example.entity.AccessRole">
            <id property="id" column="role_id"/>
            <result property="roleName" column="role_name"/>
            <collection property="permissions" ofType="com.example.entity.Permission">
                <id property="id" column="perm_id"/>
                <result property="permName" column="perm_name"/>
            </collection>
        </collection>
    </resultMap>

    <select id="selectAccountWithSecurityDetails" resultMap="AccountResultMap">
        SELECT a.*, r.id AS role_id, r.role_name, p.id AS perm_id, p.perm_name
        FROM account a
        LEFT JOIN account_role_link arl ON a.id = arl.user_id
        LEFT JOIN access_role r ON arl.role_id = r.id
        LEFT JOIN role_permission_link rpl ON r.id = rpl.role_id
        LEFT JOIN permission p ON rpl.perm_id = p.id
        WHERE a.username = #{username}
    </select>
</mapper>

Custom Shiro Realm

The AuthorizingRealm handles both authentication (verifying idetnity) and authorization (checking permissions).

public class SecurityRealm extends AuthorizingRealm {

    @Autowired
    private AccountService accountService;

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        UsernamePasswordToken upToken = (UsernamePasswordToken) token;
        Account account = accountService.findByUsername(upToken.getUsername());

        if (account == null) return null;

        return new SimpleAuthenticationInfo(account, account.getPassword(), getName());
    }

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        Account account = (Account) principals.getPrimaryPrincipal();
        SimpleAuthorizationInfo authInfo = new SimpleAuthorizationInfo();

        account.getRoles().forEach(role -> {
            authInfo.addRole(role.getRoleName());
            role.getPermissions().forEach(perm -> 
                authInfo.addStringPermission(perm.getPermName())
            );
        });

        return authInfo;
    }
}

Shiro Configuration

Configure the ShiroFilterFactoryBean and SecurityManager to define protected routes and the authentication strategy.

@Configuration
public class ShiroConfig {

    @Bean
    public SecurityManager securityManager(SecurityRealm realm) {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        securityManager.setRealm(realm);
        return securityManager;
    }

    @Bean
    public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
        ShiroFilterFactoryBean filterFactory = new ShiroFilterFactoryBean();
        filterFactory.setSecurityManager(securityManager);

        Map<String, String> chainDefinition = new LinkedHashMap<>();
        chainDefinition.put("/auth/login", "anon");
        chainDefinition.put("/public/**", "anon");
        chainDefinition.put("/logout", "logout");
        chainDefinition.put("/**", "authc");

        filterFactory.setLoginUrl("/auth/login");
        filterFactory.setSuccessUrl("/dashboard");
        filterFactory.setUnauthorizedUrl("/error/403");
        filterFactory.setFilterChainDefinitionMap(chainDefinition);

        return filterFactory;
    }

    @Bean
    public SecurityRealm securityRealm() {
        SecurityRealm realm = new SecurityRealm();
        realm.setCredentialsMatcher(new SimpleCredentialsMatcher());
        return realm;
    }
}

Authentication Controller

The controller handles the login process by creating a UsernamePasswordToken and passing it to Shiro's Subject.

@Controller
@RequestMapping("/auth")
public class AuthenticationController {

    @PostMapping("/login")
    public String executeLogin(String username, String password, HttpSession session) {
        Subject currentUser = SecurityUtils.getSubject();
        UsernamePasswordToken token = new UsernamePasswordToken(username, password);

        try {
            currentUser.login(token);
            Account account = (Account) currentUser.getPrincipal();
            session.setAttribute("current_user", account);
            return "redirect:/dashboard";
        } catch (AuthenticationException e) {
            return "login-error";
        }
    }

    @GetMapping("/logout")
    public String logout() {
        SecurityUtils.getSubject().logout();
        return "redirect:/auth/login";
    }
}

Tags: Spring Boot Apache Shiro RBAC Java Security MyBatis

Posted on Wed, 28 Oct 2026 16:33:23 +0000 by nainil