The purpose of this post is to explain how to secure a Spring Boot API using the Apache Shiro security framework.
What is Apache Shiro?
Apache Shiro is a powerful and flexible open-source security framework that cleanly handles authentication, authorization, enterprise session management and cryptography.
Apache Shiro’s first and foremost goal is to be easy to use and understand. Security can be very complex at times, even painful, but it doesn’t have to be. A framework should mask complexities where possible and expose a clean and intuitive API that simplifies the developer’s effort to make their application(s) secure. — https://shiro.apache.org/introduction.html
Why use Apache Shiro?
It is open source and free to use. Apache Shiro is a comprehensive application security framework with many features.

Shiro targets what the Shiro development team calls “the four cornerstones of application security” — Authentication, Authorization, Session Management, and Cryptography:
- Authentication: Sometimes referred to as ‘login’, this is the act of proving a user is who they say they are.
- Authorization: The process of access control, i.e. determining ‘who’ has access to ‘what’.
- Session Management: Managing user-specific sessions, even in non-web or EJB applications.
- Cryptography: Keeping data secure using cryptographic algorithms while still being easy to use.
Getting Started
Prerequisites
- Spring Boot 2.6.2
- Maven 3.6.1
- JAVA 11
- PostgreSQL 12
Setup Spring Boot Project
We will start by creating a simple Spring Boot project from start.spring.io, with the following dependencies: Web, PostgreSQL Driver, Spring Data JPA, and Lombok.
To integrate Apache Shiro in our project, we need to include the Shiro Spring web starter dependency in pom.xml file.
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-starter</artifactId>
<version>1.8.0</version>
</dependency>
Custom Realm
A Realm is a component that can access application-specific security data such as users, roles and permissions. The Realm translates this application-specific data into a format that Shiro understands so Shiro can in turn provide a single easy-to-understand Subject programming API no matter how many data sources exist or how application-specific your data might be.
public class UserRealm extends AuthorizingRealm {
@Autowired
UserService userservice;
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof UsernamePasswordToken;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String username = (String)token.getPrincipal();
Optional<User> user = userservice.getUserByUsername(username);
if(user.isEmpty()) {
throw new UnknownAccountException("Unknown Account");
}
if(!user.get().isEnabled()) {
throw new LockedAccountException("Blocked account");
}
return new SimpleAuthenticationInfo(user.get().getUsername(), user.get().getPassword(),getName());
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return new SimpleAuthorizationInfo();
}
}
Shiro Configuration
One of the main parts of Apache Shiro is the configuration of beans for security management. We will define all the beans in the ShiroConfig configuration class.
@Configuration
public class ShiroConfig {
@Bean
DefaultPasswordService pwdService(){
return new DefaultPasswordService();
}
@Bean("userRealm")
public UserRealm userRealm() {
UserRealm userRealm = new UserRealm();
PasswordMatcher pwdMatcher = new PasswordMatcher();
pwdMatcher.setPasswordService(pwdService());
userRealm.setCredentialsMatcher(pwdMatcher);
userRealm.setCachingEnabled(false);
return userRealm;
}
@Bean("shiroFilterFactoryBean")
public ShiroFilterFactoryBean shiroFilterFactoryBean(@Qualifier("securityManager")
DefaultWebSecurityManager securityManager) {
ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
Map<String, Filter> filterMap = new HashMap<>();
// Add custom filter
filterMap.put("api", new AuthenticationFilter(null));
factoryBean.setFilters(filterMap);
// Config security manager
factoryBean.setSecurityManager(securityManager);
factoryBean.setUnauthorizedUrl("/401");
/*
- anon: Access without authentication
- authc: Must be authenticated to access
- user: You have to remember me to use it
- perms: You can access a resource only if you have permission to it
- role: Have a role permission
*/
Map<String, String> filterRuleMap = new HashMap<>();
filterRuleMap.put("/api/user/**", "api");
filterRuleMap.put("/api/auth/**","anon");
filterRuleMap.put("/401", "anon");
factoryBean.setFilterChainDefinitionMap(filterRuleMap);
return factoryBean;
}
@Bean
public SessionStorageEvaluator sessionStorageEvaluator() {
DefaultSessionStorageEvaluator sessionStorageEvaluator = new DefaultWebSessionStorageEvaluator();
sessionStorageEvaluator.setSessionStorageEnabled(false);
return sessionStorageEvaluator;
}
@Bean
public DefaultSubjectDAO subjectDAO() {
DefaultSubjectDAO defaultSubjectDAO = new DefaultSubjectDAO();
defaultSubjectDAO.setSessionStorageEvaluator(sessionStorageEvaluator());
return defaultSubjectDAO;
}
@Bean(name="securityManager")
public DefaultWebSecurityManager getDefaultWebSecurityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(userRealm());
securityManager.setSubjectDAO(subjectDAO());
return securityManager;
}
@Bean
public Authenticator authenticator() {
ModularRealmAuthenticator authenticator = new ModularRealmAuthenticator();
authenticator.setRealms(List.of(userRealm()));
authenticator.setAuthenticationStrategy(new FirstSuccessfulStrategy());
return authenticator;
}
}
Custom interceptor
The next step is to customize Shiro interceptors to control access to specified requests and connect to Shiro for authentication.
@Slf4j
public class AuthenticationFilter extends AuthenticatingFilter {
private final TokenManagerService tokenManagerService;
public AuthenticationFilter(TokenManagerService tokenManagerService) {
this.tokenManagerService = tokenManagerService;
}
/**
* Check JWT token
*
* @param servletRequest request
* @param servletResponse response
* @return AuthenticationToken
*/
@Override
protected AuthenticationToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) {
HttpServletRequest request = WebUtils.toHttp(servletRequest);
if (request == null) {
throw new IllegalArgumentException("Request cannot be empty");
}
String jwt = request.getHeader(HttpHeaders.AUTHORIZATION);
if(StringUtils.isBlank(jwt) || !jwt.startsWith(SecurityConstants.TOKEN_PREFIXE)) {
throw new AuthenticationException("JWT Token is not valid");
}
String token = jwt.replace(SecurityConstants.TOKEN_PREFIXE, "");
if (Boolean.TRUE.equals(tokenManagerService.isTokenExpired(token))) {
throw new AuthenticationException("JWT Token is Expired :" + token);
}
return new JWTAuthToken(token);
}
/**
* Access Failure Handling
*/
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws IOException {
HttpServletRequest httpServletRequest = WebUtils.toHttp(request);
HttpServletResponse httpServletResponse = WebUtils.toHttp(response);
// Return to 401
httpServletResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
httpServletResponse.setCharacterEncoding("UTF-8");
httpServletResponse.setContentType("application/json");
String error =new ObjectMapper().writeValueAsString(new ErrorResponse(String.valueOf(HttpStatus.UNAUTHORIZED.value()),HttpStatus.UNAUTHORIZED.getReasonPhrase(), "Access Denied"));
httpServletResponse.getWriter().append(error);
log.error("access Denied url: {}", httpServletRequest.getRequestURI());
return false;
}
/**
* Determine whether access is allowed
*/
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
String url = WebUtils.toHttp(request).getRequestURI();
log.debug("Access Allowed url:{}", url);
if (this.isLoginRequest(request, response)) {
return true;
}
boolean allowed = false;
try {
allowed = executeLogin(request, response);
} catch (IllegalStateException e) { //not found any token
log.error("Token Can not be empty", e);
} catch (Exception e) {
log.error("Access error", e);
}
return allowed || super.isPermissive(mappedValue);
}
}
isAccessAlloweddetermines whether access is allowedonAccessDeniedwhen isaccessallowed() returns false, the login is rejected.createTokenCheck user JWT token

Test the REST APIs:
Run the Spring Boot Application.
- sign-up endpoint

- sign-in endpoint
When the login is successful, the user gets the jwt token

When the user password is incorrect

All source code is available on GitHub.
Thanks for reading! 🙂