Spring Security vs Apache Shiro vs Sa-Token: Which Java Security Framework Should You Choose?

This article compares three popular Java security frameworks—Spring Security, Apache Shiro, and Sa-Token—by examining their architectures, code examples, advantages, drawbacks, and ideal use‑cases, helping developers decide which solution best fits their project's requirements.

Su San Talks Tech
Su San Talks Tech
Su San Talks Tech
Spring Security vs Apache Shiro vs Sa-Token: Which Java Security Framework Should You Choose?

Why Use a Security Framework?

Hand‑coding authentication, authorization, CSRF protection, session handling and audit logging leads to duplicated code, tight coupling between business and security logic, and a high risk of security gaps. A dedicated framework abstracts these concerns, standardises APIs and automates common security tasks.

Spring Security

Core Architecture – Filter Chain

Incoming HTTP requests pass through a configurable chain of security filters. Each filter handles a specific concern such as authentication, authorization, CSRF protection or CORS handling.

Spring Security filter chain
Spring Security filter chain

Basic Configuration Example

@Configuration
@EnableWebSecurity
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
            .authorizeRequests()
                .antMatchers("/api/public/**").permitAll()
                .antMatchers("/api/admin/**").hasRole("ADMIN")
                .antMatchers("/api/user/**").hasAnyRole("USER", "ADMIN")
                .anyRequest().authenticated()
            .and().formLogin().disable()
            .httpBasic()
            .and().exceptionHandling()
                .authenticationEntryPoint(restAuthenticationEntryPoint())
                .accessDeniedHandler(restAccessDeniedHandler());
    }
    @Bean
    public UserDetailsService userDetailsService() {
        return username -> {
            if ("admin".equals(username)) {
                return User.withUsername("admin")
                           .password(passwordEncoder().encode("admin123"))
                           .roles("ADMIN")
                           .build();
            } else if ("user".equals(username)) {
                return User.withUsername("user")
                           .password(passwordEncoder().encode("user123"))
                           .roles("USER")
                           .build();
            }
            throw new UsernameNotFoundException("User not found: " + username);
        };
    }
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    @Bean
    public AuthenticationEntryPoint restAuthenticationEntryPoint() {
        return (request, response, authException) -> {
            response.setContentType(MediaType.APPLICATION_JSON_VALUE);
            response.setStatus(HttpStatus.UNAUTHORIZED.value());
            response.getWriter().write("{\"code\":401,\"message\":\"Unauthenticated\"}");
        };
    }
}

Advantages

Native Spring integration : works seamlessly with Spring Boot, Spring Cloud and Spring Session.

Feature‑complete : authentication, authorization, CSRF, CORS, click‑jacking protection, OAuth2, SAML, LDAP, etc.

Highly extensible : virtually every component (filters, authentication manager, decision voters) can be overridden.

Strong community and documentation : official maintenance, extensive guides and examples.

Pain Points

Steep learning curve : many concepts (SecurityContext, Authentication, GrantedAuthority) and XML/Java configuration options.

Heavyweight for simple use‑cases : even a tiny API requires a full filter chain.

Debugging complexity : tracing failures through multiple filters can be difficult.

Performance overhead : the full chain adds measurable latency.

Best suited for large enterprise applications that need deep Spring integration, advanced protocols (OAuth2, SAML) and fine‑grained method‑level security.

Apache Shiro

Core Architecture – Four Core Concepts

Shiro centres its design around Subject (the current user), SecurityManager , Realm (data source) and Session . These concepts are illustrated in the diagram below.

Shiro core concepts
Shiro core concepts

Typical Shiro Configuration

