Introduction
Spring Security is the most powerful security framework in the Java ecosystem. By default, just add dependencies, Spring Boot automatically protects all endpoints. This article explains how to configure authentication, authorization and understand the security filter chain.
1. Spring Security Architecture
1.1 Security Filter Chain
HTTP Request
│
▼
┌─────────────────────────────────┐
│ SecurityFilterChain │
│ │
│ ┌───────────────────────────┐ │
│ │ CorsFilter │ │
│ ├───────────────────────────┤ │
│ │ CsrfFilter │ │
│ ├───────────────────────────┤ │
│ │ UsernamePasswordAuth... │ │
│ ├───────────────────────────┤ │
│ │ BasicAuthenticationFilter │ │
│ ├───────────────────────────┤ │
│ │ BearerTokenAuth... (JWT) │ │
│ ├───────────────────────────┤ │
│ │ AuthorizationFilter │ │
│ ├───────────────────────────┤ │
│ │ ExceptionTranslation... │ │
│ └───────────────────────────┘ │
│ │
└─────────────────────────────────┘
│
▼
DispatcherServlet → Controller
1.2 Core Concepts
| Concepts | Description |
|---|---|
| Authentication | Authentication: "Who are you?" |
| Authorization | Decentralization: "What can you do?" |
| Principal | Current authenticated user |
| GrantedAuthority | User rights/roles |
| SecurityContext | Stores authentication info |
2. Basic configuration
2.1 Dependency
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
2.2 SecurityFilterChain Configuration
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http)
throws Exception {
http
.csrf(csrf -> csrf.disable()) // Disable cho REST API
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/api/public/**").permitAll()
.requestMatchers(HttpMethod.GET, "/api/products/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.requestMatchers("/api/users/**").hasAnyRole("USER", "ADMIN")
.anyRequest().authenticated()
)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.httpBasic(Customizer.withDefaults());
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
2.3 Authorization Rules
// Patterns
.requestMatchers("/public/**").permitAll() // Ai cũng truy cập được
.requestMatchers("/admin/**").hasRole("ADMIN") // Chỉ ADMIN
.requestMatchers("/api/**").hasAnyRole("USER", "ADMIN")
.requestMatchers("/api/**").hasAuthority("WRITE_ARTICLES")
.anyRequest().authenticated() // Phải đăng nhập
.anyRequest().denyAll() // Chặn tất cả
3. UserDetailsService — Custom User Store
3.1 Entity & Repository
@Entity
@Table(name = "users")
public class User extends BaseEntity {
@Column(nullable = false, unique = true)
private String username;
@Column(nullable = false, unique = true)
private String email;
@Column(nullable = false)
private String password; // BCrypt hash
@Column(nullable = false)
private boolean enabled = true;
@ElementCollection(fetch = FetchType.EAGER)
@CollectionTable(name = "user_roles", joinColumns = @JoinColumn(name = "user_id"))
@Column(name = "role")
private Set<String> roles = new HashSet<>();
}
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByUsername(String username);
Optional<User> findByEmail(String email);
boolean existsByUsername(String username);
boolean existsByEmail(String email);
}
3.2 Custom UserDetailsService
@Service
public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
public CustomUserDetailsService(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
.orElseThrow(() ->
new UsernameNotFoundException("User not found: " + username));
return org.springframework.security.core.userdetails.User.builder()
.username(user.getUsername())
.password(user.getPassword())
.roles(user.getRoles().toArray(String[]::new))
.disabled(!user.isEnabled())
.build();
}
}
3.3 User Registration
@Service
public class AuthService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
public AuthService(UserRepository userRepository,
PasswordEncoder passwordEncoder) {
this.userRepository = userRepository;
this.passwordEncoder = passwordEncoder;
}
public UserResponse register(RegisterRequest request) {
if (userRepository.existsByUsername(request.username())) {
throw new DuplicateResourceException("User", "username", request.username());
}
if (userRepository.existsByEmail(request.email())) {
throw new DuplicateResourceException("User", "email", request.email());
}
User user = new User();
user.setUsername(request.username());
user.setEmail(request.email());
user.setPassword(passwordEncoder.encode(request.password()));
user.setRoles(Set.of("USER"));
user.setEnabled(true);
User saved = userRepository.save(user);
return UserResponse.from(saved);
}
}
4. Authentication Manager
@Configuration
public class SecurityConfig {
@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration authConfig) throws Exception {
return authConfig.getAuthenticationManager();
}
@Bean
public AuthenticationProvider authenticationProvider(
CustomUserDetailsService userDetailsService,
PasswordEncoder passwordEncoder) {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(userDetailsService);
provider.setPasswordEncoder(passwordEncoder);
return provider;
}
}
5. Get current User information
@RestController
@RequestMapping("/api/users")
public class UserController {
// Cách 1: @AuthenticationPrincipal
@GetMapping("/me")
public UserResponse getCurrentUser(@AuthenticationPrincipal UserDetails userDetails) {
return userService.findByUsername(userDetails.getUsername());
}
// Cách 2: SecurityContextHolder
@GetMapping("/profile")
public UserResponse getProfile() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
String username = auth.getName();
return userService.findByUsername(username);
}
}
6. Password Encoding Best Practices
@Bean
public PasswordEncoder passwordEncoder() {
// BCrypt: salt tự động, strength mặc định = 10
return new BCryptPasswordEncoder();
// Hoặc DelegatingPasswordEncoder cho multiple encoders
// return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
// Verify password
boolean matches = passwordEncoder.matches(rawPassword, encodedPassword);
Important: Never save passwords in plain text. Always use BCrypt or Argon2.
Summary
- Spring Security uses SecurityFilterChain to handle authentication and authorization through a chain of filters
- UserDetailsService allows custom user store from database, combined with BCryptPasswordEncoder to secure passwords
- Authorization rules configured via HttpSecurity: permitAll(), hasRole(), hasAuthority(), authenticated()
Exercises
- Configure SecurityFilterChain for REST API: public endpoints (GET /api/products), protected endpoints (/api/users), admin endpoints (/api/admin)
- Implement CustomUserDetailsService loads user from database, registers new user with BCrypt encoded password
- Create endpoints
/api/auth/registerand/api/users/me, test authentication flow with HTTP Basic