Skip to main content

3 posts tagged with "Spring Boot"

View All Tags

· 8 min read
Huseyin BABAL

Introduction

This article dives into securing a Spring Boot application using JSON Web Tokens (JWT) for authentication. We'll explore Spring Security, JWT fundamentals, and then implement a secure API with user registration, login, and access control. Our data will be persisted in a PostgreSQL database using Spring Data JPA.

Why Spring Security?

Spring Security is an industry-standard framework for securing Spring applications. It offers comprehensive features for authentication, authorization, and access control. By leveraging Spring Security, we can efficiently manage user access to our API endpoints.

JWT Authentication Explained

JWT is a token-based authentication mechanism. Unlike traditional session-based methods, JWT stores user information in a compact, self-contained token. This token is sent with every request, allowing the server to verify the user's identity without relying on server-side sessions.

Here's a breakdown of JWT's benefits:

  • Stateless: Removes the need for session management on the server.
  • Secure: Employs digital signatures to prevent tampering.
  • Flexible: Can be configured with various claims to store user information.

Persistence Layer

In this article, we will be using PostgreSQL as our database. You can maintain your database in any database management system. For a convenient deployment option, consider cloud-based solutions like Rapidapp, which offers managed PostgreSQL databases, simplifying setup and maintenance.

tip

Create a free database in Rapidapp in seconds here

Step-by-Step Implementation

Dependencies

Be sure you have the following dependencies installed by using your favourite dependency management tool e.g. maven, gradle.

pom.xml
 <dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</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-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.7.3</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.32</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.5</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

Enabling Spring Web Security

In order to enable Spring Web Security, you need to configure it in your SecurityConfig.java file as shown below.

SecurityConfig.java
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

private static final String[] AUTH_WHITELIST = {
"/api/v1/auth/login",
"/api/v1/auth/register"
};

private final JwtAuthFilter jwtAuthFilter;

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.authorizeRequests(authorizeRequests ->
authorizeRequests
.requestMatchers(AUTH_WHITELIST).permitAll()
.anyRequest().authenticated()
)
.sessionManagement(sessionManagement ->
sessionManagement
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}

Line 2: Add @EnableWebSecurity to the SecurityConfig class to protect the API endpoints.

Line 6: Allow requests from the /api/v1/auth/login and /api/v1/auth/register endpoints without authentication.

Line 16: Disable CSRF protection, since JWT authentication is stateless.

Line 24: Set the session creation policy to STATELESS to ensure sessions are not maintained.

Line 25: Add the JwtAuthFilter to the security filter chain before the UsernamePasswordAuthenticationFilter. We will explain JwtAuthFilter class soon.

JWT Auth Filter

In order to enable JWT authentication, you need to configure it in your JwtAuthFilter.java file as shown below.

JwtAuthFilter.java
@Component
@RequiredArgsConstructor
public class JwtAuthFilter extends OncePerRequestFilter {

private final JwtService jwtService;
private final UserDetailsService userDetailsService;

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
if (request.getServletPath().contains("/api/v1/auth")) {
filterChain.doFilter(request, response);
return;
}

final String authorizationHeader = request.getHeader("Authorization");
final String jwtToken;
final String email;

if (authorizationHeader == null || !authorizationHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}

jwtToken = authorizationHeader.substring(7);
email = jwtService.extractEmail(jwtToken);

if (email != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(email);
if (jwtService.validateToken(jwtToken, userDetails)) {
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
}
filterChain.doFilter(request, response);
}
}

Line 10: Do not apply JWT auth filter for /api/v1/auth endpoints.

Line 24: Extract JWT token from the Authorization header. Its format is Bearer <token>, that's why it is substring(7).

Line 25: Extract email from the JWT token using JwtService which we will take a look at in the next section.

Line 28-32: Validate the JWT token using JwtService, load user details using UserDetails from UserDetailsService and store the authentication in SecurityContextHolder.

Implementing JWTService

This class contains all JWT related functionalities as shown below.

