Spring Security is a powerful and highly customizable authentication and access control framework for Java applications. It is the de facto standard for securing Spring-based applications. With the rise of RESTful APIs and the need for stateless authentication, JSON Web Tokens (JWT) have become popular for securing REST APIs. JWT offers a compact and self-contained way for securely transmitting information between parties as a JSON object. Combined with Spring Security, it provides a robust security solution for your Spring applications. In this blog post, we will explore how to integrate Spring Security with JWT to protect REST APIs.
We’ll cover the basics of JWT, how it works with Spring Security, and provide a step-by-step guide to implementing JWT-based authentication in a Spring Boot application. Whether you’re building a new API or looking to enhance the security of an existing one, this post will provide you with the knowledge and tools to effectively use Spring Security with JWT. Let’s dive into the world of secure REST APIs with Spring Security and JWT!
Dependencies Used In Spring Security Tutorial Project
MySQL
as DB.Spring
Data JPA as an ORM toolSpring
Security StarterLombok
to reduce boilerplate code.Validation
API to validate inputs for fields.JWT
for authentication tokens.Dev Tools
for live reload.Maven
as a build tool.
pom.xml after adding dependencies.
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-mail</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <scope>provided</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>jakarta.validation</groupId> <artifactId>jakarta.validation-api</artifactId> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.11.5</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-impl</artifactId> <version>0.11.5</version> <scope>runtime</scope> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-jackson</artifactId> <version>0.11.5</version> <scope>runtime</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <excludes> <exclude> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </exclude> </excludes> </configuration> </plugin> </plugins> </build> </project>
Entity Classes For Spring Security Implementation
Kicking off with the Entity class is an essential move in the process of integrating Spring Security, given that it lays the groundwork for establishing your domain model.
This class usually mirrors a database table, serving to outline the attributes and associations of the data you’ll be handling. In this article, we’ll focus on authenticating users through JWT
, so for the moment, we will set aside role-based authorization and will build a simple User Entity class
@Entity @Table(name = "users", uniqueConstraints = { @UniqueConstraint(columnNames = "username"), @UniqueConstraint(columnNames = "email") }) @Getter @Setter @NoArgsConstructor @AllArgsConstructor @Builder public class User { @Id @GeneratedValue(strategy = GenerationType.AUTO) private Long id; @NotBlank @Email private String username; @NotBlank private String password; private String firstName; private String lastName; public User(String username, String email, String password) { } }
User Principle In Spring Security
Within Spring Security, setting up a UserPrincipal typically happens once the user’s identity has been confirmed. This involves generating an Authentication object that represents the user’s credentials and then placing it within the SecurityContextHolder. Below is a detailed walkthrough for manually configuring the UserPrincipal.
@Getter @Setter @NoArgsConstructor @AllArgsConstructor @Builder public class UserDetailsImpl implements UserDetails { private static final long serialVersionUID = 1L; private Long id; private String username; @JsonIgnore private String password; private String firstName; private String lastName; private String about; private String profileImage; @Override public String getPassword() { return password; } @Override public String getUsername() { return username; } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; UserDetailsImpl user = (UserDetailsImpl) o; return Objects.equals(id, user.id); } @Override public Collection<? extends GrantedAuthority> getAuthorities() { return null; } }
DTO For Data Transfer
In this tutorial on Spring Security, we make use of various Data Transfer Objects (DTOs) including SignupRequest
, LoginRequest
, MessageResponse<T>
and JwtResponse
. These DTOs are essential for conveying request and response information when interacting with APIs.
@Getter @Setter @NoArgsConstructor @AllArgsConstructor @Builder public class SignupRequest { @Email private String username; private String password; private String firstName; private String lastName; private String about; private String profileImage; }
@Getter @Setter @NoArgsConstructor @AllArgsConstructor @Builder public class LoginRequest { private String username; private String password; }
@Getter @Setter @NoArgsConstructor @AllArgsConstructor @Builder public class MessageResponse<T> { private String message; private HttpStatus httpStatus; private T data; }
@Getter @Setter @NoArgsConstructor @AllArgsConstructor @Builder public class JwtResponse { private String token; private UserDetailsImpl detailsImpl; }
Repository For Spring Security
We will create a user repository to retrieve and persist or update the user data in the Database.
@Repository public interface UserRepository extends JpaRepository<User, Long> { Optional<User> findByUsername(String username); Boolean existsByUsername(String username); }
Configure Properties File
For Spring Security implementation we will Add below properties to application properties file to generated custom security key and trace spring security logs.
Note: Do not use # and $ in secrete key as they will throw invalid Base64 string.
spring.application.name=ABC spring.jpa.hibernate.ddl-auto=update spring.datasource.url=jdbc:mysql://localhost:3306/abc spring.datasource.username=abcd spring.datasource.password=dbPass spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver systems.app.jwtSecret= =====================================ABC=============== ================================= systems.app.jwtExpirationMs = 3600000 logging.level.org.springframework.security=TRACE
JWT Utility Class For Spring Security
In Spring Security we need to develop a JWT Utilities class that will set up, produce, and verify JWT tokens which are essential for user authentication and for granting access to protected resources.
@Component @Slf4j public class JwtUtils { @Value("${systems.app.jwtSecret}") private String jwtSecret; @Value("${systems.app.jwtExpirationMs}") private int jwtExpirationMs; public String generateJwtToken(String username) { return Jwts.builder() .setSubject(username) .setIssuedAt(new Date()) .setExpiration(new Date((new Date()) .getTime() + jwtExpirationMs)) .signWith(key(), SignatureAlgorithm.HS256) .compact(); } private Key key() { return Keys .hmacShaKeyFor(Decoders.BASE64.decode(jwtSecret)); } public String getUserNameFromJwtToken(String token) { return Jwts.parserBuilder() .setSigningKey(key()) .build() .parseClaimsJws(token) .getBody() .getSubject(); } public boolean validateJwtToken(String authToken) { try { Jwts.parserBuilder() .setSigningKey(key()) .build() .parse(authToken); return true; } catch (MalformedJwtException e) { log.error("Invalid JWT token: {}", e.getMessage()); } catch (ExpiredJwtException e) { log.error("JWT token is expired: {}", e.getMessage()); } catch (UnsupportedJwtException e) { log.error("JWT token is unsupported: {}", e.getMessage()); } catch (IllegalArgumentException e) { log.error("JWT claims string is empty: {}", e.getMessage()); } return false; } }
Implementation Of AuthenticationEntryPoint
AuthenticationEntryPoint is a fundamental aspect of Spring Security, especially when it comes to integrating JSON Web Tokens (JWT) for securing Spring-based applications. In this blog post, we will delve into the role of AuthenticationEntryPoint within the Spring Security framework, and how it works in tandem with JWT to provide a robust authentication mechanism. We’ll explore how to set up and configure an AuthenticationEntryPoint to handle authentication errors gracefully, ensuring that your application not only remains secure but also provides meaningful feedback to users when authentication fails.
@Component @Slf4j public class AuthEntryPointJwt implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { log.error("Unauthorized error: {}", authException.getMessage()); response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Error: Unauthorized"); } }
Implementing OncePerRequestFilter
OncePerRequestFilter is a class provided by Spring Security that ensures a filter is only executed once per request. This can be particularly useful in scenarios where multiple filter executions need to be avoided, such as when dealing with token-based authentication or request logging.
When implementing a custom filter in Spring Security, extending OncePerRequestFilter can help prevent unnecessary duplicate filter execution, ensuring that the filter logic is only applied once per request. This can be beneficial for performance optimization and avoiding unintended side effects that may arise from multiple executions of the same filter within a single request.
By extending OncePerRequestFilter and overriding the doFilterInternal method, developers can implement their custom filter logic while being assured that it will only be executed once for each incoming request. This class provides a convenient way to maintain the desired behavior of filters within the Spring Security framework.
@Component public class AuthTokenFilter extends OncePerRequestFilter { @Autowired private JwtUtils jwtUtils; @Autowired private UserDetailsServiceImpl userDetailsService; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { try { String jwt = parseJwt(request); if (jwt != null && jwtUtils.validateJwtToken(jwt)) { String email = jwtUtils.getUserNameFromJwtToken(jwt); System.out.println("Username : "+email); UserDetails userDetails = userDetailsService.loadUserByUsername(email); UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, null); authentication.setDetails(new WebAuthenticationDetailsSource() .buildDetails(request)); SecurityContextHolder.getContext() .setAuthentication(authentication); } } catch (Exception e) { logger.error("Cannot set user authentication: {}", e); } filterChain.doFilter(request, response); } private String parseJwt(HttpServletRequest request) { String headerAuth = request.getHeader("Authorization"); if (StringUtils.hasText(headerAuth) && headerAuth.startsWith("Bearer ")) { return headerAuth.substring(7, headerAuth.length()); } return null; } }
Role & Implementation Of UserDetailsService
UserDetailsService is an integral part of the Spring Security framework, which plays a crucial role in authentication processes. In the context of building secure applications, the integration of Spring Security with JSON Web Tokens (JWT) has become a popular approach to managing user authentication and authorization. JWT offers a compact and self-contained way for securely transmitting information between parties as a JSON object, and when combined with Spring Security, it provides a robust authentication mechanism.
@Service @Slf4j public class UserDetailsServiceImpl implements UserDetailsService { @Autowired private UserRepository userRepository; @Autowired private PasswordEncoder passwordEncoder; @Override @Transactional public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userRepository.findByUsername(username) .orElseThrow(() -> new UsernameNotFoundException("User Not Found with username: " + username)); UserDetailsImpl userDetails = UserDetailsImpl.builder() .password(user.getPassword()) .firstName(user.getFirstName()) .lastName(user.getLastName()) .about(user.getAbout()) .username(user.getUsername()) .id(user.getId()) .profileImage(user.getProfileImage()) .build(); return userDetails; } public MessageResponse<?> signup(SignupRequest signupRequest) { try { User user = User.builder() .about(signupRequest.getAbout()) .username(signupRequest.getUsername()) .firstName(signupRequest.getFirstName()) .lastName(signupRequest.getLastName()) .password(passwordEncoder.encode(signupRequest.getPassword())) .profileImage(signupRequest.getProfileImage()).build(); userRepository.save(user); log.info("Profile created for :" + signupRequest.getUsername()); MessageResponse<?> messageResponse = MessageResponse .builder() .message("Profile created for :" + signupRequest.getUsername()) .httpStatus(HttpStatus.CREATED) .build(); return messageResponse; } catch (Exception e) { log.error("error while adding new user", e); MessageResponse<?> messageResponse = MessageResponse .builder().message("adding user failed") .httpStatus(HttpStatus.INTERNAL_SERVER_ERROR) .build(); return messageResponse; } } }
Spring Security Without WebSecurityConfigurerAdapter
WebSecurityConfigurerAdapter
is now considered obsolete within Spring Security, we’ll proceed to set up our Spring Security configuration class without utilizing the WebSecurityConfigurerAdapter
. Traditionally, developers have relied on the WebSecurityConfigurerAdapter
class to configure security settings. However, it is possible to implement Spring Security without using it, offering a more flexible and customized approach to security configuration. By bypassing the WebSecurityConfigurerAdapter, developers can have more control over their security configuration.
This can be particularly useful in scenarios where the default behavior of WebSecurityConfigurerAdapter does not align with the specific requirements of the application. Implementing Spring Security without relying on WebSecurityConfigurerAdapter
allows for a more tailored and fine-grained approach to securing the application, providing the flexibility to define security settings programmatically as needed. In the following sections, we will explore the alternative methods and best practices for implementing Spring Security without the use of WebSecurityConfigurerAdapter
.
@Configuration @EnableWebSecurity public class WebSecurityConfig { // extends WebSecurityConfigurerAdapter { @Autowired UserDetailsService userDetailsService; @Autowired private AuthTokenFilter authenticationJwtTokenFilter; @Bean public static PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean public SecurityFilterChain filterChain(HttpSecurity http, AuthenticationManager authenticationManager) throws Exception { http.csrf(AbstractHttpConfigurer::disable).cors(Customizer.withDefaults()) .sessionManagement(session -> session .sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .authorizeHttpRequests(auth -> auth .requestMatchers("/api/v1/auth/**").permitAll() .requestMatchers("/api/test/**").permitAll() .anyRequest().authenticated()) .authenticationManager(authenticationManager) .addFilterBefore(authenticationJwtTokenFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); } @Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration = new CorsConfiguration(); configuration.setAllowedOrigins(Arrays.asList("*")); configuration.setAllowedMethods(Arrays.asList("*")); configuration.setAllowedHeaders(Arrays.asList("*")); configuration.setExposedHeaders(Arrays.asList("*")); // Limited to API routes (neither actuator nor Swagger-UI) // replace * with your expected URLS final var source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", configuration); return source; } @Bean public AuthenticationManager authenticationManager(HttpSecurity httpSecurity) throws Exception { AuthenticationManagerBuilder authenticationManagerBuilder = httpSecurity .getSharedObject(AuthenticationManagerBuilder.class); authenticationManagerBuilder .userDetailsService(userDetailsService) .passwordEncoder(passwordEncoder()); return authenticationManagerBuilder.build(); } }
Rest Entry Point To Check Spring Security
@CrossOrigin(origins = "*", maxAge = 3600) @RestController @RequestMapping("/api/v1/auth") public class AuthenticationController { @Autowired private AuthenticationManager authenticationManager; @Autowired private UserRepository userRepository; @Autowired private JwtUtils jwtUtils; @Autowired private UserDetailsServiceImpl userDetailsServiceImpl; @PostMapping("/signin") public ResponseEntity<?> authenticateUser(@Valid @RequestBody LoginRequest loginRequest) { Authentication authentication = authenticationManager.authenticate( new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword())); SecurityContextHolder.getContext().setAuthentication(authentication); UserDetailsImpl userDetails = (UserDetailsImpl) authentication.getPrincipal(); String jwt = jwtUtils.generateJwtToken(userDetails.getUsername()); JwtResponse jwtResponse = JwtResponse.builder() .token(jwt) .detailsImpl(userDetails) .build(); return ResponseEntity.ok(jwtResponse); } @PostMapping("/signup") public MessageResponse<?> registerUser(@Valid @RequestBody SignupRequest user) { if (userRepository.existsByUsername(user.getUsername())) { MessageResponse<?> messageResponse = MessageResponse.builder() .message("Error: Email is already in use!") .httpStatus(HttpStatus.ALREADY_REPORTED) .build(); return messageResponse; } return userDetailsServiceImpl.signup(user); } }
Run the Application and access APIs with Postman, SoapUi or any other tool of your choice.
APIs and response Structure
http://localhost:8080/api/v1/auth/signup
{ "username": "[email protected]", "password": "myPassword", "firstName": "Coding", "lastName": "Patient", "about": " Software Engineer working on Java and open source frameworks" }
Response
{ "message": "Profile created for :[email protected]", "httpStatus": "CREATED", "data": null }
http://localhost:8080/api/v1/auth/signin
Request
{ "username": "[email protected]", "password": "myPassword" }
Response
{ "token": "eyJhbGciOiJIUzI1NiJ9 .eyJzdWIiOiJ0ZXN0QHRlc3QuY29tIiwiaWF0IjoxNzEyMzA4MDk1LCJleHAiOjE3MTIzMTE2OTV9 .3El9JpCgcSFYZLXqz11FLJRbQucBMa_3KjCI38hYT0M", "detailsImpl": { "id": 2, "username": "[email protected]", "firstName": "Coding", "lastName": "Patient", "about": " Software Engineer working on Java and open source frameworks", "profileImage": null, "enabled": true, "authorities": null, "credentialsNonExpired": true, "accountNonExpired": true, "accountNonLocked": true } }
The token will be used for accessing secured APIs
Conclusion
We have learned how to implement Spring Security. The main purpose was to implement it without WebSecurityConfigurerAdapter
. If You find this useful let me or ping suggestions. you can reach me also at [email protected]. Thanks For reading.
See Also
How To Create CRUD For Views
Spring references from spring.io
Leave a Reply