Information Security 27 min read

Integrating Apache Shiro with Spring Boot: Configuration, Session Management, and Permission Control

This tutorial demonstrates how to integrate the lightweight Apache Shiro security framework into a Spring Boot 2.1.5 project, covering environment setup, Maven dependencies, Redis session storage, custom utilities, Shiro configuration, permission annotations, test controllers, and Postman verification.

Java Captain
Java Captain
Java Captain
Integrating Apache Shiro with Spring Boot: Configuration, Session Management, and Permission Control

1. Introduction

Shiro is a security framework used for authentication, authorization, encryption, and session management. Although it does not have as many features as Spring Security, it is lightweight and can meet most business requirements.

2. Project Environment

MyBatis-Plus version: 3.1.0

SpringBoot version: 2.1.5

JDK version: 1.8

Shiro version: 1.4

Shiro‑redis plugin version: 3.1.0

The test database contains user passwords encrypted with the value 123456 .

3. Maven Dependencies

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
    </dependency>
    <!-- AOP dependency, required for permission interception to work -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
    <!-- Lombok plugin -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <!-- Redis -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
    </dependency>
    <!-- MyBatis‑Plus core library -->
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.1.0</version>
    </dependency>
    <!-- Alibaba Druid connection pool -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>1.1.6</version>
    </dependency>
    <!-- Shiro core dependency -->
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-spring</artifactId>
        <version>1.4.0</version>
    </dependency>
    <!-- Shiro‑redis plugin -->
    <dependency>
        <groupId>org.crazycake</groupId>
        <artifactId>shiro-redis</artifactId>
        <version>3.1.0</version>
    </dependency>
    <!-- Apache Commons Lang3 -->
    <dependency>
        <groupId>org.apache.commons</groupId>
        <artifactId>commons-lang3</artifactId>
        <version>3.5</version>
    </dependency>
</dependencies>

4. Application Configuration (application.yml)

# Server port
server:
  port: 8764

spring:
  # Data source configuration
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/my_shiro?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false
    username: root
    password: root
    type: com.alibaba.druid.pool.DruidDataSource
  # Redis configuration
  redis:
    host: localhost
    port: 6379
    timeout: 6000
    password: 123456
    jedis:
      pool:
        max-active: 1000  # maximum connections (negative = no limit)
        max-wait: -1      # maximum wait time (negative = no limit)
        max-idle: 10      # maximum idle connections
        min-idle: 5       # minimum idle connections

# MyBatis‑Plus configuration
mybatis-plus:
  mapper-locations: classpath:mapper/*.xml
  global-config:
    db-config:
      id-type: auto
      field-strategy: NOT_EMPTY
      db-type: MYSQL
  configuration:
    map-underscore-to-camel-case: true
    call-setters-on-nulls: true
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

5. Basic Project Classes

Entity, DAO, and Service classes are omitted for brevity; refer to the source code.

5.1 Custom Exception for Shiro Permission Interception

@ControllerAdvice
public class MyShiroException {
    /**
     * Handles Shiro permission interception exceptions.
     * Add @ResponseBody if JSON response is required.
     */
    @ResponseBody
    @ExceptionHandler(value = AuthorizationException.class)
    public Map
defaultErrorHandler(){
        Map
map = new HashMap<>();
        map.put("403","权限不足");
        return map;
    }
}

5.2 SHA‑256 Encryption Utility

public class SHA256Util {
    /** Private constructor */
    private SHA256Util(){};
    /** Hash algorithm name */
    public final static String HASH_ALGORITHM_NAME = "SHA-256";
    /** Number of hash iterations */
    public final static int HASH_ITERATIONS = 15;
    /** Performs SHA‑256 hashing with salt */
    public static String sha256(String password, String salt) {
        return new SimpleHash(HASH_ALGORITHM_NAME, password, salt, HASH_ITERATIONS).toString();
    }
}

5.3 Spring Context Utility

@Component
public class SpringUtil implements ApplicationContextAware {
    private static ApplicationContext context;
    /** Called by Spring after bean initialization */
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        context = applicationContext;
    }
    /** Retrieves a bean by its class */
    public static
T getBean(Class
beanClass) {
        return context.getBean(beanClass);
    }
}

5.4 Shiro Utility

