Design and Implementation of Microservice Permission Control Using Shiro and Shared Session

This article details a microservice permission design using Shiro, covering challenges of integrating Shiro with gateways, shared configuration modules, a hybrid solution with separate Shiro modules, session sharing via Redis, custom CacheManager and Realm implementations, and complete code examples for a Spring Boot project.

Selected Java Interview Questions
Selected Java Interview Questions
Selected Java Interview Questions
Design and Implementation of Microservice Permission Control Using Shiro and Shared Session

Click to join:

Backend Technology Community, Let's Learn Together!

1. Microservice Permission Design

First, I explain why I wrote this article: the actual project requires fine‑grained permission control for every component on the page. I looked at the commonly used permission frameworks online; Shiro and Spring Security are the two main options. After comparison, Shiro is lighter and easier to use.

In this article we also chose Shiro as the permission framework for the whole project, and combined it with several Spring Boot + Shiro integration examples found online. The diagrams are well drawn, but when I tried to implement them myself I ran into many pitfalls.

The concrete implementation turned out to be full of traps. During the practice I considered several solutions; some seemed impossible even before coding, others failed halfway through. I also referred to the RCBA permission design model in this project.

1. Put Shiro and the gateway in the same service. This creates a problem: Shiro's Realm needs user data from the user service (to query users, roles, and permissions). Therefore Shiro would need to use a service‑discovery component (I use Dubbo) to find the user service, but the user service's login also needs Shiro authentication. Some might suggest calling the Shiro service remotely from the user service, but that would tightly couple the two services, so we discard this approach.

2. Share a Shiro configuration module in every service. This has a similar problem: Shiro is a separate module that needs the user service, which can be called via Dubbo, while the user service needs to import the Shiro configuration module via Maven. When the user service starts, it will fail because the Shiro module cannot find a service provider. This approach is also discarded.

Many people have encountered these two schemes. Shiro is still suitable for monolithic architectures, but after a week of research and attempts I finally found a way to design permissions for microservices using Shiro. The following is my solution.

2. Design Scheme

Combining the two infeasible methods above, we take the strengths of each and propose a new scheme.

Scheme 1

Since the user service and the Shiro module need to be separated but still depend on each other, we can configure a dedicated Shiro module for the user service while sharing another Shiro module among other services. These two Shiro modules must share the session.

3. Concrete Implementation

The demo project uses Spring Boot + MySQL + MyBatis‑Plus, with service discovery and registration via Dubbo + Zookeeper (you can also use Eureka + Feign).

3.1 Project Structure

common module: The common module of the whole project. common-core contains constants, return values, and exceptions needed by other microservices. common-cache includes Shiro cache configuration used by all services except the user service. common-auth is the authorization module.

gateway-service: The gateway service, entry point for all other services.

user-api: API definition for the user service.

user-provider-service: Implementation of the user service interface, the provider.

user-consumer-service: The outermost layer of the user service, called by Nginx, the consumer.

video-api: Same as user service API.

video-provider: Same as user service provider.

video-consumer: Same as user service consumer.

3.2 Table Relations

3.3 Shared Session (Cache Module common-cache)

3.3.1 Why Share Session?

Our project consists of multiple microservices. When the user service receives a login request and succeeds, it returns a sessionId stored in the browser cookie. Subsequent requests to the user service carry this sessionId, allowing the server to retrieve the stored user information.

However, if the user then requests the video service, the video service cannot obtain the user information because it does not know whether the user is logged in. Therefore we need to share the logged‑in user information across services, not just within the user service.

3.3.2 How to Implement Shared Session?

When configuring Shiro we need to customize the security manager by overriding DefaultWebSecurityManager. Below is the constructor chain of this class.

