This guide explores how to implement custom authentication and authorization mechanisms using Spring Security combined with JWT tokens. The approach enables stateless authentication suitable for modern distributed applications and microservices architectures.
--- 1. Understending Spring Security's Built-in Authentication Flow
Before implementing custom solutions, it's essential to understand how Spring Security's built-in authentication mechanism works. The framework provides a well-structured filter chain that processes authentication requests through a series of well-defined steps.
Core Authentication Pipeline
The built-in authentication flow follows a sequential process:
- The authentication filter receives credentials from form submissions and packages them into an authentication token object
- The filter delegates to an authentication manager for credential verification
- The authentication manager retrieves user details through a user details service
- A password encoder performs credential matching against stored values
- Upon successful validation, authorities and permissions are attached to the authentication object
- The authenticated token is stored in the security context for subsequent requests
--- 2. Building a Custom JSON-Based Authentication Filter
Spring Security's default form-based authentication filter expects POST data in URL-encoded format. Modern applications typically require JSON-based submissions for asynchronous client interactions. This section demonstrates how to create a custom filter that handles JSON credential submissions.
Implementing the Custom Filter
The following implementation extends the abstract authentication processing filter to accept JSON payloads:
package com.example.security.filter;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
public class JsonAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
private static final String USERNAME_FIELD = "username";
private static final String PASSWORD_FIELD = "password";
public JsonAuthenticationFilter(String authenticationUrl) {
super(authenticationUrl);
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response)
throws AuthenticationException, IOException, ServletException {
if (!"POST".equalsIgnoreCase(request.getMethod()) ||
!isJsonContentType(request)) {
throw new AuthenticationServiceException(
"Unsupported authentication method: " + request.getMethod());
}
ServletInputStream inputStream = request.getInputStream();
Map<String, String> credentials = new ObjectMapper()
.readValue(inputStream, HashMap.class);
String username = getCredential(credentials, USERNAME_FIELD);
String password = getCredential(credentials, PASSWORD_FIELD);
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(username, password);
return getAuthenticationManager().authenticate(authenticationToken);
}
private boolean isJsonContentType(HttpServletRequest request) {
String contentType = request.getContentType();
return contentType != null && (
contentType.equalsIgnoreCase(MediaType.APPLICATION_JSON_VALUE) ||
contentType.equalsIgnoreCase("application/json;charset=UTF-8")
);
}
private String getCredential(Map<String, String> credentials, String field) {
String value = credentials.get(field);
return (value != null) ? value.trim() : "";
}
@Override
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain,
Authentication authResult)
throws IOException, ServletException {
User principal = (User) authResult.getPrincipal();
String username = principal.getUsername();
Collection<GrantedAuthority> authorities = principal.getAuthorities();
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding("UTF-8");
Map<String, Object> responseData = new HashMap<>();
responseData.put("message", "Authentication successful");
responseData.put("username", username);
responseData.put("authorities", authorities);
response.getWriter().write(new ObjectMapper().writeValueAsString(responseData));
}
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException failed)
throws IOException, ServletException {
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding("UTF-8");
Map<String, Object> errorResponse = new HashMap<>();
errorResponse.put("message", "Authentication failed");
errorResponse.put("error", failed.getMessage());
response.getWriter().write(new ObjectMapper().writeValueAsString(errorResponse));
}
}
Creating the User Details Service
The authentication manager requires a user details service to load credential information from the data source:
package com.example.service;
import com.example.entity.UserAccount;
import com.example.mapper.UserAccountMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UserAccountMapper userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserAccount account = userRepository.findByUsername(username);
if (account == null) {
throw new UsernameNotFoundException("User not found: " + username);
}
List<GrantedAuthority> authorities =
AuthorityUtils.commaSeparatedStringToAuthorityList(account.getRoles());
return new User(
account.getUsername(),
account.getPassword(),
account.isEnabled(),
true, true, true,
authorities
);
}
}
Security Configuration
The configuration registers the custom filter and establishes authorization rules:
package com.example.security.config;
import com.example.security.filter.JsonAuthenticationFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/api/auth/login").permitAll()
.anyRequest().authenticated();
http.addFilterBefore(customAuthenticationFilter(),
UsernamePasswordAuthenticationFilter.class);
}
@Bean
public JsonAuthenticationFilter customAuthenticationFilter() throws Exception {
JsonAuthenticationFilter filter = new JsonAuthenticationFilter("/api/auth/login");
filter.setAuthenticationManager(authenticationManagerBean());
return filter;
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
--- 3. Implementing JWT-Based Stateless Authentication
Traditional session-based authentication maintains server-side state, which complicates horizontal scaling. JWT (JSON Web Tokens) provides an alternative approach where all necessary information is contained within the token itself, eliminating server-side session storage requirements.
Generating Tokens After Successful Authentication
Modify the successful authentication callback to generate and return a JWT token:
@Override
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain,
Authentication authResult)
throws IOException, ServletException {
UserDetails principal = (UserDetails) authResult.getPrincipal();
String username = principal.getUsername();
Collection<? extends GrantedAuthority> authorities = principal.getAuthorities();
String token = JwtTokenGenerator.createToken(username, authorities.toString());
Map<String, Object> responseData = new HashMap<>();
responseData.put("token", token);
responseData.put("username", username);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding("UTF-8");
response.getWriter().write(new ObjectMapper().writeValueAsString(responseData));
}
--- 4. Implementing Authorization Filter for JWT Validation
After issuing tokens during authentication, subsequent requests must include the token in request headers. An authorization filter validates tokens and establishes security context for incoming requests.
Creating the JWT Authorization Filter
package com.example.security.filter;
import com.example.security.util.JwtTokenValidator;
import io.jsonwebtoken.Claims;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
public class JwtAuthorizationFilter extends OncePerRequestFilter {
private static final String AUTHORIZATION_HEADER = "Authorization";
private static final String BEARER_PREFIX = "Bearer ";
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
String authHeader = request.getHeader(AUTHORIZATION_HEADER);
if (authHeader == null || !authHeader.startsWith(BEARER_PREFIX)) {
filterChain.doFilter(request, response);
return;
}
String token = authHeader.substring(BEARER_PREFIX.length());
Claims claims = JwtTokenValidator.parseToken(token);
if (claims == null) {
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.getWriter().write("{\"error\": \"Invalid or expired token\"}");
return;
}
String username = JwtTokenValidator.getUsername(claims);
String roles = JwtTokenValidator.getRoles(claims);
String cleanedRoles = roles.replaceAll("[\\[\\]]", "");
List<?> authorities = AuthorityUtils
.commaSeparatedStringToAuthorityList(cleanedRoles);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(username, null, authorities);
SecurityContextHolder.getContext().setAuthentication(authentication);
filterChain.doFilter(request, response);
}
}
Configuring the Authorization Filter
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/api/auth/login").permitAll()
.antMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll()
.anyRequest().authenticated();
http.addFilterBefore(jwtAuthorizationFilter(),
JsonAuthenticationFilter.class);
}
@Bean
public JwtAuthorizationFilter jwtAuthorizationFilter() {
return new JwtAuthorizationFilter();
}
--- 5. Customizing Access Denied Responses
When authenticated users attempt to access resources without proper permissions, or when unauthenticated users access protected resources, the framework provides default responses that may not suit API requirements. Custom handlers provide more appropriate error responses.
Handling Authorization Failures
package com.example.security.handler;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.MediaType;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request,
HttpServletResponse response,
AccessDeniedException accessDeniedException)
throws IOException, ServletException {
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding("UTF-8");
Map<String, Object> errorResponse = new HashMap<>();
errorResponse.put("status", 403);
errorResponse.put("message", "Access denied. Insufficient permissions.");
response.getWriter().write(new ObjectMapper().writeValueAsString(errorResponse));
}
}
Handling Unauthenticated Access
package com.example.security.handler;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.MediaType;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException)
throws IOException, ServletException {
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding("UTF-8");
Map<String, Object> errorResponse = new HashMap<>();
errorResponse.put("status", 401);
errorResponse.put("message", "Authentication required");
response.getWriter().write(new ObjectMapper().writeValueAsString(errorResponse));
}
}
Registering Custom Handlers
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/api/auth/login").permitAll()
.anyRequest().authenticated()
.and()
.exceptionHandling()
.accessDeniedHandler(new CustomAccessDeniedHandler())
.authenticationEntryPoint(new CustomAuthenticationEntryPoint());
http.addFilterBefore(jwtAuthorizationFilter(),
JsonAuthenticationFilter.class);
}
--- 6. Method-Level Security with Annotations
Spring Security supports method-level security through annotations, allowing fine-grained control over which users can execute specific methods within the application.
Enabling Method Security
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
// Configuration continues...
}
Applying Security Annotations
@RestController
@RequestMapping("/api/resources")
public class ResourceController {
@Autowired
private ResourceService resourceService;
@GetMapping
@PreAuthorize("hasAuthority('resource:read')")
public ResponseEntity<List<Resource>> getAllResources() {
return ResponseEntity.ok(resourceService.findAll());
}
@PostMapping
@PreAuthorize("hasAuthority('resource:create')")
public ResponseEntity<Resource> createResource(@RequestBody Resource resource) {
return ResponseEntity.ok(resourceService.create(resource));
}
@DeleteMapping("/{id}")
@PreAuthorize("hasAuthority('resource:delete')")
public ResponseEntity<Void> deleteResource(@PathVariable Long id) {
resourceService.delete(id);
return ResponseEntity.noContent().build();
}
}
--- 7. Integration Summary
The complete implementation provides a robust authentication and authorization system with the following characteristics:
- JSON-based authentication accepting credentials via POST body
- JWT token generation upon successful authentication
- Stateless authorization through token validation on each request
- Custom error responses for authentication and authorization failures
- Method-level security annotations for fine-grained access control
- Extensible architecture supporting custom user details and permission loading
This approach is particularly well-suited for microservices architectures where services must authenticate requests without relying on shared session state, and for single-page applications requiring asynchronous authentication mechanisms.