Implementing JWT Authentication with Apache Shiro in Spring Boot
Core Components
Apache Shiro requires three main configurations for JWT integration:
- Custom Realm: Handles authentication and authorization logic by validaitng JWT tokens and retrieving user roles
- Security Manager: Manages security operations and connects the realm to Shiro's filter system
- Filter Factory: Configures URL patterns and applies JWT validation filters
JWT Filter Operations
preHandle: Processes CORS preflight requestsisAccessAllowed: Validates JWT token presenceexecuteLogin: Extracts token from Authorization headergetSubject: Passes authentication data to the realm
Realm Implemantation
supports: Specifies this realm only handles JWT tokensdoGetAuthenticationInfo:- Extracts username from JWT
- Validates token expiration
- Returns authentication info
doGetAuthorizationInfo: Assigns permissions based on user roles
Configuraton Class
@Configuration
public class SecurityConfig {
@Bean
public SecurityManager securityManager(AuthRealm realm) {
DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
manager.setRealm(realm);
DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
SessionStorageEvaluator evaluator = new DefaultSessionStorageEvaluator();
evaluator.setSessionStorageEnabled(false);
subjectDAO.setSessionStorageEvaluator(evaluator);
manager.setSubjectDAO(subjectDAO);
return manager;
}
@Bean
public ShiroFilterFactoryBean shiroFilter(SecurityManager manager) {
ShiroFilterFactoryBean factory = new ShiroFilterFactoryBean();
factory.setSecurityManager(manager);
Map<String, Filter> filters = new HashMap<>();
filters.put("jwt", new JwtValidationFilter());
factory.setFilters(filters);
Map<String, String> filterMap = new LinkedHashMap<>();
filterMap.put("/api/auth/**", "anon");
filterMap.put("/static/**", "anon");
filterMap.put("/**", "jwt");
factory.setFilterChainDefinitionMap(filterMap);
return factory;
}
}
Custom Realm Implementation
@Component
public class AuthRealm extends AuthorizingRealm {
@Autowired
private JwtHelper jwtHelper;
@Override
public boolean supports(AuthenticationToken token) {
return token instanceof JwtAuthToken;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) {
String jwt = ((JwtAuthToken) token).getCredentials();
String username = jwtHelper.extractUsername(jwt);
if (jwtHelper.isTokenExpired(jwt)) {
throw new AuthenticationException("Expired token");
}
return new SimpleAuthenticationInfo(username, jwt, getName());
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
String username = (String) principals.getPrimaryPrincipal();
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
// Add role/permission logic here
info.addRole("USER");
return info;
}
}
JWT Validation Filter
public class JwtValidationFilter extends BasicHttpAuthenticationFilter {
@Override
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
try {
return executeLogin(request, response);
} catch (Exception e) {
return false;
}
}
@Override
protected boolean executeLogin(ServletRequest request, ServletResponse response) {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String token = httpRequest.getHeader("Authorization");
if (StringUtils.isBlank(token)) {
return false;
}
getSubject(request, response).login(new JwtAuthToken(token));
return true;
}
}
JWT Utility Class
@Component
public class JwtHelper {
private final String secret = "secure-secret-key-123456";
private final long validity = 10080; // 7 days in minutes
public String generateToken(String username) {
return Jwts.builder()
.setSubject(username)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + validity * 60 * 1000))
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
public String extractUsername(String token) {
return Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody()
.getSubject();
}
public boolean isTokenExpired(String token) {
return Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody()
.getExpiration()
.before(new Date());
}
}