<code style="padding: 16px; color: #abb2bf; display: -webkit-box; font-family: Operator Mono, Consolas, Monaco, Menlo, monospace; font-size: 12px"><span style="line-height: 26px"><span style="color: #c678dd; line-height: 26px">public</span> <span style="color: #61aeee; line-height: 26px">DefaultWebSecurityManager</span><span style="line-height: 26px">()</span> </span>{<br/>    <span style="color: #c678dd; line-height: 26px">super</span>();<br/>    ((DefaultSubjectDAO) <span style="color: #c678dd; line-height: 26px">this</span>.subjectDAO).setSessionStorageEvaluator(<span style="color: #c678dd; line-height: 26px">new</span> DefaultWebSessionStorageEvaluator());<br/>    <span style="color: #c678dd; line-height: 26px">this</span>.sessionMode = HTTP_SESSION_MODE;<br/>    setSubjectFactory(<span style="color: #c678dd; line-height: 26px">new</span> DefaultWebSubjectFactory());<br/>    setRememberMeManager(<span style="color: #c678dd; line-height: 26px">new</span> CookieRememberMeManager());<br/>    setSessionManager(<span style="color: #c678dd; line-height: 26px">new</span> ServletContainerSessionManager());<br/>}<br/></code>

The parent class DefaultSecurityManager further creates a DefaultSessionManager, which by default uses MemorySessionDAO to store sessions in memory. To share sessions across services we must replace MemorySessionDAO with a custom implementation, such as EnterpriseCacheSessionDAO backed by Redis.

<code style="padding: 16px; color: #abb2bf; display: -webkit-box; font-family: Operator Mono, Consolas, Monaco, Menlo, monospace; font-size: 12px"><span style="line-height: 26px"><span style="color: #c678dd; line-height: 26px">public</span> <span style="color: #61aeee; line-height: 26px">EnterpriseCacheSessionDAO</span><span style="line-height: 26px">()</span> </span>{<br/>    setCacheManager(<span style="color: #c678dd; line-height: 26px">new</span> AbstractCacheManager() {<br/>        <span style="color: #61aeee; line-height: 26px">@Override</span><br/>        <span style="line-height: 26px"><span style="color: #c678dd; line-height: 26px">protected</span> Cache<Serializable, Session> <span style="color: #61aeee; line-height: 26px">createCache</span><span style="line-height: 26px">(String name)</span> <span style="color: #c678dd; line-height: 26px">throws</span> CacheException </span>{<br/>            <span style="color: #c678dd; line-height: 26px">return</span> <span style="color: #c678dd; line-height: 26px">new</span> MapCache<Serializable, Session>(name, <span style="color: #c678dd; line-height: 26px">new</span> ConcurrentHashMap<Serializable, Session>());<br/>        }<br/>    });<br/>}<br/></code>

Because the default MemorySessionDAO stores sessions in a single JVM's memory, we need a custom SessionDAO that stores sessions in Redis so that all microservices can access the same session data.

3.3.3 Concrete Implementation

Import required dependencies

<code style="padding: 16px; color: #abb2bf; display: -webkit-box; font-family: Operator Mono, Consolas, Monaco, Menlo, monospace; font-size: 12px"><dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
        <exclusions>
            <exclusion>
                <groupId>io.lettuce</groupId>
                <artifactId>lettuce-core</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    <dependency>
        <groupId>redis.clients</groupId>
        <artifactId>jedis</artifactId>
    </dependency>
    <!-- Shiro dependencies -->
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-core</artifactId>
        <version>1.4.0</version>
    </dependency>
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-spring</artifactId>
        <version>1.4.0</version>
    </dependency>
</dependencies></code>

Implement our own CacheManager

<code style="padding: 16px; color: #abb2bf; display: -webkit-box; font-family: Operator Mono, Consolas, Monaco, Menlo, monospace; font-size: 12px"><span style="color: #61aeee; line-height: 26px">@Component</span>("myCacheManager")
public class MyCacheManager implements CacheManager {
    @Override
    public <K, V> Cache<K, V> getCache(String name) throws CacheException {
        return new MyCache();
    }
}</code>

Jedis client (Redis client)

<code style="padding: 16px; color: #abb2bf; display: -webkit-box; font-family: Operator Mono, Consolas, Monaco, Menlo, monospace; font-size: 12px">public class JedisClient {
    private static Logger logger = LoggerFactory.getLogger(JedisClient.class);
    private static JedisPool jedisPool;
    private static final String HOST = "localhost";
    private static final int PORT = 6379;
    private static final String PASSWORD = "1234";
    // pool configuration omitted for brevity
    static { initialPool(); }
    // getJedis(), setValue(), getValue(), delkey(), close() methods ...
}</code>

Custom Cache implementation

