Spring Security – podstawy zabezpieczeń

TL;DR: Spring Security to framework do zabezpieczania aplikacji Spring. Obsługuje authentication (kto to jest) i authorization (co może robić). Konfiguruje się przez WebSecurityConfigurerAdapter i @EnableWebSecurity. Domyślnie zabezpiecza wszystkie endpointy, można dostosować przez configure() methods.

Bezpieczeństwo aplikacji web to nie opcja – to konieczność. Spring Security to potężny i elastyczny framework zabezpieczeń dla aplikacji Java, który integruje się seamlessly z ekosystemem Spring. W 2016 roku to de facto standard dla zabezpieczania aplikacji enterprise.

Dlaczego Spring Security jest ważne

W erze cyberzagrożeń każda aplikacja potrzebuje solidnych mechanizmów bezpieczeństwa. Spring Security oferuje comprehensive solution – od prostego basic auth po zaawansowane OAuth2 i SAML. Framework używany przez miliony aplikacji, battle-tested w największych korporacjach.

Data breaches kosztują firmy miliony dolarów i reputację. Spring Security implementuje industry standards i best practices, znacznie redukując ryzyko security vulnerabilities. Compliance z regulations (GDPR, SOX) często wymaga professional security framework.

Co się nauczysz:

  • Podstawowe koncepty: authentication vs authorization
  • Jak skonfigurować Spring Security w aplikacji
  • Different authentication methods (form login, basic auth, JWT)
  • Jak implementować authorization z roles i authorities
  • Best practices dla zabezpieczania REST API

Wymagania wstępne:

  • Solid knowledge of Spring Framework
  • Experience with Spring Boot
  • Understanding of HTTP protocol and web security basics
  • Familiarity with Maven/Gradle build tools

Security fundamentals

Analogia: Spring Security to jak system bezpieczeństwa w biurowcu. Authentication to reception desk która sprawdza kim jesteś (ID card). Authorization to access control – na które piętra możesz wejść (CEO floor, employee areas, public spaces).
Authentication – weryfikacja tożsamości user’a (login/password, certificate, biometrics)
Authorization – określenie co authenticated user może robić (read, write, admin operations)

Spring Security implement both aspects through configurable filters and providers:

Setup i podstawowa konfiguracja

Maven dependencies

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

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

Po dodaniu spring-boot-starter-security, Spring Boot automatycznie:

  • Zabezpiecza wszystkie endpointy
  • Generuje default user „user” z random password (w logach)
  • Konfiguruje basic HTTP authentication
  • Dodaje CSRF protection
  • Enables security headers

Basic security configuration

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/public/**").permitAll()
                .antMatchers("/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            .and()
            .formLogin()
                .loginPage("/login")
                .defaultSuccessUrl("/dashboard")
                .permitAll()
            .and()
            .logout()
                .logoutSuccessUrl("/login?logout")
                .permitAll();
    }
    
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth
            .inMemoryAuthentication()
                .withUser("user")
                .password("password")
                .roles("USER")
            .and()
                .withUser("admin")
                .password("admin")
                .roles("USER", "ADMIN");
    }
}
Pro tip: @EnableWebSecurity automatically imports Spring Security configuration. WebSecurityConfigurerAdapter provides sensible defaults which you override in configure() methods.

Authentication methods

Form-based authentication

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
        .authorizeRequests()
            .anyRequest().authenticated()
        .and()
        .formLogin()
            .loginPage("/login")
            .loginProcessingUrl("/perform-login")
            .defaultSuccessUrl("/dashboard", true)
            .failureUrl("/login?error=true")
            .usernameParameter("email")
            .passwordParameter("pwd")
        .and()
        .logout()
            .logoutUrl("/perform-logout")
            .logoutSuccessUrl("/login?logout")
            .deleteCookies("JSESSIONID");
}

HTTP Basic authentication

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
        .authorizeRequests()
            .anyRequest().authenticated()
        .and()
        .httpBasic()
            .realmName("My Application")
        .and()
        .csrf().disable(); // Disable CSRF for API endpoints
}

Database authentication

@Autowired
private DataSource dataSource;

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth
        .jdbcAuthentication()
        .dataSource(dataSource)
        .usersByUsernameQuery(
            "SELECT username, password, enabled FROM users WHERE username = ?")
        .authoritiesByUsernameQuery(
            "SELECT username, authority FROM authorities WHERE username = ?")
        .passwordEncoder(passwordEncoder());
}

@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}

Custom UserDetailsService

@Service
public class CustomUserDetailsService implements UserDetailsService {
    
    @Autowired
    private UserRepository userRepository;
    
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByUsername(username);
        if (user == null) {
            throw new UsernameNotFoundException("User not found: " + username);
        }
        
        return org.springframework.security.core.userdetails.User.builder()
            .username(user.getUsername())
            .password(user.getPassword())
            .authorities(getAuthorities(user.getRoles()))
            .accountExpired(false)
            .accountLocked(false)
            .credentialsExpired(false)
            .disabled(!user.isEnabled())
            .build();
    }
    
    private Collection<? extends GrantedAuthority> getAuthorities(Set<Role> roles) {
        return roles.stream()
                .map(role -> new SimpleGrantedAuthority("ROLE_" + role.getName()))
                .collect(Collectors.toList());
    }
}

// Configuration
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.userDetailsService(customUserDetailsService)
        .passwordEncoder(passwordEncoder());
}

Authorization i role-based access

Method-level security

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
}

@RestController
public class UserController {
    
    @GetMapping("/users")
    @PreAuthorize("hasRole('ADMIN')")
    public List<User> getAllUsers() {
        return userService.findAll();
    }
    
    @GetMapping("/users/{id}")
    @PreAuthorize("hasRole('USER') and #id == authentication.principal.id")
    public User getUser(@PathVariable Long id) {
        return userService.findById(id);
    }
    
    @PostMapping("/users")
    @Secured("ROLE_ADMIN")
    public User createUser(@RequestBody User user) {
        return userService.save(user);
    }
    
    @DeleteMapping("/users/{id}")
    @PreAuthorize("hasRole('ADMIN') or @userService.isOwner(#id, authentication.name)")
    public void deleteUser(@PathVariable Long id) {
        userService.delete(id);
    }
}

URL-based authorization

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
        .authorizeRequests()
            // Public endpoints
            .antMatchers("/api/public/**").permitAll()
            .antMatchers("/login", "/register").permitAll()
            .antMatchers(HttpMethod.GET, "/api/products/**").permitAll()
            
            // Role-based access
            .antMatchers("/api/admin/**").hasRole("ADMIN")
            .antMatchers("/api/moderator/**").hasAnyRole("ADMIN", "MODERATOR")
            
            // HTTP method restrictions
            .antMatchers(HttpMethod.POST, "/api/**").hasRole("USER")
            .antMatchers(HttpMethod.DELETE, "/api/**").hasRole("ADMIN")
            
            // Pattern matching
            .antMatchers("/api/users/*/profile").access("@securityService.canAccessProfile(authentication, #userId)")
            
            // Default
            .anyRequest().authenticated()
        .and()
        .oauth2Login()
        .and()
        .jwt();
}

