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.
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
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"); } }
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); } }
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
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.
Tak, ale wymaga więcej manual configuration. Spring Boot znacznie upraszcza setup przez auto-configuration i starter dependencies.
Spring Security supports multiple authentication providers: database, LDAP, OAuth2, SAML. Configure multiple providers in AuthenticationManagerBuilder and they’ll be tried in order.
SpEL expressions used in @PreAuthorize, @PostAuthorize: hasRole(), hasAuthority(), authentication.name, etc. Pozwalają na complex authorization logic directly in annotations.
Use .rememberMe() in HttpSecurity configuration. Spring Security provides persistent and hash-based remember-me implementations out of the box.
Use stateless authentication (JWT tokens), disable CSRF for API endpoints, implement proper CORS configuration, and always validate authorization on each request.
Przydatne zasoby:
- Spring Security Reference Documentation
- Spring Guide – Securing a Web Application
- OWASP Top 10 Security Risks
- Spring Security GitHub Repository
🚀 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!