Skip to main content

Spring Security Fundamentals

Spring Security is a powerful and highly customizable authentication and authorization framework that is the de-facto standard for securing Spring-based applications. This guide will introduce you to the core concepts of Spring Security and help you implement basic security features in your Spring applications.

Introduction to Spring Security

Spring Security provides comprehensive security services for Java EE-based enterprise software applications. It focuses on two main areas:

  1. Authentication - Verifying the identity of a user, system, or service.
  2. Authorization - Determining if an authenticated entity has permission to access a resource.

Spring Security integrates seamlessly with Spring applications and offers protection against common security threats such as:

  • Session fixation
  • Clickjacking
  • Cross-site request forgery (CSRF)
  • Cross-site scripting (XSS)

Getting Started with Spring Security

Adding Dependencies

To add Spring Security to your project, include the following dependencies in your pom.xml (Maven) or build.gradle (Gradle) file:

Maven:

xml
<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>
</dependencies>

Gradle:

groovy
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-web'
}

Default Security Configuration

Once you add Spring Security to your project, it automatically secures all HTTP endpoints with basic authentication. When you start your application, you'll notice:

  1. All endpoints require authentication
  2. A login form is provided
  3. A default user named "user" is created
  4. A random password is generated at startup (visible in your application logs)

Console output when starting application:

Using generated security password: 8e557245-73e2-4286-969a-ff57fe326336

Basic Security Configuration

To customize Spring Security, create a configuration class:

java
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/public/**").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.permitAll()
)
.logout(logout -> logout
.permitAll());

return http.build();
}
}

This configuration:

  • Allows anyone to access URLs that start with /public/
  • Restricts /admin/ URLs to users with the "ADMIN" role
  • Requires authentication for all other requests
  • Sets up a custom login page
  • Configures logout functionality

User Authentication Methods

Spring Security offers several ways to authenticate users:

1. In-Memory Authentication

Ideal for testing or simple applications:

java
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;

@Configuration
public class UserConfig {

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

@Bean
public InMemoryUserDetailsManager userDetailsService() {
UserDetails user = User.builder()
.username("user")
.password(passwordEncoder().encode("password"))
.roles("USER")
.build();

UserDetails admin = User.builder()
.username("admin")
.password(passwordEncoder().encode("admin"))
.roles("USER", "ADMIN")
.build();

return new InMemoryUserDetailsManager(user, admin);
}
}

2. JDBC Authentication

For database-stored credentials:

java
@Bean
public DataSource dataSource() {
return new EmbeddedDatabaseBuilder()
.setType(EmbeddedDatabaseType.H2)
.addScript("classpath:schema.sql")
.addScript("classpath:data.sql")
.build();
}

@Bean
public UserDetailsService users(DataSource dataSource) {
return new JdbcUserDetailsManager(dataSource);
}

You'll need to provide SQL schema and data files that match Spring Security's expected structure or customize the queries.

3. Custom UserDetailsService

For custom authentication logic:

java
@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"));

return org.springframework.security.core.userdetails.User
.withUsername(user.getUsername())
.password(user.getPassword())
.roles(user.getRoles().toArray(new String[0]))
.build();
}
}

Password Encoding

Always encode passwords before storing them. Spring Security provides several password encoders:

java
@Bean
public PasswordEncoder passwordEncoder() {
// Most recommended encoder for production use
return new BCryptPasswordEncoder();

// Other options include:
// return new Pbkdf2PasswordEncoder();
// return new SCryptPasswordEncoder();
// return new Argon2PasswordEncoder();
}

Method Level Security

You can secure individual methods using annotations:

java
@Configuration
@EnableMethodSecurity
public class MethodSecurityConfig {
// Configuration goes here
}

Then in your services or controllers:

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

@GetMapping("/users")
@PreAuthorize("hasRole('ADMIN')")
public List<User> getAllUsers() {
// Only accessible to users with ADMIN role
return userService.findAll();
}

@GetMapping("/profile")
@PreAuthorize("hasRole('USER')")
public User getUserProfile(@AuthenticationPrincipal UserDetails userDetails) {
// Accessible to authenticated users
return userService.findByUsername(userDetails.getUsername());
}

@PostAuthorize("returnObject.username == authentication.name")
public User loadUser(Long id) {
// User can only access their own data
return userService.findById(id);
}
}

Real-World Example: Securing a REST API

Let's implement security for a REST API with JWT (JSON Web Token) authentication:

  1. Add JWT dependencies:
xml
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
<scope>runtime</scope>
</dependency>
  1. Create a JwtTokenUtil class:
java
@Component
public class JwtTokenUtil {

private final String secret = "yourSecretKey"; // In production, use environment variables
private final long jwtExpiration = 86400000; // 24 hours

public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
return createToken(claims, userDetails.getUsername());
}

private String createToken(Map<String, Object> claims, String subject) {
return Jwts.builder()
.setClaims(claims)
.setSubject(subject)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + jwtExpiration))
.signWith(Keys.hmacShaKeyFor(secret.getBytes()), SignatureAlgorithm.HS256)
.compact();
}

public String getUsernameFromToken(String token) {
return getClaimFromToken(token, Claims::getSubject);
}

public Date getExpirationDateFromToken(String token) {
return getClaimFromToken(token, Claims::getExpiration);
}

public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
final Claims claims = getAllClaimsFromToken(token);
return claimsResolver.apply(claims);
}

private Claims getAllClaimsFromToken(String token) {
return Jwts.parserBuilder()
.setSigningKey(Keys.hmacShaKeyFor(secret.getBytes()))
.build()
.parseClaimsJws(token)
.getBody();
}

public Boolean validateToken(String token, UserDetails userDetails) {
final String username = getUsernameFromToken(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}

private Boolean isTokenExpired(String token) {
final Date expiration = getExpirationDateFromToken(token);
return expiration.before(new Date());
}
}
  1. Create a JwtAuthenticationFilter:
java
public class JwtAuthenticationFilter extends OncePerRequestFilter {

private final UserDetailsService userDetailsService;
private final JwtTokenUtil jwtTokenUtil;

public JwtAuthenticationFilter(UserDetailsService userDetailsService, JwtTokenUtil jwtTokenUtil) {
this.userDetailsService = userDetailsService;
this.jwtTokenUtil = jwtTokenUtil;
}

@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {

final String authorizationHeader = request.getHeader("Authorization");

String username = null;
String jwt = null;

if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
jwt = authorizationHeader.substring(7);
try {
username = jwtTokenUtil.getUsernameFromToken(jwt);
} catch (Exception e) {
// Invalid token
}
}

if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);

if (jwtTokenUtil.validateToken(jwt, userDetails)) {
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());

authentication.setDetails(
new WebAuthenticationDetailsSource().buildDetails(request));

SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
chain.doFilter(request, response);
}
}
  1. Configure security for the JWT-based authentication:
java
@Configuration
@EnableWebSecurity
public class SecurityConfig {

private final UserDetailsService userDetailsService;
private final JwtTokenUtil jwtTokenUtil;
private final PasswordEncoder passwordEncoder;

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

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/api/auth/**").permitAll()
.anyRequest().authenticated()
)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.addFilterBefore(
new JwtAuthenticationFilter(userDetailsService, jwtTokenUtil),
UsernamePasswordAuthenticationFilter.class
);

return http.build();
}

@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration authConfig) throws Exception {
return authConfig.getAuthenticationManager();
}
}
  1. Create an authentication controller:
java
@RestController
@RequestMapping("/api/auth")
public class AuthController {

private final AuthenticationManager authenticationManager;
private final UserDetailsService userDetailsService;
private final JwtTokenUtil jwtTokenUtil;

public AuthController(
AuthenticationManager authenticationManager,
UserDetailsService userDetailsService,
JwtTokenUtil jwtTokenUtil) {
this.authenticationManager = authenticationManager;
this.userDetailsService = userDetailsService;
this.jwtTokenUtil = jwtTokenUtil;
}

@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest loginRequest) {
try {
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
loginRequest.getUsername(),
loginRequest.getPassword()
)
);
} catch (BadCredentialsException e) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body("Invalid username or password");
}

final UserDetails userDetails = userDetailsService
.loadUserByUsername(loginRequest.getUsername());

final String token = jwtTokenUtil.generateToken(userDetails);

return ResponseEntity.ok(new AuthResponse(token));
}

// Helper classes
public static class LoginRequest {
private String username;
private String password;

// Getters and setters
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getPassword() { return password; }
public void setPassword(String password) { this.password = password; }
}

public static class AuthResponse {
private String token;

public AuthResponse(String token) {
this.token = token;
}

public String getToken() { return token; }
public void setToken(String token) { this.token = token; }
}
}

Common Security Headers

Spring Security can automatically add important security headers to HTTP responses:

java
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// Other configuration...
.headers(headers -> headers
.xssProtection(xss -> xss.enable())
.contentSecurityPolicy(csp -> csp.policyDirectives("default-src 'self'"))
.frameOptions(frame -> frame.deny())
.cacheControl(cache -> cache.disable())
);

return http.build();
}

CORS Configuration

To handle Cross-Origin Resource Sharing:

java
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("https://example.com"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE"));
configuration.setAllowCredentials(true);
configuration.setAllowedHeaders(Arrays.asList("Authorization", "Content-Type"));

UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
// Other configuration...

return http.build();
}

Summary

In this guide, we've covered the fundamentals of Spring Security:

  • Basic authentication and authorization concepts
  • Adding Spring Security to your project
  • Configuring authentication with different user stores
  • Password encoding
  • Method-level security
  • Implementing JWT authentication for RESTful APIs
  • Security headers and CORS configuration

Spring Security provides a robust foundation for securing your applications, and this guide should give you a solid starting point to implement security in your own Spring projects.

Additional Resources

Practice Exercises

  1. Basic Authentication: Create a simple Spring Boot application with form-based authentication that has both USER and ADMIN roles.

  2. Database Authentication: Extend the application to use a database to store user credentials using JdbcUserDetailsManager.

  3. REST API with JWT: Implement a RESTful API with JWT authentication that allows users to register, login, and access protected resources.

  4. OAuth2 Integration: Add OAuth2 support to allow users to login using Google or GitHub.

  5. Custom Authentication: Implement a custom authentication mechanism such as API key authentication for your API.

With these fundamentals, you're well on your way to building secure Spring applications!



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)