public class ShiroUtils {
    /** Private constructor */
    private ShiroUtils(){}
    private static RedisSessionDAO redisSessionDAO = SpringUtil.getBean(RedisSessionDAO.class);
    /** Gets the current user's session */
    public static Session getSession(){
        return SecurityUtils.getSubject().getSession();
    }
    /** Logs out the current user */
    public static void logout(){
        SecurityUtils.getSubject().logout();
    }
    /** Retrieves the current user information */
    public static SysUserEntity getUserInfo(){
        return (SysUserEntity) SecurityUtils.getSubject().getPrincipal();
    }
    /** Deletes user cache and optionally the session */
    public static void deleteCache(String username, boolean isRemoveSession){
        Session session = null;
        Collection
sessions = redisSessionDAO.getActiveSessions();
        for(Session sessionInfo : sessions){
            Object attribute = sessionInfo.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);
            if(attribute == null) continue;
            SysUserEntity sysUserEntity = (SysUserEntity) ((SimplePrincipalCollection) attribute).getPrimaryPrincipal();
            if(sysUserEntity == null) continue;
            if(Objects.equals(sysUserEntity.getUsername(), username)){
                session = sessionInfo;
            }
        }
        if(session == null || attribute == null) return;
        if(isRemoveSession){
            redisSessionDAO.delete(session);
        }
        DefaultWebSecurityManager securityManager = (DefaultWebSecurityManager) SecurityUtils.getSecurityManager();
        Authenticator authc = securityManager.getAuthenticator();
        ((LogoutAware) authc).onLogout((SimplePrincipalCollection) attribute);
    }
}

5.5 Custom SessionId Generator

public class ShiroSessionIdGenerator implements SessionIdGenerator {
    @Override
    public Serializable generateId(Session session) {
        Serializable sessionId = new JavaUuidSessionIdGenerator().generateId(session);
        return String.format("login_token_%s", sessionId);
    }
}

6. Shiro Core Classes

6.1 Realm for Authorization and Authentication

public class ShiroRealm extends AuthorizingRealm {
    @Autowired
    private SysUserService sysUserService;
    @Autowired
    private SysRoleService sysRoleService;
    @Autowired
    private SysMenuService sysMenuService;
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        SysUserEntity sysUserEntity = (SysUserEntity) principalCollection.getPrimaryPrincipal();
        Long userId = sysUserEntity.getUserId();
        Set
rolesSet = new HashSet<>();
        Set
permsSet = new HashSet<>();
        List
sysRoleEntityList = sysRoleService.selectSysRoleByUserId(userId);
        for(SysRoleEntity sysRoleEntity : sysRoleEntityList){
            rolesSet.add(sysRoleEntity.getRoleName());
            List
sysMenuEntityList = sysMenuService.selectSysMenuByRoleId(sysRoleEntity.getRoleId());
            for(SysMenuEntity sysMenuEntity : sysMenuEntityList){
                permsSet.add(sysMenuEntity.getPerms());
            }
        }
        authorizationInfo.setStringPermissions(permsSet);
        authorizationInfo.setRoles(rolesSet);
        return authorizationInfo;
    }
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        String username = (String) authenticationToken.getPrincipal();
        SysUserEntity user = sysUserService.selectUserByName(username);
        if(user == null){
            throw new AuthenticationException();
        }
        if(user.getState() == null || user.getState().equals("PROHIBIT")){
            throw new LockedAccountException();
        }
        SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
                user,
                user.getPassword(),
                ByteSource.Util.bytes(user.getSalt()),
                getName()
        );
        ShiroUtils.deleteCache(username, true);
        return authenticationInfo;
    }
}

6.2 Session Manager (Token from Header)

public class ShiroSessionManager extends DefaultWebSessionManager {
    private static final String AUTHORIZATION = "Authorization";
    private static final String REFERENCED_SESSION_ID_SOURCE = "Stateless request";
    public ShiroSessionManager(){
        super();
        this.setDeleteInvalidSessions(true);
    }
    @Override
    public Serializable getSessionId(ServletRequest request, ServletResponse response) {
        String token = WebUtils.toHttp(request).getHeader(AUTHORIZATION);
        if(!StringUtils.isEmpty(token)){
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, REFERENCED_SESSION_ID_SOURCE);
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, token);
            request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE);
            return token;
        } else {
            return super.getSessionId(request, response);
        }
    }
}

6.3 Shiro Configuration

