Spring security and JWT
Introduction
In the world of web development, securing your applications is a top priority. As cyber threats continue to grow in sophistication, the need for robust security mechanisms has never been more critical. For Java developers, leveraging Spring Security combined with JSON Web Tokens (JWT) is an effective way to ensure that your web applications remain secure.
In this blog, we’ll explore how to implement security in a Java web application using Spring Security and JWT. We will walk you through essential components like authentication, authorization, and token management while providing practical examples and guidance along the way.
What is Spring Security?
Spring Security is a robust and flexible framework for managing authentication and access control. It allows for extensive customization and is the de facto standard for securing Spring-based applications. Moreover, it provides comprehensive security services for Java EE-based enterprise software applications.
What is JWT?
JSON Web Token (JWT) is an open standard (RFC 7519) that defines a lightweight and self-contained way to securely transmit data between parties in the form of a JSON object. It’s widely used in stateless authentication mechanisms, where the server doesn’t need to keep session information between requests..
Setting Up the Spring Boot Project
- Spring Web: For building web applications.
- Spring Security: For implementing security features.
- Spring Boot DevTools: For development-time automation.
- Spring Data JPA: For data persistence.
- Java JWT: A Java library for creating and verifying JSON Web Tokens.
Implementing User AuthenticationStep
1: Create a User Entity Start by creating a User entity to store user credentials. We’ll use JPA to map this entity to a database table.
@Entity public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(nullable = false, unique = true) private String username; @Column(nullable = false) private String password; @Column private String roles; }
Step 2: Create a UserRepository
Create a repository interface to perform database operations on the User entity.
Step 3: Implement UserDetailsService
Spring Security uses the UserDetailsService interface to fetch user details during authentication. Implement this interface to load users from the database.
@Service public class CustomUserDetailsService implements UserDetailsService { @Autowired private UserRepository userRepository; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userRepository.findByUsername(username) .orElseThrow(() -> new UsernameNotFoundException("User not found: " + username)); return new org.springframework.security.core.userdetails.User( user.getUsername(), user.getPassword(), Arrays.stream(user.getRoles().split(",")) .map(SimpleGrantedAuthority::new) .collect(Collectors.toList()) ); } }
Step 4: Configure Spring Security
Next, create a configuration class to set up Spring Security with JWT.
@EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private CustomUserDetailsService userDetailsService; @Autowired private JwtAuthenticationEntryPoint unauthorizedHandler; @Bean public JwtAuthenticationFilter jwtAuthenticationFilter() { return new JwtAuthenticationFilter(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService) .passwordEncoder(passwordEncoder()); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Override protected void configure(HttpSecurity http) throws Exception { http.cors().and().csrf().disable() .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() .authorizeRequests() .antMatchers("/api/auth/**").permitAll() .anyRequest().authenticated(); http.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); } }
- We define an entry point for unauthorized access attempts.
- We disable CSRF (since we are using JWT).
- We permit all requests to the /api/auth/** endpoints (for authentication).
- All other requests require authentication.
Generating and Validating JWT
Step 1: Create a JWT Utility Class
Create a utility class for generating & validating JWT tokens.
@Component public class JwtTokenProvider { private final String JWT_SECRET = "secretKey"; private final long JWT_EXPIRATION = 604800000L; public String generateToken(Authentication authentication) { UserDetails userDetails = (UserDetails) authentication.getPrincipal(); return Jwts.builder() .setSubject(userDetails.getUsername()) .setIssuedAt(new Date()) .setExpiration(new Date(new Date().getTime() + JWT_EXPIRATION)) .signWith(SignatureAlgorithm.HS512, JWT_SECRET) .compact(); } public String getUsernameFromToken(String token) { return Jwts.parser() .setSigningKey(JWT_SECRET) .parseClaimsJws(token) .getBody() .getSubject(); } public boolean validateToken(String token) { try { Jwts.parser().setSigningKey(JWT_SECRET).parseClaimsJws(token); return true; } catch (Exception e) { e.printStackTrace(); } return false; } }
Step 2: Create an Authentication Filter
Create a filter to intercept requests and authenticate them using the JWT token.
public class JwtAuthenticationFilter extends OncePerRequestFilter { @Autowired private JwtTokenProvider tokenProvider; @Autowired private CustomUserDetailsService customUserDetailsService; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String token = getJwtFromRequest(request); if (StringUtils.hasText(token) && tokenProvider.validateToken(token)) { String username = tokenProvider.getUsernameFromToken(token); UserDetails userDetails = customUserDetailsService.loadUserByUsername(username); UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authentication); } filterChain.doFilter(request, response); } private String getJwtFromRequest(HttpServletRequest request) { String bearerToken = request.getHeader("Authorization"); if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) { return bearerToken.substring(7); } return null; } }
Implementing Authentication and Authorization Endpoints
Step 1: Create Authentication Controller
Create a controller to handle user authentication.
Step 2: Create DTOs for Authentication
Define the LoginRequest and JwtAuthenticationResponse DTOs:
public class LoginRequest { private String username; private String password; } public class JwtAuthenticationResponse { private String accessToken; private String tokenType = "Bearer"; public JwtAuthenticationResponse(String accessToken) { this.accessToken = accessToken; } }
Testing the Application
- Register a new user (this can be done via a dedicated registration endpoint or manually in the database).
- Authenticate by sending a POST request to /api/auth/login with the user’s credentials.
- Use the received JWT token in the Authorization header for subsequent requests.
Conclusion
By following this guide, you’ve learned how to secure a Java web application using Spring Security and JWT. This approach ensures that your application is protected from unauthorized access, and user sessions are managed securely without relying on server-side sessions.
Implementing JWT with Spring Security provides a scalable and secure solution for modern web applications. You can further enhance this setup by adding features like token refresh, role-based access control, and more.