In modern web applications, JSON Web Tokens (JWT) are widely used for securing APIs due to their stateless and scalable nature. In this post, we'll explore the core idea behind implementing JWT-based authentication in a Spring Security setup, including configuring public URLs for Actuator and Swagger, and handling unauthorized access with a custom authentication entry point.
Core Idea: How JWT Authentication Works
JWT authentication in Spring Security involves a few key steps:
- Extract and Validate the JWT:
- Parse the incoming request's JWT token (usually from the
Authorizationheader). - Verify the token's signature, expiration, and claims.
- Parse the incoming request's JWT token (usually from the
- Retrieve User Details:
- Extract the username and roles from the token's claims.
- Set Security Context:
- Create an
Authenticationobject with the user's details and granted authorities. - Set the
Authenticationobject in theSecurityContextHolder.
- Create an
- Authorize Requests:
- Spring Security uses the information in the
SecurityContextHolderto authorize requests based on roles and permissions.
- Spring Security uses the information in the
Key Components
JWT Filter
A custom filter is needed to intercept requests, validate JWTs, and populate the security context.
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtService jwtService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String token = extractToken(request);
if (token != null && jwtService.validateToken(token)) {
String username = jwtService.extractUsername(token);
List<GrantedAuthority> authorities = jwtService.extractRoles(token).stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
Authentication authentication = new UsernamePasswordAuthenticationToken(username, null, authorities);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
}
private String extractToken(HttpServletRequest request) {
String header = request.getHeader("Authorization");
if (header != null && header.startsWith("Bearer ")) {
return header.substring(7);
}
return null;
}
}
Security Configuration
The filter must be added before the UsernamePasswordAuthenticationFilter. Additionally, we’ll configure public URLs for Actuator and Swagger, and a custom entry point for handling unauthorized access.
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http, JwtAuthenticationFilter jwtFilter) throws Exception {
http.csrf().disable()
.authorizeHttpRequests()
.antMatchers("/swagger-ui/**", "/v3/api-docs/**", "/actuator/**").permitAll()
.anyRequest().authenticated()
.and()
.exceptionHandling()
.authenticationEntryPoint(customAuthenticationEntryPoint())
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public AuthenticationEntryPoint customAuthenticationEntryPoint() {
return (request, response, authException) -> {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json");
response.getWriter().write("{\\"error\\":\\"Unauthorized\\"}");
};
}
}
JWT Service
A utility class for handling JWT-related operations, such as validation and claims extraction.
@Service
public class JwtService {
private final String secretKey = "your-secret-key";
public boolean validateToken(String token) {
try {
Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token);
return true;
} catch (JwtException e) {
return false;
}
}
public String extractUsername(String token) {
return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject();
}
public List<String> extractRoles(String token) {
Claims claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody();
return claims.get("roles", List.class);
}
}
Testing the Setup
- JWT Validation:
- Send a request with a valid JWT in the
Authorizationheader:Authorization: Bearer <your-token>. - The application should authenticate the user and allow access based on roles.
- Send a request with a valid JWT in the
- Public Endpoints:
- Access
http://localhost:8080/actuator/healthorhttp://localhost:8080/swagger-ui.htmlwithout a JWT to verify public access.
- Access
- Unauthorized Access:
- Send a request to a secured endpoint without a JWT.
- You should receive a 401 response with the custom error message:
{"error":"Unauthorized"}.
Conclusion
JWT-based authentication in Spring Security provides a clean and scalable way to secure APIs. By setting up a custom filter, configuring public endpoints, and handling unauthorized access gracefully, you can create a robust security mechanism tailored to your application's needs. Let me know your thoughts or if you'd like more details on extending this setup!
No comments:
Post a Comment