@Configuration
public class ShiroConfig {
    private final String CACHE_KEY = "shiro:cache:";
    private final String SESSION_KEY = "shiro:session:";
    private final int EXPIRE = 1800;
    // Redis properties
    @Value("${spring.redis.host}")
    private String host;
    @Value("${spring.redis.port}")
    private int port;
    @Value("${spring.redis.timeout}")
    private int timeout;
    @Value("${spring.redis.password}")
    private String password;
    /** Enable Shiro AOP annotations */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;
    }
    /** Shiro filter configuration */
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactory(SecurityManager securityManager){
        ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
        bean.setSecurityManager(securityManager);
        Map
filterChain = new LinkedHashMap<>();
        filterChain.put("/static/**","anon");
        filterChain.put("/userLogin/**","anon");
        filterChain.put("/**","authc");
        bean.setLoginUrl("/userLogin/unauth");
        bean.setFilterChainDefinitionMap(filterChain);
        return bean;
    }
    /** Security manager */
    @Bean
    public SecurityManager securityManager(){
        DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
        manager.setSessionManager(sessionManager());
        manager.setCacheManager(cacheManager());
        manager.setRealm(shiroRealm());
        return manager;
    }
    /** Realm bean */
    @Bean
    public ShiroRealm shiroRealm(){
        ShiroRealm realm = new ShiroRealm();
        realm.setCredentialsMatcher(hashedCredentialsMatcher());
        return realm;
    }
    /** Credentials matcher using SHA‑256 */
    @Bean
    public HashedCredentialsMatcher hashedCredentialsMatcher(){
        HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
        matcher.setHashAlgorithmName(SHA256Util.HASH_ALGORITHM_NAME);
        matcher.setHashIterations(SHA256Util.HASH_ITERATIONS);
        return matcher;
    }
    /** Redis manager */
    @Bean
    public RedisManager redisManager(){
        RedisManager manager = new RedisManager();
        manager.setHost(host);
        manager.setPort(port);
        manager.setTimeout(timeout);
        manager.setPassword(password);
        return manager;
    }
    /** Cache manager (stores permissions/roles in Redis) */
    @Bean
    public RedisCacheManager cacheManager(){
        RedisCacheManager manager = new RedisCacheManager();
        manager.setRedisManager(redisManager());
        manager.setKeyPrefix(CACHE_KEY);
        manager.setPrincipalIdFieldName("userId");
        return manager;
    }
    /** SessionId generator bean */
    @Bean
    public ShiroSessionIdGenerator sessionIdGenerator(){
        return new ShiroSessionIdGenerator();
    }
    /** RedisSessionDAO bean */
    @Bean
    public RedisSessionDAO redisSessionDAO(){
        RedisSessionDAO dao = new RedisSessionDAO();
        dao.setRedisManager(redisManager());
        dao.setSessionIdGenerator(sessionIdGenerator());
        dao.setKeyPrefix(SESSION_KEY);
        dao.setExpire(EXPIRE);
        return dao;
    }
    /** Session manager bean */
    @Bean
    public SessionManager sessionManager(){
        ShiroSessionManager manager = new ShiroSessionManager();
        manager.setSessionDAO(redisSessionDAO());
        return manager;
    }
}

7. Permission Control with Shiro Annotations

Shiro provides five annotations. The most commonly used are @RequiresPermissions and @RequiresRoles . Multiple roles or permissions can be specified; the default logical operator is AND, but it can be changed to OR via the logical attribute.

// Access if the user has any of the listed roles
@RequiresRoles(value={"ADMIN","USER"}, logical = Logical.OR)
// Access only if the user has all listed permissions
@RequiresPermissions(value={"sys:user:info","sys:role:info"}, logical = Logical.AND)

The order of annotation checking is: RequiresRoles → RequiresPermissions → RequiresAuthentication → RequiresUser → RequiresGuest .

8. Test Controllers

8.1 UserRoleController (role‑based access)

@RestController
@RequestMapping("/role")
public class UserRoleController {
    @Autowired
    private SysUserService sysUserService;
    @Autowired
    private SysRoleService sysRoleService;
    @Autowired
    private SysMenuService sysMenuService;
    @Autowired
    private SysRoleMenuService sysRoleMenuService;
    @RequestMapping("/getAdminInfo")
    @RequiresRoles("ADMIN")
    public Map
getAdminInfo(){
        Map
map = new HashMap<>();
        map.put("code",200);
        map.put("msg","这里是只有管理员角色能访问的接口");
        return map;
    }
    @RequestMapping("/getUserInfo")
    @RequiresRoles("USER")
    public Map
getUserInfo(){
        Map
map = new HashMap<>();
        map.put("code",200);
        map.put("msg","这里是只有用户角色能访问的接口");
        return map;
    }
    @RequestMapping("/getRoleInfo")
    @RequiresRoles(value={"ADMIN","USER"}, logical = Logical.OR)
    @RequiresUser
    public Map
getRoleInfo(){
        Map
map = new HashMap<>();
        map.put("code",200);
        map.put("msg","这里是只要有ADMIN或者USER角色能访问的接口");
        return map;
    }
    @RequestMapping("/getLogout")
    @RequiresUser
    public Map
getLogout(){
        ShiroUtils.logout();
        Map
map = new HashMap<>();
        map.put("code",200);
        map.put("msg","登出");
        return map;
    }
}