@Configuration
public class ShiroConfig {
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
        ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean();
        bean.setSecurityManager(securityManager);
        bean.setLoginUrl("/login");
        bean.setUnauthorizedUrl("/unauthorized");
        Map<String, String> chain = new LinkedHashMap<>();
        chain.put("/static/**", "anon");
        chain.put("/css/**", "anon");
        chain.put("/js/**", "anon");
        chain.put("/api/public/**", "anon");
        chain.put("/login", "anon");
        chain.put("/api/user/**", "authc");
        chain.put("/api/admin/**", "authc, roles[admin]");
        chain.put("/api/products/create", "authc, perms[product:create]");
        chain.put("/api/products/delete/*", "authc, perms[product:delete]");
        chain.put("/**", "authc");
        bean.setFilterChainDefinitionMap(chain);
        return bean;
    }

    @Bean
    public SecurityManager securityManager() {
        DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
        manager.setRealm(customRealm());
        manager.setSessionManager(sessionManager());
        manager.setCacheManager(cacheManager());
        return manager;
    }

    @Bean
    public Realm customRealm() {
        CustomRealm realm = new CustomRealm();
        HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
        matcher.setHashAlgorithmName("SHA-256");
        matcher.setHashIterations(1024);
        matcher.setStoredCredentialsHexEncoded(false);
        realm.setCredentialsMatcher(matcher);
        realm.setCachingEnabled(true);
        realm.setAuthenticationCachingEnabled(true);
        realm.setAuthenticationCacheName("authenticationCache");
        realm.setAuthorizationCachingEnabled(true);
        realm.setAuthorizationCacheName("authorizationCache");
        return realm;
    }
}

Advantages

Simplicity : concise API, low learning cost.

Flexible configuration : supports INI, XML, annotations and Java config.

Feature‑complete : authentication, authorization, session management, encryption, caching.

Container‑agnostic : can run in any Java environment, not tied to Spring.

Pain Points

Less native Spring integration : requires extra wiring when used with Spring Boot.

Community activity declining : slower updates compared with Spring Security.

Limited extensibility for advanced features : some enterprise‑grade capabilities need custom implementation.

Ideal for medium‑sized projects, internal tools, or non‑Spring stacks where rapid development and straightforward permission models are required.

Sa‑Token

Core Design – Simple Yet Powerful

Sa‑Token follows a “zero‑config, all‑in‑one” philosophy. The framework provides a tiny set of static utility methods ( StpUtil.xxx()) that cover authentication, role checks, permission checks, token management, session handling and advanced features such as forced logout and second‑factor authentication.

Sa‑Token core components
Sa‑Token core components

5‑Minute Setup

1. Maven dependency (pom.xml)

implementation "cn.dev33:sa-token-spring-boot-starter:1.34.0"

2. Application configuration (application.yml)

sa-token:
  token-name: satoken
  timeout: 2592000   # 30 days (seconds)
  is-concurrent: true
  is-share: true
  max-login-count: 12
  token-style: uuid

3. Optional interceptor for URL‑level checks

@Configuration
public class SaTokenConfig {
    @Bean
    public SaInterceptor saInterceptor() {
        return new SaInterceptor()
            .addPathPatterns("/**")
            .excludePathPatterns("/api/user/login", "/api/public/**")
            .check(r -> {
                SaRouter.match("/api/**", () -> StpUtil.checkLogin());
                SaRouter.match("/api/admin/**", () -> StpUtil.checkRole("admin"));
                SaRouter.match("/api/products/create", () -> StpUtil.checkPermission("product.create"));
                SaRouter.match("/api/products/delete/**", () -> StpUtil.checkPermission("product.delete"));
            });
    }
}

4. Login controller example

@RestController
@RequestMapping("/api/user")
public class UserController {
    @PostMapping("/login")
    public ApiResult login(@RequestBody LoginDTO dto) {
        User user = userService.findByUsername(dto.getUsername());
        if (user == null || !passwordEncoder.matches(dto.getPassword(), user.getPassword())) {
            return ApiResult.error("Invalid username or password");
        }
        StpUtil.login(user.getId());
        String token = StpUtil.getTokenValue();
        long ttl = StpUtil.getTokenTimeout();
        LoginVO vo = new LoginVO();
        vo.setUserId(user.getId());
        vo.setUsername(user.getUsername());
        vo.setToken(token);
        vo.setExpireTime(ttl);
        return ApiResult.success("Login successful", vo);
    }
    // logout, getInfo, etc.
}

5. Using Sa‑Token in business controllers

@RestController
@RequestMapping("/api/products")
public class ProductController {
    @GetMapping("/list")
    public ApiResult list() {
        long userId = StpUtil.getLoginIdAsLong();
        List<Product> list = productService.getProductsByOwner(userId);
        return ApiResult.success(list);
    }

