Tutorial 30: Security Implementation 🔐
📋 Table of Contents
- Understanding the Question
- Solution Approach
- Prerequisites & Requirements
- Key Topics & Plan of Action
- Complete Implementation
- Important Considerations
- Practice Questions
1. Understanding the Question ❓
What are we trying to achieve?
- What is Spring Security? - Framework for providing authentication, authorization, and protection against common attacks
- Why does it exist? - Applications need robust security without reinventing the wheel
- When to use it? - Always in production applications handling user data
- How does it work? - Uses filters, authentication providers, and access control mechanisms
- What are best practices? - Use passwords hashing, HTTPS, CSRF protection, input validation, principle of least privilege
The Problem It Solves
Before Security:
java
// Unsecured endpoint - anyone can access
@GetMapping("/admin/users")
public List<User> getAllUsers() {
return userRepository.findAll(); // NO authentication check!
}
// Password stored as plaintext - CRITICAL VULNERABILITY
user.setPassword("password123"); // EXPOSED if database breached!
// No protection against CSRF attacks
// Attackers can forge requests on behalf of usersWith Spring Security:
java
// Secured endpoint - requires ADMIN role
@GetMapping("/admin/users")
@PreAuthorize("hasRole('ADMIN')")
public List<User> getAllUsers() {
return userRepository.findAll();
}
// Password automatically hashed with bcrypt
passwordEncoder.encode("password123"); // Hashed securely
// CSRF protection automatic
// Form tokens prevent cross-site forgery2. Solution Approach 🎯
Definition
Spring Security is a powerful and customizable authentication and access control framework for Java applications, providing protection against common vulnerabilities like CSRF, session fixation, and clickjacking.
Core Components
- Authentication: Who are you? (login, credentials verification)
- Authorization: What can you do? (permissions, roles)
- Protection: Security filters, CSRF tokens, HTTPS, headers
- Cryptography: Password encoding, encryption
- Audit: Logging security events
3. Complete Implementation 💻
Example 1: Basic Security Configuration
pom.xml
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>SecurityConfig.java
java
package com.example.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
@EnableMethodSecurity // Enable @PreAuthorize, @PostAuthorize
public class SecurityConfig {
/**
* Configure which URLs require authentication
*/
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf()
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.and()
.authorizeHttpRequests()
// Public endpoints
.requestMatchers("/", "/public/**", "/auth/login", "/auth/register").permitAll()
// Admin only
.requestMatchers("/admin/**").hasRole("ADMIN")
// User endpoints - require authentication
.requestMatchers("/user/**").authenticated()
// Everything else requires auth
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/auth/login")
.defaultSuccessUrl("/dashboard")
.and()
.logout()
.logoutUrl("/logout")
.logoutSuccessUrl("/")
.and()
.exceptionHandling()
.accessDeniedPage("/error/403");
return http.build();
}
/**
* Password encoder - never store plaintext passwords!
* BCrypt: Slow hashing algorithm resistant to brute force
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}AuthenticationController.java
java
package com.example.controller;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/auth")
@RequiredArgsConstructor
public class AuthenticationController {
private final UserService userService;
private final PasswordEncoder passwordEncoder;
@PostMapping("/register")
public ResponseEntity<?> register(@RequestBody RegisterRequest request) {
User user = new User();
user.setUsername(request.getUsername());
// Hash password before storing
user.setPassword(passwordEncoder.encode(request.getPassword()));
user.setEmail(request.getEmail());
user.setRoles(Set.of(new Role("ROLE_USER")));
userService.save(user);
return ResponseEntity.ok("User registered successfully");
}
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest request) {
// Spring Security handles authentication
// This is usually form-based or JWT-based
return ResponseEntity.ok("Login handled by Spring Security");
}
}Method Level Security
java
package com.example.service;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
@Service
public class UserService {
/**
* Only ADMIN role can delete users
*/
@PreAuthorize("hasRole('ADMIN')")
public void deleteUser(Long id) {
userRepository.deleteById(id);
}
/**
* User can only view their own data
*/
@PreAuthorize("hasRole('USER') and #userId == authentication.principal.id")
public User getUserData(Long userId) {
return userRepository.findById(userId).orElseThrow();
}
/**
* Deny access to specific role
*/
@PreAuthorize("!hasRole('BANNED')")
public void accessService() {
// Banned users cannot access this
}
}Example 2: JWT-Based Authentication
JwtTokenProvider.java
java
package com.example.security;
import io.jsonwebtoken.*;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.Date;
@Component
public class JwtTokenProvider {
@Value("${app.security.jwt.secret}")
private String jwtSecret;
@Value("${app.security.jwt.expiration}")
private long jwtExpirationMs;
/**
* Generate JWT token for authenticated user
*/
public String generateToken(Authentication authentication) {
UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal();
return Jwts.builder()
.setSubject(userPrincipal.getUsername())
.claim("roles", userPrincipal.getAuthorities())
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + jwtExpirationMs))
.signWith(SignatureAlgorithm.HS512, jwtSecret)
.compact();
}
/**
* Validate and parse JWT token
*/
public String getUserNameFromJWT(String token) {
return Jwts.parser()
.setSigningKey(jwtSecret)
.parseClaimsJws(token)
.getBody()
.getSubject();
}
/**
* Check if token is valid and not expired
*/
public boolean validateToken(String token) {
try {
Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token);
return true;
} catch (JwtException | IllegalArgumentException e) {
return false;
}
}
}JwtAuthenticationFilter.java
java
package com.example.security;
import org.springframework.web.filter.OncePerRequestFilter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtTokenProvider tokenProvider;
private final UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
try {
String jwt = getJwtFromRequest(request);
if (jwt != null && tokenProvider.validateToken(jwt)) {
String username = tokenProvider.getUserNameFromJWT(jwt);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception e) {
logger.error("Could not validate JWT", e);
}
filterChain.doFilter(request, response);
}
private String getJwtFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}Example 3: Role-Based Authorization
User and Role Entities
java
@Entity
@Data
public class User {
@Id
@GeneratedValue
private Long id;
private String username;
private String password; // Hashed!
private String email;
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(
name = "user_roles",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "role_id")
)
private Set<Role> roles = new HashSet<>();
}
@Entity
@Data
public class Role {
@Id
@GeneratedValue
private Long id;
private String name; // ADMIN, USER, MODERATOR
}UserDetailsService Implementation
java
package com.example.security;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
// Convert roles to Spring Security authorities
Set<GrantedAuthority> authorities = user.getRoles().stream()
.map(role -> new SimpleGrantedAuthority(role.getName()))
.collect(Collectors.toSet());
return new org.springframework.security.core.userdetails.User(
user.getUsername(),
user.getPassword(), // Hashed password stored in DB
authorities
);
}
}6. Important Considerations ⚠️
Best Practices
1. Always Hash Passwords
java
✅ DO: Use BCryptPasswordEncoder
PasswordEncoder encoder = new BCryptPasswordEncoder();
user.setPassword(encoder.encode(plainPassword));
❌ DON'T: Store plaintext passwords
user.setPassword(plainPassword); // CRITICAL VULNERABILITY!
📝 WHY: If DB is breached, plaintext passwords are compromised immediately2. Use HTTPS in Production
properties
✅ DO: Force HTTPS in production
server.ssl.enabled=true
security.require-https=true
❌ DON'T: Use HTTP for authentication
# Credentials transmitted in plaintext over network!
📝 WHY: HTTPS encrypts data in transit, prevents man-in-the-middle attacks3. Implement CSRF Protection
java
✅ DO: Enable CSRF tokens for form submissions
http.csrf()
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
❌ DON'T: Disable CSRF protection in production
// http.csrf().disable(); // INSECURE!
📝 WHY: CSRF tokens prevent attackers from forging requestsSecurity Checklist
- Password hashing with BCrypt or PBKDF2
- HTTPS/TLS in production
- CSRF protection enabled
- Authorization on all endpoints
- Input validation and sanitization
- No sensitive data in logs
- Security headers (X-Frame-Options, X-Content-Type-Options)
- Rate limiting on auth endpoints
- Account lockout after failed attempts
- Password reset with email verification
- Audit logging of security events
- Regular security updates
Practice Questions 📝
Question 1: Why hash passwords?
text
Q: Why not store passwords in plaintext?
A: If DB is breached:
- Plaintext: Attacker immediately has all passwords
- Hashed: Attacker cannot reverse hashes to passwords
Use BCrypt: Slow algorithm (milliseconds per password)
Makes brute force attacks infeasibleQuestion 2: Secure an admin endpoint
text
Q: Only admins can delete users
A:
@DeleteMapping("/users/{id}")
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<?> deleteUser(@PathVariable Long id) {
userRepository.deleteById(id);
return ResponseEntity.noContent().build();
}Question 3: JWT token validation
text
Q: How do you validate JWT tokens?
A:
@Component
public class JwtTokenProvider {
public boolean validateToken(String token) {
try {
Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token); // Will throw if invalid
return true;
} catch (JwtException e) {
return false;
}
}
}🎯 Key Takeaways
- ✅ Always hash passwords - Use BCryptPasswordEncoder
- ✅ HTTPS in production - Encrypt data in transit
- ✅ Enable CSRF protection - Prevent request forgery
- ✅ Authorization on endpoints - Use @PreAuthorize
- ✅ Principle of least privilege - Grant minimum necessary permissions
- ✅ Audit security events - Log authentication/authorization
Changelog
- 2025-11-23: Initial creation with JWT and role-based examples
- Added: Password hashing and CSRF protection patterns
- Added: Security configuration and best practices
Congratulations! You now master Spring Security! 🎉
Implement security from day one in your applications!