JwtService.java
@Service
public class JwtService {

@Value("${jwt.secret}")
private String secret;

public String extractEmail(String jwtToken) {
return extractClaim(jwtToken, Claims::getSubject);
}

public <T> T extractClaim(String jwtToken, Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(jwtToken);
return claimsResolver.apply(claims);
}

private Claims extractAllClaims(String jwtToken) {
return Jwts.parser().verifyWith(getSigningKey()).build().parseSignedClaims(jwtToken).getPayload();
}

private SecretKey getSigningKey() {
byte [] bytes = Decoders.BASE64.decode(secret);
return Keys.hmacShaKeyFor(bytes);
}

public boolean validateToken(String jwtToken, UserDetails userDetails) {
final String email = extractEmail(jwtToken);
return email.equals(userDetails.getUsername()) && !isTokenExpired(jwtToken);
}

private boolean isTokenExpired(String jwtToken) {
return extractExpiration(jwtToken).before(new Date());
}

private Date extractExpiration(String jwtToken) {
return extractClaim(jwtToken, Claims::getExpiration);
}

public String generateToken(User u) {
return createToken(u.getEmail());
}

private String createToken(String email) {
return Jwts.builder()
.subject(email)
.issuedAt(new Date(System.currentTimeMillis()))
.expiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 10))
.signWith(getSigningKey())
.compact();
}
}

Line 5: This is the secret key used to sign JWT tokens. This should be carefully protected, it is not something that we can share or expose publicly. All the other functions are self-explanatory.

UserDetailsService

UserDetailsService is design for showing spring boot security authentication how to load user details from database as shown below.

UserDetailsService.java
@Service
@RequiredArgsConstructor
public class UserDetailService implements UserDetailsService {
private final UserRepository userRepository;


@Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
return userRepository.findByEmail(email)
.map(user -> User.builder().username(user.getEmail())
.password(user.getPassword())
.build())
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
}
}

Until this point, we have only focused on JWT authentication. However, how we will generate JWT tokens in the next section? What is its use-case?

Registering User

Before generating JWT token to authenticate the user, we need to register the user. We will use AuthController to register user.

AuthController.java
@RestController
@RequestMapping(path = "api/v1/auth")
@RequiredArgsConstructor
public class AuthController {

private final AuthService authService;


@PostMapping(path = "/register")
@ResponseStatus(HttpStatus.NO_CONTENT)
public void register(@RequestBody RegisterRequest registerRequest) {
authService.register(registerRequest);
}

@PostMapping(path = "/login")
public ResponseEntity<String> login(@RequestBody LoginRequest loginRequest) {
return ResponseEntity.ok(authService.login(loginRequest));
}
}

In above controller, we are using AuthService to register and login user. AuthService uses UserRepository to interact database for user related operations.

AuthService.java
@Service
@RequiredArgsConstructor
public class AuthService {

private final UserRepository userRepository;
private final AuthenticationManager authenticationManager;
private final JwtService jwtService;
private final BCryptPasswordEncoder bCryptPasswordEncoder;

public void register(RegisterRequest registerRequest) {
User u = User.builder()
.email(registerRequest.getEmail())
.password(bCryptPasswordEncoder.encode(registerRequest.getPassword()))
.firstName(registerRequest.getFirstName())
.lastName(registerRequest.getLastName())
.build();
userRepository.save(u);
}

public String login(LoginRequest loginRequest) {
authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(loginRequest.getEmail(), loginRequest.getPassword()));
User u = userRepository.findByEmail(loginRequest.getEmail()).orElseThrow(() -> new EntityNotFoundException("User not found"));
return jwtService.generateToken(u);

}
}

Line 10: Register user by using the details provided in the request payload. The bCryptPasswordEncoder is used to hash the password before storing it in the database.

Line 21: The login operation is done through authenticationManager since it knows how to validate username and password.

Restricted Access to UserController

You can see a sample endpoint implementation for user object.

UserController.java
@RestController
@RequestMapping(path = "api/v1")
public class UserController {
private final UserRepository userRepository;
public UserController(UserRepository userRepository) {
this.userRepository = userRepository;
}

@GetMapping("/users")
public List<User> getUsers() {
return userRepository.findAll();
}
}

Assume you registered a new user with email admin password ssshhhh. Then in order to generate a JWT token, you can use the following curl request.

curl -X POST -H "Content-Type: application/json" \
-d '{"email": "admin", "password": "ssshhhh"}' http://localhost:8080/api/v1/auth/login

