Spring Security Basics
Spring Security is the industry standard for Java security—with its trio of filter chains, authentication, and authorization—it safeguards API security.
1. What You'll Learn
- Spring Security Filter Chain Architecture and
SecurityFilterChainConfiguration HttpSecurityConfiguration: Path Authorization, CSRF, CORS- In-Memory User Authentication
UserDetailsServiceandPasswordEncoder @PreAuthorize/@SecuredMethod-Level Authorization- Alice restricts access to the order management API to the ADMIN role only
2. A Product Manager's True Story
(1) Pain Point: Unprotected APIs
During a product review meeting, Charlie discovered that none of OrderFlow's APIs were authenticated at all—anyone could call the APIs to create orders or delete products. To make matters worse, Bob had exposed the Actuator endpoint to the public internet, allowing scanners to obtain production database information. Charlie demanded that security controls be implemented immediately, and Alice needed the fastest possible solution.
(2) Spring Security Solution
Spring Security can secure your API with just a few lines of configuration:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/v1/admin/**").hasRole("ADMIN")
.requestMatchers("/api/v1/orders/**").authenticated()
.anyRequest().permitAll()
)
.httpBasic(Customizer.withDefaults());
return http.build();
}
}
(3) Revenue
After Alice implemented authentication and authorization for OrderFlow using Spring Security, the management API was accessible only to users with the ADMIN role, the standard API required users to log in, and Actuator endpoints were restricted to internal network access. Charlie was satisfied with the security compliance.
3. Spring Security Architecture
(1) Filter Chain
sequenceDiagram
participant Client
participant Chain as SecurityFilterChain
participant Auth as Authentication Filter
participant Authz as Authorization Filter
participant Controller
Client->>Chain: HTTP Request
Chain->>Auth: 1. Authentication
alt Credentials Invalid
Auth-->>Client: 401 Unauthorized
end
Auth->>Authz: 2. Authorization
alt Access Denied
Authz-->>Client: 403 Forbidden
end
Authz->>Controller: 3. Forward to Controller
Controller-->>Client: 200 OK
| Key Concepts | Description |
|---|---|
SecurityFilterChain |
An ordered chain of filters, through which each request passes sequentially |
Authentication |
Verification: Confirm "Who You Are" |
Authorization |
Authorization: Confirm "What You Can Do" |
SecurityContext |
Security context containing the current user's information |
GrantedAuthority |
Permissions/Role Information |
(2) Relationships Among Core Components
| Component | Responsibilities | Interface |
|---|---|---|
AuthenticationManager |
Authentication Manager | authenticate() |
ProviderManager |
AuthenticationManager default implementation | Delegates to AuthenticationProvider |
UserDetailsService |
Loading user information | loadUserByUsername() |
PasswordEncoder |
Password Encoding | encode() / matches() |
4. SecurityFilterChain Configuration
(1) ▶ Example: Basic Security Configuration
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/v1/products/**").permitAll()
.requestMatchers("/api/v1/orders/**").authenticated()
.requestMatchers("/api/v1/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.httpBasic(Customizer.withDefaults());
return http.build();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(List.of("https://orderflow.example.com"));
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));
config.setAllowedHeaders(List.of("*"));
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/api/**", config);
return source;
}
}
Output:
// Execution Successful
| Configuration Option | Description | REST API Recommendation |
|---|---|---|
| CSRF | Cross-Site Request Forgery Protection | Disabled (not required for stateless APIs) |
| CORS | Cross-Origin Resource Sharing | Configure Allowed Domains |
| Session | Session Management | STATELESS (Stateless) |
| httpBasic | HTTP Basic Authentication | For Development/Testing |
| formLogin | Form Login | For traditional web applications |
5. In-Memory User Authentication
(1) ▶ Example: InMemoryUserDetailsManager
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public UserDetailsService userDetailsService(PasswordEncoder encoder) {
UserDetails admin = User.builder()
.username("alice")
.password(encoder.encode("admin123"))
.roles("ADMIN", "CUSTOMER")
.build();
UserDetails customer = User.builder()
.username("bob")
.password(encoder.encode("customer123"))
.roles("CUSTOMER")
.build();
return new InMemoryUserDetailsManager(admin, customer);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
Output:
// Execution Successful
| PasswordEncoder | Security | Use Cases |
|---|---|---|
BCryptPasswordEncoder |
High (includes salt value) | Recommended for production environments |
Argon2PasswordEncoder |
Highest (GPU Resistance) | High Security Requirements |
NoOpPasswordEncoder |
None (plaintext) | For testing purposes only |
NoOpPasswordEncoder in a production environment. BCrypt is the minimum standard; Argon2 is a higher standard.
(2) ▶ Example: Test Certification
# Access without credentials -> 401
curl http://localhost:8080/api/v1/orders
# Access with customer credentials -> 200
curl -u bob:customer123 http://localhost:8080/api/v1/orders
# Access admin endpoint with customer -> 403
curl -u bob:customer123 http://localhost:8080/api/v1/admin/dashboard
# Access admin endpoint with admin -> 200
curl -u alice:admin123 http://localhost:8080/api/v1/admin/dashboard
Output:
{"status":"ok","data":{}}
6. Method-Level Authorization
(1) ▶ Example: @PreAuthorize Method Authorization
@Service
public class OrderService {
@Transactional(readOnly = true)
@PreAuthorize("hasAnyRole('ADMIN', 'CUSTOMER')")
public Order getOrder(Long orderId) {
return orderRepository.findById(orderId).orElseThrow();
}
@Transactional
@PreAuthorize("hasRole('ADMIN')")
public void deleteOrder(Long orderId) {
orderRepository.deleteById(orderId);
}
@Transactional
@PreAuthorize("hasRole('ADMIN') or #customerId == authentication.principal.id")
public List<Order> getCustomerOrders(Long customerId) {
return orderRepository.findByCustomerId(customerId);
}
}
Output:
// Execution Successful
| Notes | Features | Use Cases |
|---|---|---|
@PreAuthorize |
SpEL expression, checks before accessing the method | Most flexible, recommended |
@PostAuthorize |
SpEL expression; check after the method is executed | Permissions must be determined based on the return value |
@Secured |
Character List, does not support SpEL | Simple Character Check |
@RolesAllowed |
JSR-250 Standard Annotations | Cross-Framework Compatibility |
@EnableMethodSecurity to the configuration class.
(2) ▶ Example: @EnableMethodSecurity configuration
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
// ... SecurityFilterChain and UserDetailsService beans
}
Output:
// Execution Successful
7. Quick Reference for Path Authorization Rules
| Matching Method | Description | Example |
|---|---|---|
requestMatchers(String) |
Ant-style path matching | /api/v1/orders/** |
requestMatchers(HttpMethod, String) |
Restricted HTTP Methods | POST /api/v1/orders |
anyRequest() |
Matches all requests | Placed last as a catch-all |
| Authorization Method | Description |
|---|---|
permitAll() |
Open to everyone |
authenticated() |
Authentication Required |
hasRole("ADMIN") |
Requires the ADMIN role |
hasAnyRole("A", "B") |
Any character needed |
denyAll() |
Block All Access |
(1) ▶ Example: Restricting API Access by Role
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers(HttpMethod.GET, "/api/v1/products/**").permitAll()
.requestMatchers(HttpMethod.POST, "/api/v1/products").hasRole("ADMIN")
.requestMatchers(HttpMethod.PUT, "/api/v1/products/**").hasRole("ADMIN")
.requestMatchers(HttpMethod.DELETE, "/api/v1/products/**").hasRole("ADMIN")
.requestMatchers("/api/v1/orders/**").hasAnyRole("ADMIN", "CUSTOMER")
.requestMatchers("/actuator/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.httpBasic(Customizer.withDefaults());
return http.build();
}
Output:
// Execution Successful
8. Comprehensive Example: OrderFlow Security Configuration
// SecurityConfig.java
package com.orderflow.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers(HttpMethod.GET, "/api/v1/products/**").permitAll()
.requestMatchers("/api/v1/orders/**").hasAnyRole("CUSTOMER", "ADMIN")
.requestMatchers("/api/v1/admin/**").hasRole("ADMIN")
.requestMatchers("/actuator/health").permitAll()
.requestMatchers("/actuator/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.httpBasic(Customizer.withDefaults());
return http.build();
}
@Bean
public UserDetailsService userDetailsService(PasswordEncoder encoder) {
var admin = User.builder()
.username("alice").password(encoder.encode("admin123"))
.roles("ADMIN", "CUSTOMER").build();
var customer = User.builder()
.username("bob").password(encoder.encode("pass123"))
.roles("CUSTOMER").build();
return new InMemoryUserDetailsManager(admin, customer);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
// OrderController with method-level security
@RestController
@RequestMapping("/api/v1/orders")
public class OrderController {
@GetMapping
@PreAuthorize("hasAnyRole('CUSTOMER', 'ADMIN')")
public List<Order> listOrders() { /* ... */ }
@GetMapping("/{id}")
@PreAuthorize("hasRole('ADMIN') or @orderSecurity.isOwner(#id, authentication)")
public Order getOrder(@PathVariable Long id) { /* ... */ }
@DeleteMapping("/{id}")
@PreAuthorize("hasRole('ADMIN')")
public void deleteOrder(@PathVariable Long id) { /* ... */ }
}
❓ FAQ
hasRole and hasAuthority?hasRole("ADMIN") automatically adds the "ROLE_" prefix to check for the ROLE_ADMIN permission. hasAuthority("ADMIN") does not add a prefix and directly checks the ADMIN permission. We recommend using hasRole consistently.SecurityContextHolder.getContext().getAuthentication(); 2) Injecting parameters into a Controller method Principal principal; 3) @AuthenticationPrincipal UserDetails user. We recommend the third method.📖 Summary
- Spring Security Core Architecture: SecurityFilterChain → Authentication → Authorization
SecurityFilterChainConfigure path authorization rules: permitAll / authenticated / hasRole- REST API Best Practices: Disable CSRF, STATELESS sessions, HTTP Basic or Bearer tokens
@PreAuthorize+ SpEL: Implementing Method-Level Fine-Grained Authorization Using ExpressionsBCryptPasswordEncoderis the minimum security standard for password encoding- InMemoryUserDetailsManager is for development use only; in a production environment, you must implement a custom UserDetailsService.
📝 Exercises
-
Basic Exercise (Difficulty: ⭐): Configure Spring Security for OrderFlow to implement
permitAllfor product queries,authenticatedfor order operations, andhasRole("ADMIN")for administrative APIs. Test using HTTP Basic authentication and in-memory users. -
Advanced Exercise (Difficulty ⭐⭐): Implement
@PreAuthorizemethod-level authorization—users can only view their own orders, while ADMIN can view all orders. Hint: Create an OrderSecurity helper bean and reference it in SpEL. -
Challenge (Difficulty: ⭐⭐⭐): Implement a custom
UserDetailsServiceto load user and role information from the database (JPA), replacing theInMemoryUserDetailsManager. Consider security design for password storage and the user registration process.