8.2 UserMenuController (permission‑based access)

@RestController
@RequestMapping("/menu")
public class UserMenuController {
    @Autowired
    private SysUserService sysUserService;
    @Autowired
    private SysRoleService sysRoleService;
    @Autowired
    private SysMenuService sysMenuService;
    @Autowired
    private SysRoleMenuService sysRoleMenuService;
    @RequestMapping("/getUserInfoList")
    @RequiresPermissions("sys:user:info")
    public Map
getUserInfoList(){
        Map
map = new HashMap<>();
        List
list = sysUserService.list();
        map.put("sysUserEntityList",list);
        return map;
    }
    @RequestMapping("/getRoleInfoList")
    @RequiresPermissions("sys:role:info")
    public Map
getRoleInfoList(){
        Map
map = new HashMap<>();
        List
list = sysRoleService.list();
        map.put("sysRoleEntityList",list);
        return map;
    }
    @RequestMapping("/getMenuInfoList")
    @RequiresPermissions("sys:menu:info")
    public Map
getMenuInfoList(){
        Map
map = new HashMap<>();
        List
list = sysMenuService.list();
        map.put("sysMenuEntityList",list);
        return map;
    }
    @RequestMapping("/getInfoAll")
    @RequiresPermissions("sys:info:all")
    public Map
getInfoAll(){
        Map
map = new HashMap<>();
        map.put("sysUserEntityList", sysUserService.list());
        map.put("sysRoleEntityList", sysRoleService.list());
        map.put("sysMenuEntityList", sysMenuService.list());
        return map;
    }
    @RequestMapping("/addMenu")
    public Map
addMenu(){
        // Add permission for ADMIN role
        SysRoleMenuEntity entity = new SysRoleMenuEntity();
        entity.setMenuId(4L);
        entity.setRoleId(1L);
        sysRoleMenuService.save(entity);
        // Clear cache for ADMIN user
        String username = "admin";
        ShiroUtils.deleteCache(username,false);
        Map
map = new HashMap<>();
        map.put("code",200);
        map.put("msg","权限添加成功");
        return map;
    }
}

8.3 UserLoginController (login & unauth handling)

@RestController
@RequestMapping("/userLogin")
public class UserLoginController {
    @Autowired
    private SysUserService sysUserService;
    @RequestMapping("/login")
    public Map
login(@RequestBody SysUserEntity sysUserEntity){
        Map
map = new HashMap<>();
        try{
            Subject subject = SecurityUtils.getSubject();
            UsernamePasswordToken token = new UsernamePasswordToken(sysUserEntity.getUsername(), sysUserEntity.getPassword());
            subject.login(token);
        }catch(IncorrectCredentialsException e){
            map.put("code",500);
            map.put("msg","用户不存在或者密码错误");
            return map;
        }catch(LockedAccountException e){
            map.put("code",500);
            map.put("msg","登录失败,该用户已被冻结");
            return map;
        }catch(AuthenticationException e){
            map.put("code",500);
            map.put("msg","该用户不存在");
            return map;
        }catch(Exception e){
            map.put("code",500);
            map.put("msg","未知异常");
            return map;
        }
        map.put("code",0);
        map.put("msg","登录成功");
        map.put("token",ShiroUtils.getSession().getId().toString());
        return map;
    }
    @RequestMapping("/unauth")
    public Map
unauth(){
        Map
map = new HashMap<>();
        map.put("code",500);
        map.put("msg","未登录");
        return map;
    }
}

9. POSTMAN Testing

After a successful login, a token is returned. Because the system uses single‑sign‑on, a subsequent login generates a new token and invalidates the previous one in Redis.

When an interface is accessed for the first time, Shiro stores the permission data in the cache. Subsequent requests retrieve permissions from the cache, provided the request header contains the token.

If a user (e.g., ADMIN) lacks a specific permission such as sys:info:all , the request is denied. After dynamically assigning the permission and clearing the cache, Shiro re‑authorizes the user and grants access.

10. Project Source Code

https://gitee.com/liselotte/spring-boot-shiro-demo https://github.com/xuyulong2017/my-java-demo

(End)

JavaRedisSpring BootsecurityAuthenticationauthorizationshiro
Java Captain
Written by

Java Captain

Focused on Java technologies: SSM, the Spring ecosystem, microservices, MySQL, MyCat, clustering, distributed systems, middleware, Linux, networking, multithreading; occasionally covers DevOps tools like Jenkins, Nexus, Docker, ELK; shares practical tech insights and is dedicated to full‑stack Java development.

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.