It will return a JWT token, which you can use to authenticate the user. Store it somewhere.

Now in order to access restricted user endpoint, you can use the following curl request.

curl -X GET -H "Authorization: Bearer <token>" http://localhost:8080/api/v1/users

Conclusion

This hands-on tutorial equipped you with the knowledge to implement JWT Authentication in your Spring Boot application. We explored user registration, login, and access control, leveraging Spring Security and JPA for data persistence. By following these steps and customizing the code examples to your specific needs, you can secure your API endpoints and ensure authorized user access. Remember to prioritize security best practices. Here are some additional points to consider:

  • Secret Key Management: Store your JWT secret key securely in environment variables or a dedicated secret management service. Never expose it in your codebase.
  • Token Expiration: Set a reasonable expiration time for JWT tokens to prevent unauthorized access due to compromised tokens.
  • Error Handling: Implement proper error handling mechanisms for invalid or expired tokens to provide informative feedback to users.
  • Advanced Features: Explore advanced JWT features like refresh tokens for longer-lived sessions and role-based access control (RBAC) for granular authorization. With JWT authentication in place, your Spring Boot application is well on its way to becoming a secure and robust platform. Deploy it with confidence, knowing that user access is properly controlled.
tip

You can find the complete source code for this project on GitHub.

· 6 min read
Huseyin BABAL

Introduction

Caching is a fundamental technique to improve the performance of applications by storing frequently accessed data in memory, thereby reducing the need for repeated database queries. In Spring Boot, caching is often implemented at two levels: the first level cache and the second level cache.

First Level and Second Level Cache

First Level Cache: This is the cache associated with the Hibernate session. It is enabled by default and works only within the scope of a session, meaning that the data cached is only available within a single transaction.

Second Level Cache: This is a more sophisticated cache that works at the session factory level. It is shared among all sessions, making it possible to cache data across transactions. This type of cache can significantly reduce database load and improve application performance.

In this blog post, we will focus on integrating the second level cache in a Spring Boot application using Redis.

Spring Starter Projects and Database Integration

For our Spring Boot application, we will use the following starter projects:

  • Spring Data JPA: For ORM and database interactions.
  • PostgreSQL: As our relational database.
  • Redis: For caching purposes.

We will also leverage two SaaS solutions:

  • Rapidapp for PostgreSQL: To quickly set up and manage our PostgreSQL database.
tip

Create a free database in Rapidapp Starter in seconds here

  • Upstash Redis: A managed Redis service optimized for low-latency data caching.
tip

Create a free Redis database in Upstash here

Spring Boot Application Setup

Application

This section contains application level configurations such as the application name, datasource, and jpa as shown below:

application.yaml
spring:
application:
name: sb-l2-redis
datasource:
driver-class-name: org.postgresql.Driver
url: <connection-string-from-rapidapp|or your own managed postgres url>
jpa:
database-platform: org.hibernate.dialect.PostgreSQLDialect
hibernate:
ddl-auto: update
show-sql: true
properties:
hibernate:
cache:
use_second_level_cache: true

Line 5: JDBC driver class for PostgreSQL.

Line 6: Connection URL for the PostgreSQL database. You can obtain this from Rapidapp or your own managed PostgreSQL service. It should have a format like jdbc:postgresql://<host>:<port>/<database>?user=<user>&password=<password>&sslmode=require.

warning

We use connection string here for demo purposes, but it is not secure to use connection string in application.yaml. You should use environment variables (SPRING_DATASOURCE_URL) or secrets management tools to store sensitive information.

Line 10: This will create tables in PostgreSQL database automatically by using entities.

Line 15: Enable second level cache in Hibernate. We will visit this configuration later.

Entities

Let's create a simple Product entity that will be stored in the PostgreSQL database.

Product.java
@Builder
@Data
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Cache(region = "products", usage = CacheConcurrencyStrategy.READ_WRITE)
class Product {
@Id
@GeneratedValue
private Long id;
private String name;
private String description;
private BigDecimal price;
}
  1. @Entity annotation is used to mark the class as a JPA entity.
  2. @Cache annotation is used to enable caching for the entity.
  • region attribute is used to specify the cache name.
  • usage attribute is used to specify the cache concurrency strategy. The remaining attributes are coming from Lombok library to automatically generate getters, setters, and constructors.