<code style="padding: 16px; color: #abb2bf; display: -webkit-box; font-family: Operator Mono, Consolas, Monaco, Menlo, monospace; font-size: 12px">public class MyCache<S, V> implements Cache<Object, Object> {
    private Duration cacheExpireTime = Duration.ofMinutes(30);
    @Override
    public Object get(Object key) throws CacheException {
        byte[] bytes = JedisClient.getValue(objectToBytes(key));
        return bytes == null ? null : (SimpleSession) bytesToObject(bytes);
    }
    @Override
    public Object put(Object key, Object value) throws CacheException {
        JedisClient.setValue(objectToBytes(key), objectToBytes(value), (int) cacheExpireTime.getSeconds());
        return key;
    }
    // objectToBytes, bytesToObject, remove, clear, size, keys, values methods ...
}</code>

Note that objectToBytes and bytesToObject convert the session object to a byte array before storing it in Redis, because Shiro's SimpleSession contains transient fields that cannot be directly serialized.

3.4 Authorization Module common-auth

Import required dependencies (same as above) and the user‑service API.

<code style="padding: 16px; color: #abb2bf; display: -webkit-box; font-family: Operator Mono, Consolas, Monaco, Menlo, monospace; font-size: 12px">public class UserRealm extends AuthorizingRealm {
    @Reference(version = "0.0.1")
    private UserService userService;
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        String userName = (String) principals.getPrimaryPrincipal();
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        info.setRoles(userService.selectRolesByUsername(userName));
        info.setStringPermissions(userService.selectPermissionByUsername(userName));
        return info;
    }
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        // authentication not implemented here, return null
        return null;
    }
}
</code>

3.5 User Consumer Service (user‑consumer)

The service imports the cache module common-cache and uses the custom Shiro configuration. The Realm implements both authentication and authorization.

<code style="padding: 16px; color: #abb2bf; display: -webkit-box; font-family: Operator Mono, Consolas, Monaco, Menlo, monospace; font-size: 12px">@Component
public class UserRealm extends AuthorizingRealm {
    @Reference(version = "0.0.1")
    private UserService userService;
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        String userName = (String) principals.getPrimaryPrincipal();
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        info.setRoles(userService.selectRolesByUsername(userName));
        info.setStringPermissions(userService.selectPermissionByUsername(userName));
        return info;
    }
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        String userName = (String) token.getPrincipal();
        User user = userService.selectByUsername(userName);
        if (user != null) {
            return new SimpleAuthenticationInfo(user.getUsername(), user.getPassword(), "myRealm");
        }
        return null;
    }
}
</code>

Shiro configuration for this service:

<code style="padding: 16px; color: #abb2bf; display: -webkit-box; font-family: Operator Mono, Consolas, Monaco, Menlo, monospace; font-size: 12px">@Configuration
public class ShiroConfig {
    @Bean(name = "shiroFilterFactoryBean")
    public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("SecurityManager") DefaultWebSecurityManager securityManager) {
        ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
        bean.setSecurityManager(securityManager);
        Map<String, String> filterMap = new LinkedHashMap<>();
        filterMap.put("/**", "authc");
        bean.setFilterChainDefinitionMap(filterMap);
        bean.setUnauthorizedUrl("/user/unAuth");
        return bean;
    }
    @Bean(name = "SecurityManager")
    public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("userRealm") UserRealm userRealm,
                                                                   @Qualifier("myDefaultWebSessionManager") DefaultWebSessionManager sessionManager,
                                                                   @Qualifier("myCacheManager") MyCacheManager cacheManager) {
        DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
        manager.setRealm(userRealm);
        manager.setSessionManager(sessionManager);
        manager.setCacheManager(cacheManager);
        return manager;
    }
    @Bean
    public UserRealm userRealm() { return new UserRealm(); }
    @Bean
    @ConditionalOnMissingBean
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator creator = new DefaultAdvisorAutoProxyCreator();
        creator.setProxyTargetClass(true);
        return creator;
    }
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;
    }
    @Bean
    public DefaultWebSessionManager myDefaultWebSessionManager(SimpleCookie simpleCookie) {
        DefaultWebSessionManager manager = new DefaultWebSessionManager();
        manager.setSessionDAO(new EnterpriseCacheSessionDAO());
        manager.setSessionIdCookie(simpleCookie);
        return manager;
    }
    @Bean
    public SimpleCookie simpleCookie() {
        SimpleCookie cookie = new SimpleCookie("myCookie");
        cookie.setPath("/");
        cookie.setMaxAge(30);
        return cookie;
    }
}
</code>