Password encoding i security

@Configuration
public class PasswordConfig {
    
    @Bean
    public PasswordEncoder passwordEncoder() {
        // BCrypt with strength 12 (default 10)
        return new BCryptPasswordEncoder(12);
    }
    
    // Alternative encoders
    @Bean
    public PasswordEncoder scryptEncoder() {
        return new SCryptPasswordEncoder();
    }
    
    @Bean 
    public PasswordEncoder argon2Encoder() {
        return new Argon2PasswordEncoder();
    }
}

@Service
public class UserService {
    
    @Autowired
    private PasswordEncoder passwordEncoder;
    
    public User registerUser(String username, String rawPassword) {
        User user = new User();
        user.setUsername(username);
        user.setPassword(passwordEncoder.encode(rawPassword));
        return userRepository.save(user);
    }
    
    public boolean validatePassword(String rawPassword, String encodedPassword) {
        return passwordEncoder.matches(rawPassword, encodedPassword);
    }
}
Security warning: Never store passwords in plain text! Always use proper password encoders. BCrypt, SCrypt, or Argon2 are recommended for 2016.

REST API security

Stateless authentication z JWT

@Component
public class JwtTokenProvider {
    
    private final String secretKey = "mySecretKey";
    private final long validityInMilliseconds = 3600000; // 1h
    
    public String createToken(String username, List<String> roles) {
        Claims claims = Jwts.claims().setSubject(username);
        claims.put("roles", roles);
        
        Date now = new Date();
        Date validity = new Date(now.getTime() + validityInMilliseconds);
        
        return Jwts.builder()
            .setClaims(claims)
            .setIssuedAt(now)
            .setExpiration(validity)
            .signWith(SignatureAlgorithm.HS256, secretKey)
            .compact();
    }
    
    public String getUsername(String token) {
        return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token)
                .getBody().getSubject();
    }
    
    public boolean validateToken(String token) {
        try {
            Jws<Claims> claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token);
            return !claims.getBody().getExpiration().before(new Date());
        } catch (JwtException | IllegalArgumentException e) {
            return false;
        }
    }
}

@Component
public class JwtTokenFilter extends OncePerRequestFilter {
    
    @Autowired
    private JwtTokenProvider jwtTokenProvider;
    
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, 
                                  FilterChain filterChain) throws ServletException, IOException {
        String token = resolveToken(request);
        
        if (token != null && jwtTokenProvider.validateToken(token)) {
            String username = jwtTokenProvider.getUsername(token);
            UserDetails userDetails = userDetailsService.loadUserByUsername(username);
            
            UsernamePasswordAuthenticationToken authentication = 
                new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }
        
        filterChain.doFilter(request, response);
    }
    
    private String resolveToken(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");
        if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        }
        return null;
    }
}

