How to Secure Backend APIs with Spring Security
This tutorial explains why backend API permission checks are essential in RBAC‑based systems, walks through configuring Spring Security for authentication and authorization, demonstrates URL‑level and annotation‑based access control, and discusses token‑driven role verification and caching strategies.
1. Introduction
Most modern systems manage permissions with an RBAC model (users, roles, permissions). In the author’s company the RBAC checks were only enforced on the front‑end, leaving backend endpoints unprotected. Consequently, a normal user could perform admin‑only actions via tools like Postman, which is a serious security flaw. Adding backend permission verification with Spring Security resolves this risk.
2. Spring Security Overview
Spring Security is a powerful, highly extensible authentication and authorization framework for Spring‑based Java applications. Compared with Apache Shiro, Spring Security offers richer features and a larger community, making it the default choice for medium‑to‑large projects, while Shiro is often used for smaller prototypes because of its simplicity.
2.1 Authentication and Authorization
Authentication: verifies that the request comes from a legitimate user and identifies the user. Authorization: after authentication, determines whether the user has permission to perform a specific operation.
2.2 Getting Started with Authentication
Add the following Maven dependencies to a Spring Boot web project:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>Create a simple test controller:
@RestController
@RequestMapping("/fds/test")
public class TestController {
@GetMapping("/auth")
public String testAuth() {
return "auth pass, success back";
}
}When the application starts, accessing http://127.0.0.1:18888/fds/test/auth redirects to the default Spring Security login page.
Spring Boot automatically configures an InMemoryUserDetailsManager. If spring.security.user is not defined, a user named user with a randomly generated UUID password is created (the password is printed in the console). The generated password is for development only.
[] [WARN] [2023-01-12 15:55:21.143] ... Using generated security password: 369bc4e0-b2e1-446c-8df4-2ac5455ee496After logging in with the generated credentials, the browser returns to the original endpoint.
2.3 Authorization Configuration Example
Define a SecurityConfig class to configure URL‑level permissions:
@Configuration
public class SecurityConfig {
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity.authorizeRequests()
.antMatchers("/fds/test/auth").permitAll() // accessible by anyone
.antMatchers("/fds/test/auth/admin").hasRole("ADMIN") // requires ADMIN role
.antMatchers("/fds/test/auth/normal").access("hasRole('ROLE_NORMAL')") // requires NORMAL role
.anyRequest().authenticated() // all other requests need authentication
.and().formLogin(); // enable form‑based login
return httpSecurity.build();
}
}Common HttpSecurity methods used for access control include: #permitAll() – allow all users. #denyAll() – deny all users. #authenticated() – require authentication. #anonymous() – allow anonymous access. #hasIpAddress(String) – restrict by IP. #hasRole(String) – require a specific role. #hasAuthority(String) – require a specific authority. #access(String) – evaluate a Spring EL expression.
Configure a user in application.yml (or application.properties) to avoid the generated password:
spring:
security:
user:
name: shepherd
password: shepherd
roles: ADMIN,NORMALAdd three test endpoints:
@RestController
@RequestMapping("/fds/test")
public class TestController {
@GetMapping("/auth")
public String testAuth() { return "auth pass, success back"; }
@GetMapping("/auth/admin")
public String testAuthAdmin() { return "admin"; }
@GetMapping("/auth/normal")
public String testAuthNormal() { return "normal"; }
}After restarting, /fds/test/auth is publicly accessible, while /fds/test/auth/admin and /fds/test/auth/normal redirect to the login page and return data only after successful authentication. Removing the ADMIN role from the user results in a 403 Forbidden response for the admin endpoint.
2.4 Annotation‑Based Permission Control
Enable method‑level security with @EnableGlobalMethodSecurity and use @PreAuthorize on controller methods. The annotation is equivalent to #access(String) with a Spring EL expression.
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig {
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity.authorizeRequests().anyRequest().authenticated().and().formLogin();
return httpSecurity.build();
}
} @RestController
@RequestMapping("/fds/test")
public class TestController {
@GetMapping("/auth")
public String testAuth() { return "auth pass, success back"; }
@PreAuthorize("hasRole('ROLE_ADMIN')")
@GetMapping("/auth/admin")
public String testAuthAdmin() { return "admin"; }
@PreAuthorize("hasRole('ROLE_NORMAL')")
@GetMapping("/auth/normal")
public String testAuthNormal() { return "normal"; }
}The behavior is identical to the URL‑level configuration.
3. Real‑World Project Implementation
The core logic for backend permission verification is:
Extract the token from the request.
Identify the logged‑in user.
Query the user’s roles.
Query the permissions (menus, buttons) associated with those roles.
Determine whether the user is allowed to invoke the target endpoint.
Using the annotation approach, the author defines a custom bean ss with a method #hasAuth(String) that evaluates the RBAC permission:
@PreAuthorize("@ss.hasAuth('user:list')")
@GetMapping("/user/list")
public void getUserList() { }Here ss is a Spring bean injected into the container; #hasAuth() checks the RBAC table for the resource identifier user:list. If the expression returns true, access is granted.
When request volume is low, querying the database for each permission check is acceptable. For high‑traffic systems, this creates a performance bottleneck. A multi‑level cache (Redis + local cache) can alleviate the load: store permissions in Redis, synchronize updates to the local cache via Redis Pub/Sub, and read from the local cache for fast checks. The author refers to a separate article on multi‑level caching for details.
Implementation details are omitted for confidentiality, but the overall flow remains: token → user → roles → permissions → access decision.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Shepherd Advanced Notes
Dedicated to sharing advanced Java technical insights, daily work snippets, and the power of persistent effort.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