Repository

Next, we will create a ProductRepository interface that extends JpaRepository to interact with the Product entity.

ProductRepository.java
interface ProductRepository extends JpaRepository<Product, Long> {}

Controller

Finally, let's set up a simple Spring Boot application endpoint. We will create a ProductController with endpoints to create, get, and list products.

ProductController.java
@RestController
@RequestMapping("/products")
class ProductController {

private final ProductRepository productRepository;

public ProductController(ProductRepository productRepository) {
this.productRepository = productRepository;
}

@GetMapping
public Iterable<Product> getProducts() {
return productRepository.findAll();
}

@GetMapping("/{id}")
public Product getProduct(@PathVariable Long id) {
return productRepository.findById(id).orElseThrow();
}

@PostMapping
public Product createProduct(@RequestBody CreateProductRequest request) {
Product product = Product.builder()
.name(request.getName())
.description(request.getDescription())
.price(request.getPrice())
.build();
return productRepository.save(product);
}
}

Generate Data

Right after running application, we can now generate some sample data with the following curl command.

curl \
--parallel \
-XPOST \
-H "Content-Type: application/json" \
http://localhost:8080/products\?\[1-200\] \
-d '{"name": "book", "description": "desc", "price": "100.3"}'

Line 2: --parallel flag is used to send multiple requests in parallel. Line 5: [1-200] is used to sent same payload 200 times.

Since we have spring.jpa.show_sql=true in our application.yaml, we can see 200 insert SQL statements in the console. In same way, we can see select SQL statements when we call GET /products/{id} endpoint. This is where second level cache comes into play.

Implementing Second Level Cache with Redis

Redis Client Configuration

First, we need to configure the Redis client in our Spring Boot application. We will use redisson as the Redis client library.

redisson.yaml
singleServerConfig:
idleConnectionTimeout: 10000
connectTimeout: 10000
timeout: 3000
retryAttempts: 3
retryInterval: 1500
password: <redis_password>
subscriptionsPerConnection: 5
clientName: sb-l2-redis
address: <redis_address>
subscriptionConnectionMinimumIdleSize: 1
subscriptionConnectionPoolSize: 50
connectionMinimumIdleSize: 10
connectionPoolSize: 64
database: 0
dnsMonitoringInterval: 5000

Line 7: Redis password. You can obtain this from Upstash or your own managed Redis service.

Line 10: Redis server address. You can obtain this from Upstash or your own managed Redis service.

You can see the other configuration internals here. Now we have redis client configuration, and with the help of spring.jpa.hibernate.properties.cache.use_second_level_cache: true in application.yaml file, we can enable second level cache in Hibernate. The cache provider class is automatically injected in to the application with following dependency.

pom.xml
<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson-hibernate-53</artifactId>
<version>3.30.0</version>
</dependency>

Cache Verification

After running the application, we can see that the select SQL statements are not executed when we call the GET /products/{id} endpoint second time. This is because the data is fetched from the Redis cache instead of the database.

Conclusion

Implementing a second level cache in your Spring Boot application using Redis can significantly improve performance by reducing database load. With the combination of Spring Data JPA, PostgreSQL, and Redis, you can achieve scalable and efficient caching solution. Using managed services like Rapidapp and Upstash for Redis further simplifies the setup and management of your infrastructure.

By following the steps outlined in this blog post, you can easily integrate a second level cache in your Spring Boot application and enjoy the benefits of faster data access and reduced database queries.

tip

You can find the complete source code for this project on GitHub.

· 5 min read
Huseyin BABAL

In the rapidly evolving landscape of software development, Spring Boot has emerged as a beacon for Java developers seeking to streamline their application development process. One of Spring Boot's most powerful features is its ability to utilize "starters" – pre-configured sets of code and dependencies that can be easily included in projects to provide specific functionality. These starters not only save time but also enforce best practices and reduce the likelihood of errors.

Understanding Spring Boot Starters

Spring Boot starters are essentially a set of convenient dependency descriptors that you can include in your application. Each starter provides a quick way to add and configure a specific technology or feature to your Spring Boot application, without the hassle of managing individual dependencies and their compatible versions. This approach significantly simplifies the build configuration, enabling developers to focus more on their application's unique functionality rather than boilerplate code and configuration.

