Chuyển đến nội dung chính

Lesson 9: Spring Security Fundamentals — Authentication & Authorization

SecurityFilterChain, HttpSecurity configuration. UserDetailsService, PasswordEncoder, login form, HTTP Basic. Role-based access control (RBAC) with @Secured, @PreAuthorize.

💻 Programming — Lesson 8 Lesson 9: Spring Security Fundamentals — Authentication & Authorization

Spring Boot 4: From Basics to Advanced

Part 3: Application security

xdev.asia

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

ConceptsDescription
AuthenticationAuthentication: "Who are you?"
AuthorizationDecentralization: "What can you do?"
PrincipalCurrent authenticated user
GrantedAuthorityUser rights/roles
SecurityContextStores 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

  1. Configure SecurityFilterChain for REST API: public endpoints (GET /api/products), protected endpoints (/api/users), admin endpoints (/api/admin)
  2. Implement CustomUserDetailsService loads user from database, registers new user with BCrypt encoded password
  3. Create endpoints /api/auth/register and /api/users/me, test authentication flow with HTTP Basic