    @PostMapping("/create")
    public ApiResult create(@RequestBody ProductDTO dto) {
        StpUtil.checkPermission("product.create");
        long userId = StpUtil.getLoginIdAsLong();
        dto.setOwnerId(userId);
        Product p = productService.createProduct(dto);
        return ApiResult.success(p);
    }

    @DeleteMapping("/{id}")
    @SaCheckPermission("product.delete")
    public ApiResult delete(@PathVariable Long id) {
        Product p = productService.getById(id);
        if (p.getOwnerId() != StpUtil.getLoginIdAsLong()) {
            StpUtil.checkRole("admin");
        }
        productService.deleteProduct(id);
        return ApiResult.success("Deleted");
    }
}

6. Advanced features (force logout, account disable, second‑factor auth)

@Service
public class AdvancedSecurityService {
    public void forceLogout(Object loginId) {
        StpUtil.logout(loginId);
    }
    public void disableAccount(Object loginId, long seconds) {
        StpUtil.disable(loginId, seconds);
    }
    public boolean isDisabled(Object loginId) {
        return StpUtil.isDisable(loginId);
    }
    public boolean startSecondAuth(long ttl) {
        return StpUtil.openSafe(ttl);
    }
    public void checkSecondAuth() {
        StpUtil.checkSafe();
    }
}

Advantages

Extremely concise API : a single static class ( StpUtil) covers most operations.

Zero‑configuration out‑of‑the‑box : a working permission system can be built with only a few lines of code.

Rich feature set : login, logout, token refresh, role/permission checks, session sharing, concurrent login control, account disabling, forced logout, second‑factor authentication.

Chinese documentation : comprehensive guides written in Chinese, matching local developer habits.

Lightweight : minimal dependencies, fast startup.

Pain Points

Young ecosystem : fewer third‑party extensions and integrations compared with Spring Security.

Smaller community : harder to find solutions for very specific or advanced problems.

Limited enterprise‑grade protocols : OAuth2, SAML and LDAP support are not as mature.

Over‑abstraction : some low‑level customisations require digging into the internal API.

Best for startups, MVPs or internal tools where rapid development and low overhead are priorities.

Quick Comparative Overview

All three frameworks provide authentication, authorization and session handling, but they differ in learning curve, configurability and ecosystem maturity.

Learning curve : Spring Security (steep) > Apache Shiro (moderate) > Sa‑Token (gentle).

Configuration complexity : Spring Security (high) > Apache Shiro (moderate) > Sa‑Token (low).

Feature completeness : Spring Security (full) > Apache Shiro (near‑full) > Sa‑Token (core + many extras).

Spring ecosystem integration : Spring Security (native) > Apache Shiro (good) > Sa‑Token (good).

Performance overhead : Spring Security (higher) > Apache Shiro (moderate) > Sa‑Token (lowest).

Community activity : Spring Security (very active) > Apache Shiro (moderate) > Sa‑Token (growing).

Choosing the Right Framework

Consider the following typical scenarios:

Large enterprise applications with complex security requirements (OAuth2, SAML, fine‑grained method security) : choose Spring Security .

Mid‑size internal systems, non‑Spring stacks, or projects that need a simple yet powerful permission model : choose Apache Shiro .

Startups, MVPs, or micro‑services where speed of development and minimal configuration are critical : choose Sa‑Token .

If a project outgrows its initial choice, adopt a gradual migration strategy: keep the existing framework for stable modules, introduce a new one for new modules, and abstract security operations behind a common interface (e.g., SecurityService) so that the underlying implementation can be swapped without touching business code.

Conclusion

Spring Security is the heavyweight, enterprise‑grade option with the richest feature set but a steep learning curve. Apache Shiro offers a balanced, lightweight alternative that works well for many mid‑size applications. Sa‑Token provides the fastest path to a functional permission system, ideal for rapid prototyping and small‑to‑medium projects. The optimal choice depends on project size, team expertise and the specific security features required.

Javaframework comparisonsecurityAuthenticationAuthorizationSpring SecurityApache ShiroSa-Token
Su San Talks Tech
Written by

Su San Talks Tech

Su San, former staff at several leading tech companies, is a top creator on Juejin and a premium creator on CSDN, and runs the free coding practice site www.susan.net.cn.

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.