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