CSRF protection

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
        // Enable CSRF for web applications
        .csrf()
            .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
        .and()
        
        // Disable CSRF for API endpoints
        .antMatcher("/api/**")
        .csrf().disable()
        
        // Custom CSRF configuration
        .csrf()
            .ignoringAntMatchers("/api/webhook/**")
            .csrfTokenRepository(new HttpSessionCsrfTokenRepository());
}

// Custom CSRF token endpoint
@RestController
public class CsrfController {
    
    @GetMapping("/api/csrf")
    public CsrfToken csrf(HttpServletRequest request) {
        return (CsrfToken) request.getAttribute(CsrfToken.class.getName());
    }
}

Security headers

@Override
protected void configure(HttpSecurity http) throws Exception {
    http
        .headers()
            .frameOptions().deny()
            .contentTypeOptions().and()
            .httpStrictTransportSecurity(hstsConfig -> 
                hstsConfig
                    .maxAgeInSeconds(31536000)
                    .includeSubdomains(true))
            .and()
        .sessionManagement()
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .maximumSessions(1)
            .maxSessionsPreventsLogin(false);
}

Testing Spring Security

@RunWith(SpringRunner.class)
@WebMvcTest(UserController.class)
public class UserControllerSecurityTest {
    
    @Autowired
    private MockMvc mockMvc;
    
    @Test
    public void shouldRequireAuthenticationForProtectedEndpoint() throws Exception {
        mockMvc.perform(get("/api/users"))
                .andExpect(status().isUnauthorized());
    }
    
    @Test
    @WithMockUser(roles = "ADMIN")
    public void shouldAllowAdminAccess() throws Exception {
        mockMvc.perform(get("/api/admin/users"))
                .andExpect(status().isOk());
    }
    
    @Test
    @WithMockUser(roles = "USER")
    public void shouldDenyUserAccessToAdminEndpoint() throws Exception {
        mockMvc.perform(get("/api/admin/users"))
                .andExpected(status().isForbidden());
    }
    
    @Test
    public void shouldAllowAnonymousAccessToPublicEndpoint() throws Exception {
        mockMvc.perform(get("/api/public/info"))
                .andExpected(status().isOk());
    }
}

Common security vulnerabilities

Top security issues to avoid:

  • Weak passwords: Enforce strong password policies
  • Session fixation: Spring Security handles this automatically
  • CSRF attacks: Enable CSRF protection for web forms
  • XSS: Validate and sanitize all input
  • SQL injection: Use parameterized queries
  • Insecure direct object references: Validate authorization for each resource

Best practices

✅ Security best practices:

  • Principle of least privilege: Grant minimum necessary permissions
  • Defense in depth: Multiple security layers
  • Secure defaults: Start restrictive, then open up as needed
  • Regular updates: Keep Spring Security and dependencies current
  • Audit logging: Log all authentication and authorization events
  • HTTPS everywhere: Never transmit credentials over HTTP
Błąd #1: Disabling CSRF protection without understanding implications – opens door to attacks.
Błąd #2: Using weak password encoders or storing plain text passwords.
Błąd #3: Overly permissive authorization rules – allowing too much access by default.
Błąd #4: Not testing security configuration – security bugs are critical bugs.
Jak Spring Security integruje się z Spring Boot?

Spring Boot auto-configuration automatically secures all endpoints when spring-boot-starter-security is on classpath. Provides sensible defaults with option to customize through WebSecurityConfigurerAdapter.

Czy mogę używać Spring Security bez Spring Boot?

Tak, ale wymaga więcej manual configuration. Spring Boot znacznie upraszcza setup przez auto-configuration i starter dependencies.

Jak obsłużyć różne authentication providers?

Spring Security supports multiple authentication providers: database, LDAP, OAuth2, SAML. Configure multiple providers in AuthenticationManagerBuilder and they’ll be tried in order.

Co to są security expressions?

SpEL expressions used in @PreAuthorize, @PostAuthorize: hasRole(), hasAuthority(), authentication.name, etc. Pozwalają na complex authorization logic directly in annotations.

Jak zaimplementować remember-me functionality?

Use .rememberMe() in HttpSecurity configuration. Spring Security provides persistent and hash-based remember-me implementations out of the box.

Jak secured REST API od unauthorized access?

Use stateless authentication (JWT tokens), disable CSRF for API endpoints, implement proper CORS configuration, and always validate authorization on each request.

Przydatne zasoby:

🚀 Zadanie dla Ciebie

Zaimplementuj complete security setup dla blog application:

  • User registration i login z bcrypt password encoding
  • Role-based access: ADMIN może zarządzać wszystkimi posts, USER tylko swoimi
  • REST API endpoints z JWT authentication
  • Method-level security z @PreAuthorize
  • Custom login page z CSRF protection
  • Integration tests dla security configuration

Przetestuj:

  • Unauthorized access returns 401
  • Insufficient privileges return 403
  • JWT tokens expire properly
  • CSRF protection działa dla forms

Masz pytania o Spring Security? Podziel się swoimi doświadczeniami w komentarzach – security jest complex topic, ale Spring Security makes it manageable!

Zostaw komentarz

Twój adres e-mail nie zostanie opublikowany. Wymagane pola są oznaczone *

Przewijanie do góry