The starters cover a wide range of needs, from web applications with spring-boot-starter-web to data access with spring-boot-starter-data-jpa, and much more. By abstracting complex configurations, starters offer a seamless, convention-over-configuration approach, adhering to the Spring Boot philosophy.

Introducing RapidApp Postgres Starter

tip

Create a free database in Rapidapp Starter in seconds here

Building on the concept of starters, we are excited to introduce the spring-boot-starter-rapidapp for users of RapidApp, a SaaS platform that includes PostgreSQL as a service. This starter is designed to make it incredibly easy for Spring Boot applications to integrate with a RapidApp PostgreSQL database, eliminating the need for developers to manage the database themselves.

Here's how it works:

  1. Easy Configuration: Developers need to add a few lines to their application.properties or application.yml file, specifying that they want to enable RapidApp Postgres, their API key, and the database ID. This is all it takes to configure the connection to the RapidApp PostgreSQL database:
<!-- pom.xml -->
<dependency>
<groupId>io.rapidapp</groupId>
<artifactId>spring-boot-starter-rapidapp</artifactId>
<version>0.0.2</version> <!-- Replace with the latest version https://mvnrepository.com/artifact/io.rapidapp/spring-boot-starter-rapidapp -->
</dependency>
# application.yaml
rapidapp:
postgres:
enabled: true
apiKey: <your_api_key> # Obtain from https://app.rapidapp.io/api_keys
  1. Automatic Resource Management: When a Spring Boot application using the spring-boot-starter-rapidapp is run, the starter automatically creates a PostgreSQL database, prepares a datasource and establishes a connection to the created PostgreSQL database. This is not the only use-case that you can achieve with Rapidapp starter project, let's take a look a couple of use cases.

Rapidapp starter use-cases

Temporary database

Assume you need a temporary database for a short-term task, where a PostgreSQL database is set up before your Spring Boot application starts and is destroyed before the application shuts down. While you could use an in-memory database like H2, imagine you have multiple replicas of your Spring Boot app that need to connect to a central database, such as PostgreSQL, in Rapidapp. You can easily accomplish this by configuring your Spring Boot project with the following steps:

# application.yaml
rapidapp:
postgres:
enabled: true
apiKey: <your_api_key> # Obtain from https://app.rapidapp.io/api_key
dropBeforeApplicationExit: true

Connecting to a pre-configured database

There are several ways to create a PostgreSQL database in Rapidapp. You can create it directly through the Rapidapp UI or automate the process using the Rapidapp Terraform Provider within your Infrastructure as Code (IaC) pipeline. After creating the PostgreSQL database, you can easily obtain the database ID by navigating to Details > Connection Properties and copying the database ID. Then, you can add it to your Spring application properties file as shown below.

# application.yaml
rapidapp:
postgres:
enabled: true
apiKey: <your_api_key> # Obtain from https://app.rapidapp.io/api_key
databaseId: <db_id>

You can optionally provide a database name for display in the Rapidapp UI (this name does not affect the actual database name). If you don't specify one, Rapidapp will automatically generate a name for your database.

Advantages of Using Spring Boot Starters like RapidApp Starter

The use of starters, including the RapidApp starter, brings several advantages to the table:

  • Simplicity: By abstracting the complexity of dependency management and configuration, starters make it much simpler to add and configure new features in a Spring Boot application.
  • Speed: Starters can significantly reduce the time required to bootstrap new applications or add new features, enabling faster development cycles.
  • Best Practices: Starters are designed with best practices in mind, ensuring that applications are configured optimally right from the start.
  • Focus on Business Logic: With starters handling much of the boilerplate code and configuration, developers can focus more on the unique business logic of their applications.

In conclusion, the spring-boot-starter-rapidapp exemplifies how Spring Boot starters can be leveraged to simplify and optimize application development. By providing an easy and efficient way to integrate Spring Boot applications with RapidApp's managed PostgreSQL service, it opens up new possibilities for developers to build scalable, serverless applications with minimal overhead. As the ecosystem of Spring Boot starters continues to grow, the opportunities for developers to innovate and streamline their development processes will only expand.

tip

You can see the demo project here