Skip to main content
Java intermediate Lesson 56 of 58

Spring Security — Authentication and Authorization

Secure Spring Boot applications with Spring Security — HTTP security config, user details, password encoding, role-based authorization, and method-level security.

Spring Security is the standard security framework for Spring Boot. It integrates deeply with the framework — once configured, security rules apply automatically to every request without any changes to your controllers. Adding the starter dependency immediately locks down all endpoints, so you configure explicitly what should be public and what requires authentication.

Setup

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

Adding this dependency immediately activates three defaults:

  • Requires authentication for all endpoints
  • Generates a random password printed to the console on startup
  • Enables CSRF protection (fine for web apps, disable for stateless REST APIs)

Security Configuration

The SecurityFilterChain bean is where you define the security rules for your application. It replaces the old WebSecurityConfigurerAdapter approach. Each http method call configures one aspect of the security chain — CSRF handling, session policy, authorization rules, and the authentication provider.

@Configuration
@EnableWebSecurity
@EnableMethodSecurity   // enables @PreAuthorize and @PostAuthorize on methods
public class SecurityConfig {

    private final UserDetailsService userDetailsService;
    private final PasswordEncoder    passwordEncoder;

    public SecurityConfig(UserDetailsService userDetailsService,
                          PasswordEncoder passwordEncoder) {
        this.userDetailsService = userDetailsService;
        this.passwordEncoder    = passwordEncoder;
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable())  // stateless REST APIs don't need CSRF protection
            .sessionManagement(s -> s.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/auth/**").permitAll()          // login/register are public
                .requestMatchers("/api/admin/**").hasRole("ADMIN")   // admin-only section
                .requestMatchers(HttpMethod.GET, "/api/products/**").permitAll() // public reads
                .anyRequest().authenticated()                         // everything else requires login
            )
            .authenticationProvider(authenticationProvider());

        return http.build();
    }

    @Bean
    public AuthenticationProvider authenticationProvider() {
        // DaoAuthenticationProvider loads users from your UserDetailsService
        // and verifies passwords using the PasswordEncoder
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        provider.setUserDetailsService(userDetailsService);
        provider.setPasswordEncoder(passwordEncoder);
        return provider;
    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration config)
            throws Exception {
        return config.getAuthenticationManager();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder(12); // cost factor 12 — strong enough for production
    }
}

UserDetailsService — Loading Users

Spring Security calls loadUserByUsername during every authentication attempt. It’s the bridge between your user database and Spring Security’s user model. Return a UserDetails object that contains the username, hashed password, and granted authorities (roles).

@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {

    private final UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
        // Spring Security calls this with whatever the client sent as the username field
        return userRepository.findByEmail(email)
            .map(user -> org.springframework.security.core.userdetails.User.builder()
                .username(user.getEmail())
                .password(user.getPasswordHash())  // already hashed — Spring verifies it
                .roles(user.getRole().name())       // becomes ROLE_USER, ROLE_ADMIN, etc.
                .build())
            .orElseThrow(() -> new UsernameNotFoundException("User not found: " + email));
    }
}

Password Encoding

Storing plain-text passwords is a critical security vulnerability. If your database is ever compromised, all user passwords are exposed. BCrypt is a deliberately slow hashing algorithm — the cost factor controls how long each hash takes, making brute-force attacks impractical even with the hash in hand.

@Service
@RequiredArgsConstructor
public class AuthService {

    private final UserRepository  userRepository;
    private final PasswordEncoder passwordEncoder;

    public void register(RegisterRequest req) {
        if (userRepository.existsByEmail(req.email())) {
            throw new DuplicateResourceException("Email already registered");
        }
        User user = new User(
            req.name(),
            req.email(),
            passwordEncoder.encode(req.password()),  // hash the plain-text password before storing
            UserRole.USER
        );
        userRepository.save(user);
    }

    public boolean checkPassword(String rawPassword, String storedHash) {
        // matches() hashes the rawPassword and compares — never compare hashes directly
        return passwordEncoder.matches(rawPassword, storedHash);
    }
}

Role-Based Authorization

URL-Level Rules

URL-level rules are declared in the SecurityFilterChain and apply before any controller code runs. They’re good for coarse-grained access control — entire sections of the API that are restricted to a role.

.authorizeHttpRequests(auth -> auth
    .requestMatchers("/api/auth/**").permitAll()                          // public
    .requestMatchers("/api/admin/**").hasRole("ADMIN")                   // admin only
    .requestMatchers(HttpMethod.DELETE, "/api/**").hasAnyRole("ADMIN", "MODERATOR")
    .requestMatchers(HttpMethod.GET, "/api/**").hasAnyRole("USER", "ADMIN")
    .anyRequest().authenticated()
)

Method-Level Security

Method-level security with @PreAuthorize gives you fine-grained control. You can reference the authenticated user directly in the expression, which makes it easy to enforce ownership rules — for example, allowing users to see their own profile but not others’.

@RestController
@RequestMapping("/api/users")
public class UserController {

    @GetMapping
    @PreAuthorize("hasRole('ADMIN')")  // only admins can list all users
    public List<UserResponse> getAllUsers() { ... }

    @GetMapping("/{id}")
    // admins can see anyone; users can only see themselves
    @PreAuthorize("hasRole('ADMIN') or #id == authentication.principal.id")
    public UserResponse getUserById(@PathVariable Long id) { ... }

    @DeleteMapping("/{id}")
    @PreAuthorize("hasRole('ADMIN')")
    public void deleteUser(@PathVariable Long id) { ... }
}

Getting the Current User

Spring Security stores the authenticated user in SecurityContextHolder for the duration of the request. You can access it directly from a controller parameter or call the static method from anywhere in the request thread.

// Inject Authentication directly — Spring binds it automatically
@GetMapping("/me")
public UserResponse getCurrentUser(Authentication authentication) {
    String email = authentication.getName(); // returns the username from UserDetails
    return userService.findByEmail(email);
}

// Or access from a service via the SecurityContextHolder
public static String getCurrentUserEmail() {
    return SecurityContextHolder.getContext()
        .getAuthentication()
        .getName();
}

HTTPS and Security Headers

Security headers are a low-effort, high-value hardening measure. HSTS prevents protocol downgrade attacks. frameOptions(deny) prevents clickjacking. contentTypeOptions prevents MIME-type sniffing attacks. These should be enabled in any production deployment.

http
    .requiresChannel(channel -> channel.anyRequest().requiresSecure()) // redirect HTTP → HTTPS
    .headers(headers -> headers
        .frameOptions(frame -> frame.deny())                // prevents iframe embedding (clickjacking)
        .contentTypeOptions(Customizer.withDefaults())      // prevents MIME sniffing
        .httpStrictTransportSecurity(hsts -> hsts
            .maxAgeInSeconds(31536000)   // browsers remember HTTPS-only for 1 year
            .includeSubDomains(true))
    );

Test Endpoints With Security

Spring Security’s test support lets you simulate authenticated requests without running a real login flow. @WithMockUser injects a fake authenticated user into the security context for the duration of the test.

@SpringBootTest
@AutoConfigureMockMvc
class UserControllerTest {

    @Autowired MockMvc mockMvc;

    @Test
    @WithMockUser(roles = "USER")
    void getUser_authenticated_returns200() throws Exception {
        mockMvc.perform(get("/api/users/1"))
               .andExpect(status().isOk());
    }

    @Test
    void getUser_unauthenticated_returns401() throws Exception {
        // No @WithMockUser — request has no authentication context
        mockMvc.perform(get("/api/users/1"))
               .andExpect(status().isUnauthorized());
    }

    @Test
    @WithMockUser(roles = "ADMIN")
    void deleteUser_asAdmin_returns204() throws Exception {
        mockMvc.perform(delete("/api/users/1"))
               .andExpect(status().isNoContent());
    }

    @Test
    @WithMockUser(roles = "USER")
    void deleteUser_asUser_returns403() throws Exception {
        // USER role doesn't have delete permission — should be rejected
        mockMvc.perform(delete("/api/users/1"))
               .andExpect(status().isForbidden());
    }
}

Frequently Asked Questions

What is the difference between authentication and authorization?
Authentication answers 'who are you?' — verifying identity via credentials. Authorization answers 'what can you do?' — checking permissions after identity is confirmed. Spring Security handles both: AuthenticationManager for the who, and access control rules (hasRole, @PreAuthorize) for the what.
What is the security filter chain?
Spring Security operates as a chain of servlet filters that intercept every HTTP request. Each filter handles one concern: CORS, CSRF protection, session management, authentication, authorization. You configure the chain in a SecurityFilterChain bean — the order and which filters are active controls how security behaves.
Should I use sessions or tokens (JWT) for REST APIs?
REST APIs should be stateless — use JWT tokens. Sessions require server-side state (a session store) which doesn't scale horizontally without sticky sessions or a shared Redis. JWT tokens carry the user's identity and roles in the token itself so any server can verify them without shared state.