How Do You Secure REST APIs in Spring Boot?
Securing REST APIs in Spring Boot means protecting backend endpoints from unauthorized access, invalid users, data theft, token misuse, and common web security attacks.
REST APIs are commonly used by web applications, mobile apps, microservices, and third-party clients. Because APIs expose business data and operations, they must be secured properly using authentication, authorization, encryption, validation, and security best practices.
Why REST API Security is Important
REST APIs often handle sensitive information such as user profiles, payments, orders, transactions, admin actions, course purchases, and personal data.
Without proper security, attackers may:
- Access private user data
- Call admin APIs without permission
- Modify or delete important records
- Steal authentication tokens
- Perform brute-force attacks
- Exploit weak validation
Main Ways to Secure REST APIs in Spring Boot
- Use Spring Security
- Use JWT authentication for stateless APIs
- Apply role-based authorization
- Encrypt passwords using BCrypt
- Use HTTPS in production
- Configure CORS properly
- Validate all request data
- Handle exceptions securely
- Protect sensitive endpoints
- Use rate limiting for public APIs
1. Add Spring Security Dependency
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
After adding this dependency, Spring Boot enables security automatically. By default, all endpoints become protected.
2. Add JWT Dependency
JWT is commonly used to secure REST APIs because REST APIs are usually stateless.
<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>
3. Use BCrypt for Password Encryption
Never store passwords in plain text. Always hash passwords before saving them in the database.
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
Example:
String encodedPassword = passwordEncoder.encode("admin123");
4. Configure SecurityFilterChain
Modern Spring Security uses SecurityFilterChain instead of the older
WebSecurityConfigurerAdapter.
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthenticationFilter;
public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter) {
this.jwtAuthenticationFilter = jwtAuthenticationFilter;
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.cors(Customizer.withDefaults())
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.addFilterBefore(
jwtAuthenticationFilter,
UsernamePasswordAuthenticationFilter.class
);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
5. Create JWT Utility Class
@Component
public class JwtService {
private static final String SECRET_KEY =
"my-very-secure-secret-key-my-very-secure-secret-key";
public String generateToken(String username) {
return Jwts.builder()
.setSubject(username)
.setIssuedAt(new Date())
.setExpiration(
new Date(System.currentTimeMillis() + 1000 * 60 * 60)
)
.signWith(
Keys.hmacShaKeyFor(SECRET_KEY.getBytes()),
SignatureAlgorithm.HS256
)
.compact();
}
public String extractUsername(String token) {
return Jwts.parserBuilder()
.setSigningKey(Keys.hmacShaKeyFor(SECRET_KEY.getBytes()))
.build()
.parseClaimsJws(token)
.getBody()
.getSubject();
}
public boolean isTokenValid(String token, UserDetails userDetails) {
String username = extractUsername(token);
return username.equals(userDetails.getUsername())
&& !isTokenExpired(token);
}
private boolean isTokenExpired(String token) {
Date expiration = Jwts.parserBuilder()
.setSigningKey(Keys.hmacShaKeyFor(SECRET_KEY.getBytes()))
.build()
.parseClaimsJws(token)
.getBody()
.getExpiration();
return expiration.before(new Date());
}
}
6. Create JWT Authentication Filter
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtService jwtService;
private final UserDetailsService userDetailsService;
public JwtAuthenticationFilter(
JwtService jwtService,
UserDetailsService userDetailsService) {
this.jwtService = jwtService;
this.userDetailsService = userDetailsService;
}
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
String authHeader = request.getHeader("Authorization");
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
String token = authHeader.substring(7);
String username = jwtService.extractUsername(token);
if (username != null
&& SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails =
userDetailsService.loadUserByUsername(username);
if (jwtService.isTokenValid(token, userDetails)) {
UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(
userDetails,
null,
userDetails.getAuthorities()
);
authToken.setDetails(
new WebAuthenticationDetailsSource()
.buildDetails(request)
);
SecurityContextHolder
.getContext()
.setAuthentication(authToken);
}
}
filterChain.doFilter(request, response);
}
}
7. Create Login API
@RestController
@RequestMapping("/api/auth")
public class AuthController {
private final AuthenticationManager authenticationManager;
private final JwtService jwtService;
public AuthController(
AuthenticationManager authenticationManager,
JwtService jwtService) {
this.authenticationManager = authenticationManager;
this.jwtService = jwtService;
}
@PostMapping("/login")
public Map<String, String> login(
@RequestBody LoginRequest request) {
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
request.getUsername(),
request.getPassword()
)
);
String token = jwtService.generateToken(request.getUsername());
return Map.of("token", token);
}
}
8. Secure APIs Using Roles
@RestController
@RequestMapping("/api/admin")
public class AdminController {
@GetMapping("/dashboard")
public String dashboard() {
return "Admin Dashboard";
}
}
This endpoint is protected by:
.requestMatchers("/api/admin/**").hasRole("ADMIN")
9. Use Method-Level Security
@PreAuthorize("hasRole('ADMIN')")
@DeleteMapping("/users/{id}")
public String deleteUser(@PathVariable Long id) {
return "User deleted";
}
Method-level security is useful when access control depends on business logic, permissions, or roles.
10. Configure CORS Securely
@Configuration
public class CorsConfig {
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(List.of(
"https://www.dhanishempower.com"
));
config.setAllowedMethods(List.of(
"GET", "POST", "PUT", "DELETE", "OPTIONS"
));
config.setAllowedHeaders(List.of(
"Authorization", "Content-Type"
));
config.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source =
new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return source;
}
}
11. Validate Request Data
Always validate request payloads before processing.
public class RegisterRequest {
@NotBlank(message = "Name is required")
private String name;
@Email(message = "Invalid email address")
private String email;
@Size(min = 8, message = "Password must contain at least 8 characters")
private String password;
}
@PostMapping("/register")
public String register(@Valid @RequestBody RegisterRequest request) {
return "User registered";
}
12. Handle Security Exceptions Properly
Do not expose stack traces or internal server details in API responses.
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(AccessDeniedException.class)
@ResponseStatus(HttpStatus.FORBIDDEN)
public Map<String, Object> handleAccessDenied() {
return Map.of(
"status", 403,
"message", "Access denied"
);
}
}
Important REST API Security Best Practices
- Use HTTPS in production
- Never store plain-text passwords
- Use short-lived access tokens
- Use refresh tokens securely
- Do not store sensitive data inside JWT payload
- Restrict admin APIs using roles
- Allow only trusted CORS origins
- Validate every request body and parameter
- Log suspicious authentication failures
- Use rate limiting for login and public APIs
JWT Request Flow
- User logs in using username and password
- Spring Security validates credentials
- Server generates JWT token
- Client sends token in Authorization header
- JWT filter validates token
- SecurityContext is updated
- Protected API is accessed
Authorization Header Example
Authorization: Bearer eyJhbGciOiJIUzI1Ni...
Common HTTP Status Codes for Secured APIs
| Status Code | Meaning |
|---|---|
| 200 OK | Request successful |
| 201 CREATED | Resource created successfully |
| 400 BAD REQUEST | Invalid request data |
| 401 UNAUTHORIZED | User is not authenticated |
| 403 FORBIDDEN | User does not have permission |
| 500 INTERNAL SERVER ERROR | Server-side error |
Session-Based Security vs JWT Security
| Feature | Session-Based Security | JWT Security |
|---|---|---|
| State | Stateful | Stateless |
| Storage | Server stores session | Client stores token |
| Best For | Traditional web apps | REST APIs and microservices |
| Scalability | Moderate | High |
Common Mistakes While Securing REST APIs
- Using plain-text passwords
- Allowing all CORS origins in production
- Keeping JWT tokens valid for too long
- Storing secrets directly in source code
- Not validating request payloads
- Exposing stack traces in API responses
- Not protecting admin endpoints
Common Interview Questions
How do you secure REST APIs in Spring Boot?
REST APIs can be secured using Spring Security, JWT authentication, role-based authorization, password encryption, HTTPS, CORS configuration, request validation, and secure exception handling.
Why is JWT used for REST APIs?
JWT is stateless, scalable, and works well with REST APIs, mobile apps, and microservices.
Why do we disable CSRF for JWT APIs?
JWT APIs usually use Authorization headers instead of browser sessions, so CSRF protection is commonly disabled for stateless APIs.
What is the difference between 401 and 403?
401 means the user is not authenticated. 403 means the user is authenticated but does not have permission to access the resource.
How do you protect admin APIs?
Admin APIs can be protected using role-based rules such as
hasRole("ADMIN") or method-level annotations like
@PreAuthorize("hasRole('ADMIN')").
Conclusion
Securing REST APIs in Spring Boot requires multiple layers of protection. Spring Security provides authentication, authorization, filters, password encoding, JWT support, CORS integration, and method-level security.
A production-ready REST API should use HTTPS, JWT tokens, BCrypt password hashing, role-based access control, request validation, secure exception handling, and strict CORS rules.
Understanding REST API security is essential for Spring Boot developers because secure APIs are mandatory in enterprise applications, microservices, banking systems, payment platforms, and modern backend systems.