Exception handling for unauthorized access:

<code style="padding: 16px; color: #abb2bf; display: -webkit-box; font-family: Operator Mono, Consolas, Monaco, Menlo, monospace; font-size: 12px">@Configuration
public class AuthorizationExceptionConfig {
    @Bean
    public SimpleMappingExceptionResolver simpleMappingExceptionResolver() {
        SimpleMappingExceptionResolver resolver = new SimpleMappingExceptionResolver();
        Properties props = new Properties();
        props.setProperty("org.apache.shiro.authz.AuthorizationException", "/user/unAuth");
        resolver.setExceptionMappings(props);
        return resolver;
    }
}
</code>

User login controller (simplified):

<code style="padding: 16px; color: #abb2bf; display: -webkit-box; font-family: Operator Mono, Consolas, Monaco, Menlo, monospace; font-size: 12px">@RestController
@RequestMapping("/user")
public class UserController {
    @Reference(version = "0.0.1")
    private UserService userService;
    @PostMapping("/login")
    public R login(@RequestBody User user) {
        UsernamePasswordToken token = new UsernamePasswordToken(user.getUsername(), user.getPassword());
        Subject subject = SecurityUtils.getSubject();
        try {
            subject.login(token);
            return R.ok();
        } catch (Exception e) {
            return R.failed();
        }
    }
    @GetMapping("/unAuth")
    public R unAuth() { return R.failed("User not authorized!"); }
    @RequiresRoles("admin")
    @GetMapping("/testFunc")
    public R testFunc() { return R.ok("yes success!!!"); }
}
</code>

After logging in, the user can access the protected endpoint /user/testFunc. If the user lacks the admin role, access is denied.

3.6 Video Consumer Service (video‑consumer)

This service tests whether the shared session works across microservices. The controller simply returns R.ok().

<code style="padding: 16px; color: #abb2bf; display: -webkit-box; font-family: Operator Mono, Consolas, Monaco, Menlo, monospace; font-size: 12px">@RestController
@RequestMapping("/video")
public class VideoController {
    @GetMapping("/getVideo")
    public R getVideo() { return R.ok(); }
}
</code>

When accessed without logging in, Shiro redirects to its default login page. After logging in through the user service, the session is shared and the video service can retrieve the user information.

Adding @RequiresRoles("admin") to the video endpoint demonstrates that permission checks also work across services. If the user does not have the required role, the request is redirected to the unauthorized page configured earlier.

The tests confirm that the common‑auth module successfully shares session and permission data via Redis, achieving seamless authentication and authorization across microservices.

<section style="letter-spacing: 0px; line-height: 1.6"><section><br/></section></section><p style='font-family: -apple-system-font, BlinkMacSystemFont, "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif; letter-spacing: 0.544px; color: rgb(62, 62, 62); text-align: center'><span style="color: rgb(214, 168, 65)"><strong><span>Instead of searching for interview questions online?</span></strong><strong><span> Follow us now~</span></strong></span></p><p style='font-family: -apple-system-font, BlinkMacSystemFont, "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif; letter-spacing: 0.544px; text-align: center'><img src="https://mmbiz.qpic.cn/mmbiz_png/8KKrHK5ic6XCNL57sCQk4JF8FIsgp3eAkKtbZL3DXIBPPnsGiccE1XAySvQ1bLrFq9Z0j57WME3ianED3WGTkxWSw/640?wx_fmt=png&wxfrom=5&wx_lazy=1&wx_co=1" style="display: initial; width: 677px !important"/></p><p style='font-family: -apple-system-font, BlinkMacSystemFont, "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif; letter-spacing: 0.544px'><br/></p><p style='font-family: -apple-system-font, BlinkMacSystemFont, "Helvetica Neue", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei UI", "Microsoft YaHei", Arial, sans-serif; letter-spacing: 0.544px'><strong><span style="font-family: Optima-Regular, PingFangTC-light; letter-spacing: 0.544px; color: rgb(255, 0, 0); font-size: 18px">↓ Click to read the original article, Java interview questions are all here!</span></strong></p>
Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

Selected Java Interview Questions
Written by

Selected Java Interview Questions

A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!

0 followers
Reader feedback

How this landed with the community

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.