Spring is a dominant framework for building robust, scalable, production-grade applications. It simplifies web development with Spring Web and enforces strong security patterns through Spring Security. Interviewers commonly evaluate candidates on these to assess their ability to build secure, RESTful applications — a core skill in most backend or full-stack roles.
Spring Web
Spring MVC and Controllers
Spring MVC is based on the Model-View-Controller architecture, which helps organize code by separating the different parts of an application: input, UI, and business logic.
You can add it to a project using the
spring-boot-starter-webdependency.An embedded server is the component of a web app required to run it. The default server for Spring MVC is Apache Tomcat.
The default port is
8080, and the context path is empty. You can change the default port and context path using theapplication.propertiesfile.To help with implementing MVC, we will need a template engine. A template engine allows us to write static template files in our application. Three template engines commonly used in Spring are: JSP, Thymeleaf, Freemarker.
You can declare a
Controllerusing annotations like@Controller,@RestController; and map URLs using@GetMapping,@PostMapping,@PutMapping,@DeleteMapping, etc.
@RestController
@RequestMapping("/api")
class UserController {
// Handles GET requests to /api/users
@GetMapping("/users")
fun getUsers(): List<String> = listOf("Alice", "Bob")
}@RestController
@RequestMapping("/api")
public class UserController {
// Handles GET requests to /api/users
@GetMapping("/users")
public List<String> getUsers() {
return List.of("Alice", "Bob");
}
}Request Handling
Use
@RequestBodyto bind request JSON to model objects.Use
@PathVariableand@RequestParamfor route and query parameters.
// Using @RequestBody: Accepts JSON payload to create a user
@PostMapping("/register")
fun register(@RequestBody user: User): ResponseEntity<String> { ... }
// Using @PathVariable: Fetches user details based on path ID
@GetMapping("/user/{id}")
fun getUser(@PathVariable id: Long): User { ... }
// Using @RequestParam: Filters users by name via query parameter
@GetMapping("/search")
fun searchUserByName(@RequestParam name: String): List<User> { ... }// Using @RequestBody: Accepts JSON payload to create a user
@PostMapping("/register")
public ResponseEntity<String> register(@RequestBody User user) { ... }
// Using @PathVariable: Fetches user details based on path ID
@GetMapping("/user/{id}")
public User getUser(@PathVariable Long id) { ... }
// Using @RequestParam: Filters users by name via query parameter
@GetMapping("/search")
public List<User> searchUserByName(@RequestParam String name) { ... }Exception Handling
Use
@ControllerAdvicewith@ExceptionHandlerto manage errors globally.// @ControllerAdvice makes this class a global exception handler for all controllers @ControllerAdvice class GlobalExceptionHandler { // This method handles exceptions of type // `UserNotFoundException` thrown in any controller // and returns a 404 Not Found response with a custom message @ExceptionHandler(UserNotFoundException::class) fun handleNotFound(): ResponseEntity<String> = ResponseEntity("User not found", HttpStatus.NOT_FOUND) }// @ControllerAdvice makes this class a global exception handler for all controllers @ControllerAdvice public class GlobalExceptionHandler { // This method handles exceptions of type // `UserNotFoundException` thrown in any controller // and returns a 404 Not Found response with a custom message @ExceptionHandler(UserNotFoundException.class) public ResponseEntity<String> handleNotFound() { return new ResponseEntity<>("User not found", HttpStatus.NOT_FOUND); } }In the
@ControllerAdviceclass, we can handle not only custom exceptions but also standard exceptions thrown by Spring itself, usingResponseEntityExceptionHandlerabstract class.// Global exception handler for controller-related errors @ControllerAdvice class ControllerExceptionHandler : ResponseEntityExceptionHandler() { // Handles validation errors when @Valid fails on method arguments override fun handleMethodArgumentNotValid( ex: MethodArgumentNotValidException, headers: HttpHeaders, status: HttpStatusCode, request: WebRequest ): ResponseEntity<Any> { // Custom error response logic here } }// Global exception handler for controller-related errors @ControllerAdvice public class ControllerExceptionHandler extends ResponseEntityExceptionHandler { // Handles validation errors when @Valid fails on method arguments @Override protected ResponseEntity<Object> handleMethodArgumentNotValid( MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatusCode status, WebRequest request ) { // Custom error response logic here } }
Bean Validation
In web-based applications, a client can send data to the server. So, it is important to handle the cases if a user sends data which violates the business logic of the application. We do it using JSR-380 annotations like:
@NotNull,@NotEmpty,@NotBlank,@Size,@Min,@Max,@Patternand@Emailon model classes.class SpecialAgent( @NotNull var name: String?, @NotEmpty var motto: String, @NotBlank var status: String, @Size(min = 1, max = 3) var code: String, @Size(min = 0, max = 4) var cars: List<String>, @Min(value = 18) var age: Int, @Max(5) var numberOfCurrentMissions: Int, @Pattern(regexp = "[0-9]{1,3}") var anotherCode: String, @NotNull @Email var email: String? )public class SpecialAgent { @NotNull private String name; @NotEmpty private String motto; @NotBlank private String status; @Size(min = 1, max = 3) private String code; @Size(min = 0, max = 4) private List<String> cars; @Min(value = 18) private int age; @Max(5) private int numberOfCurrentMissions; @Pattern(regexp = "[0-9]{1,3}") private String anotherCode; @NotNull @Email private String email; // Getters and setters omitted for brevity }Use the
@Validatedannotation for path variables and request parameters@RestController class SpecialAgentController { @PostMapping("/agent") fun validate(@RequestBody @Valid agent: SpecialAgent): ResponseEntity<String> { return ResponseEntity.ok("Agent info is valid.") } }@RestController public class SpecialAgentController { @PostMapping("/agent") public ResponseEntity<String> validate(@RequestBody @Valid SpecialAgent agent) { return ResponseEntity.ok("Agent info is valid."); } }Use the
@Validannotation in controllers@RestController @Validated class SpecialAgentController { @GetMapping("/agents/{id}") fun validateAgentPathVariable(@PathVariable("id") @Min(1) id: Int): ResponseEntity<String> { return ResponseEntity.ok("Agent id is valid.") } @GetMapping("/agents") fun validateAgentRequestParam( @RequestParam("code") @Pattern(regexp = "[0-9]{1,3}") code: String ): ResponseEntity<String> { return ResponseEntity.ok("Agent code is valid.") } }@RestController @Validated public class SpecialAgentController { @GetMapping("/agents/{id}") public ResponseEntity<String> validateAgentPathVariable( @PathVariable("id") @Min(1) int id ) { return ResponseEntity.ok("Agent id is valid."); } @GetMapping("/agents") public ResponseEntity<String> validateAgentRequestParam( @RequestParam("code") @Pattern(regexp = "[0-9]{1,3}") String code ) { return ResponseEntity.ok("Agent code is valid."); } }
You can use all these annotations from the jakarta.validation package by adding the spring-boot-starter-validation dependency to your project.
REST and HTTP Communication
It's important to use the correct HTTP status codes to represent the outcome of operations. HTTP status codes are grouped into ranges based on their meaning:
1xx (Informational) – Request received, continuing process (rarely used directly)
2xx (Success) – Request was successfully received, understood, and accepted
200 OK– successful GET or general request201 Created– resource created successfully (e.g., after POST)204 No Content– request successful, no content to return (e.g., after DELETE)
3xx (Redirection) – Further action needed to complete the request
301 Moved Permanently,302 Found
4xx (Client Error) – The client made an error
400 Bad Request– malformed or invalid request401 Unauthorized– authentication failed or missing403 Forbidden– authenticated but not allowed404 Not Found– requested resource doesn’t exist409 Conflict– request conflicts with current state
5xx (Server Error) – The server failed to fulfill a valid request
500 Internal Server Error– general server error503 Service Unavailable– server is temporarily overloaded or down
Properly using these status codes helps clients understand what happened and how to handle the response.
For REST calls in Spring applications, use:
RestTemplatefor synchronous/blocking operations. It provides convenient methods for performing the most common REST verbs, includingGET,POST,PUT, andDELETE, making it easy to interact with a RESTful API.val restTemplate = RestTemplate() // GET: fetch data from an endpoint val getResponse = restTemplate.getForEntity("https://api.example.com/data", String::class.java) // POST: send data to create a new resource val requestBody = YourRequestObject(...) val postResponse = restTemplate.postForEntity("https://api.example.com/data", requestBody, String::class.java) // PUT: update an existing resource val updatedData = YourUpdatedObject(...) restTemplate.put("https://api.example.com/data/123", updatedData) // DELETE: remove a resource restTemplate.delete("https://api.example.com/data/123")RestTemplate restTemplate = new RestTemplate(); // GET: fetch data from an endpoint ResponseEntity<String> getResponse = restTemplate.getForEntity("https://api.example.com/data", String.class); // POST: send data to create a new resource YourRequestObject requestBody = new YourRequestObject(...); ResponseEntity<String> postResponse = restTemplate.postForEntity("https://api.example.com/data", requestBody, String.class); // PUT: update an existing resource YourUpdatedObject updatedData = new YourUpdatedObject(...); restTemplate.put("https://api.example.com/data/123", updatedData); // DELETE: remove a resource restTemplate.delete("https://api.example.com/data/123");WebClientfor asynchronous and non-blocking operations (preferred in reactive apps). It is a non-blocking, reactive HTTP client provided by Spring WebFlux. It's recommended for making asynchronous HTTP requests or when you need better scalability, especially in reactive applications.val webClient = WebClient.create("https://api.example.com") // GET: fetch data from an endpoint val getResponse = webClient.get() .uri("/data") .retrieve() .bodyToMono(String::class.java) .block() // block to get the result in a non-reactive environment (use carefully in production) // POST: send data to create a new resource val requestBody = YourRequestObject(...) val postResponse = webClient.post() .uri("/data") .bodyValue(requestBody) .retrieve() .bodyToMono(String::class.java) .block() // PUT: update an existing resource val updatedData = YourUpdatedObject(...) webClient.put() .uri("/data/123") .bodyValue(updatedData) .retrieve() .toBodilessEntity() .block() // block if you want to wait for completion in a non-reactive way // DELETE: remove a resource webClient.delete() .uri("/data/123") .retrieve() .toBodilessEntity() .block()WebClient webClient = WebClient.create("https://api.example.com"); // GET: fetch data from an endpoint String getResponse = webClient.get() .uri("/data") .retrieve() .bodyToMono(String.class) .block(); // block to get the result in a non-reactive environment (use carefully in production) // POST: send data to create a new resource YourRequestObject requestBody = new YourRequestObject(/*...*/); String postResponse = webClient.post() .uri("/data") .bodyValue(requestBody) .retrieve() .bodyToMono(String.class) .block(); // PUT: update an existing resource YourUpdatedObject updatedData = new YourUpdatedObject(/*...*/); webClient.put() .uri("/data/123") .bodyValue(updatedData) .retrieve() .toBodilessEntity() .block(); // block if you want to wait for completion in a non-reactive way // DELETE: remove a resource webClient.delete() .uri("/data/123") .retrieve() .toBodilessEntity() .block();
Spring Security
At some point, you'll want to protect your application and differentiate users to personalize content or enforce access control. Spring Security is a module of the Spring Framework that handles authentication and authorization.
Spring Security acts as a security layer between the client and your application, intercepting all incoming requests and allowing you to configure which users can access which resources or functionalities. To use Spring Security, simply add the spring-boot-starter-security dependency to your project.
Once enabled, Spring Security intercepts client requests using a chain of filters. Some of these filters delegate authentication tasks to an AuthenticationManager. If authentication is successful, user details are stored in the SecurityContext, making them available throughout the lifecycle of the request.
Filter chain
The Spring Security filter chain processes incoming HTTP requests and enforces security controls such as authentication and authorization. It consists of a series of filters executed in a specific order, from top to bottom. You can rely on the default filters or customize the chain by adding or modifying filters as needed.
The order of filters is crucial — it determines how requests are processed and how access is granted or denied. Filters are usually arranged from less strict to more strict, to avoid processing errors or unintended behavior.
There are several default security filters. These filters can be customized, replaced, or disabled. Here are the most common ones:
UsernamePasswordAuthenticationFilteris a filter that performs user authentication based on the username and password passed as parameters in the request and then calls theAuthenticationManagerto authenticate the user.BasicAuthenticationFilteris a filter that looks for the standard authorization header ("Authorization") and then calls theAuthenticationManagerto authenticate the user.SecurityContextPersistenceFilterpopulatesSecurityContextbased onHttpSession, or creates a new one if it doesn't exist.LogoutFilteris a filter that logs out the user, invalidating the session and deleting the authentication information.ExceptionTranslationFilter: Catches security exceptions and returns appropriate HTTP responses (e.g., redirects or 401s).AuthorizationFilter: Restricts URL access usingAuthorizationManager; last in the filter chain.RememberMeAuthenticationFilter: Enables "remember me" login via tokens or cookies; not included by default.CsrfFilter: Protects against CSRF attacks by validating CSRF tokens.
Authentication configuration
Authentication is the process of verifying a user's identity before granting access to the application. By default, adding the Spring Security starter dependency automatically enables authentication and creates a default user. However, in most cases, we need to configure custom users.
To configure authentication in Spring Security 6.1.0, follow these steps:
Create a Security Configuration Class
Start by creating a configuration class and defining a
UserDetailsServicebean. This bean provides in-memory user details using the built-inUserclass andInMemoryUserDetailsManager.@Configuration class SecurityConfig { @Bean fun userDetailsService(): UserDetailsService { val user1 = User.withUsername("user1") .password(passwordEncoder().encode("pass1")) .roles() .build() val user2 = User.withDefaultPasswordEncoder() .username("user2") .password("pass2") .roles() .build() return InMemoryUserDetailsManager(user1, user2) } @Bean fun passwordEncoder(): PasswordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder() }@Configuration public class SecurityConfig { @Bean public UserDetailsService userDetailsService() { UserDetails user1 = User.withUsername("user1") .password(passwordEncoder().encode("pass1")) .roles() .build(); UserDetails user2 = User.withDefaultPasswordEncoder() .username("user2") .password("pass2") .roles() .build(); return new InMemoryUserDetailsManager(user1, user2); } @Bean public PasswordEncoder passwordEncoder() { return PasswordEncoderFactories.createDelegatingPasswordEncoder(); } }User.withUsernameandUser.withDefaultPasswordEncoderare used to define users.InMemoryUserDetailsManagerstores user data in memory.A
PasswordEncoderis required to encode passwords, even in memory.
withDefaultPasswordEncoder()is deprecated and not recommended for production use. It should be used only for demo or testing purposes.Understand Password Encoding
Spring Security enforces password encoding to prevent storing plain text passwords. Implementations of
PasswordEncoder(likeBCryptPasswordEncoder) provide secure encoding and verification mechanisms.encode(rawPassword)encodes the password for storage.matches(rawPassword, encodedPassword)checks if the password is correct during login.
You can create a delegating encoder as shown above or use a specific one like
BCryptPasswordEncoder.Customize HTTP Security
You can define a
SecurityFilterChainbean to control the authentication mechanisms like form-based login and HTTP basic auth:@Bean fun securityFilterChain(http: HttpSecurity): SecurityFilterChain = http .authorizeHttpRequests { it.anyRequest().authenticated() } .formLogin(Customizer.withDefaults()) .httpBasic(Customizer.withDefaults()) .build()@Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { return http .authorizeHttpRequests(auth -> auth.anyRequest().authenticated()) .formLogin(Customizer.withDefaults()) .httpBasic(Customizer.withDefaults()) .build(); }authorizeHttpRequestsensures all endpoints require authentication.formLoginandhttpBasicenable the respective authentication methods with default settings.Customizations can be added using
Customizerlambdas.
When you define a custom
SecurityFilterChain, the default configuration is overridden. Be sure to explicitly enable the desired features.
Authorization configuration
In Spring Security, authorization is about controlling access to resources based on the user's identity. To distinguish users and assign access, Spring provides the concepts of roles and authorities.
Authorities are granular permissions like
READ,WRITE,DELETEandUPDATE. For example, user "Jessica" might haveREADandWRITE, while "Joseph" has all four.Roles are groups of authorities. For example,
ROLE_USERmay includeREADandWRITE, whileROLE_ADMINmay include all four permissions.
In practice, roles and authorities are often interchangeable: roles("ADMIN") is equivalent to authorities("ROLE_ADMIN"). Note: roles(...) auto-adds the ROLE_ prefix; authorities(...) requires you to add it manually.
Let’s look at an example app with these endpoints:
GET /,GET /public— accessible to all (unauthenticated users too).GET /secured— for any authenticated user.GET /user— for users with rolesUSERorADMIN.GET /admin— for users with theADMINrole.POST /public— for users with theWRITEauthority.
To support this, we’ll create a REST controller:
@RestController
class DemoController {
@PostMapping("/public")
fun postPublic() = "Access to 'POST /public' granted"
@GetMapping("/public")
fun getPublic() = "Access to 'GET /public' granted"
@GetMapping("/secured") fun secured() = "Access to '/secured' granted"
@GetMapping("/user")
fun user() = "Access to '/user' granted"
@GetMapping("/admin")
fun admin() = "Access to '/admin' granted"
}@RestController
public class DemoController {
@PostMapping("/public")
public String postPublic() {
return "Access to 'POST /public' granted";
}
@GetMapping("/public")
public String getPublic() {
return "Access to 'GET /public' granted";
}
@GetMapping("/secured")
public String secured() {
return "Access to '/secured' granted";
}
@GetMapping("/user")
public String user() {
return "Access to '/user' granted";
}
@GetMapping("/admin")
public String admin() {
return "Access to '/admin' granted";
}
}And we'll define three users in memory:
@Configuration
class SecurityConfig {
@Bean
fun userDetailsService(): UserDetailsService {
val user1 = User.withUsername("user1")
.password(passwordEncoder().encode("pass1"))
.authorities("WRITE")
.build()
val user2 = User.withUsername("user2")
.password(passwordEncoder().encode("pass2"))
.roles("USER")
.build()
val user3 = User.withUsername("user3")
.password(passwordEncoder().encode("pass3"))
.authorities("ROLE_ADMIN", "WRITE")
.build()
return InMemoryUserDetailsManager(user1, user2, user3)
}
@Bean
fun passwordEncoder(): PasswordEncoder = BCryptPasswordEncoder()
}@Configuration
public class SecurityConfig {
@Bean
public UserDetailsService userDetailsService() {
UserDetails user1 = User.withUsername("user1")
.password(passwordEncoder().encode("pass1"))
.authorities("WRITE")
.build();
UserDetails user2 = User.withUsername("user2")
.password(passwordEncoder().encode("pass2"))
.roles("USER")
.build();
UserDetails user3 = User.withUsername("user3")
.password(passwordEncoder().encode("pass3"))
.authorities("ROLE_ADMIN", "WRITE")
.build();
return new InMemoryUserDetailsManager(user1, user2, user3);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}Remember: authorities are case-sensitive, and when using authorities(), you must include ROLE_ if assigning a role.
Now let’s configure access to endpoints using the SecurityFilterChain:
@Bean
fun securityFilterChain(http: HttpSecurity): SecurityFilterChain =
http
.authorizeHttpRequests { auth ->
auth
.requestMatchers("/user").hasAnyRole("USER", "ADMIN")
.requestMatchers("/admin").hasRole("ADMIN")
.requestMatchers(HttpMethod.POST, "/public").hasAuthority("WRITE")
.requestMatchers("/secured").authenticated()
.requestMatchers(HttpMethod.GET, "/*").permitAll()
.anyRequest().denyAll()
}
.httpBasic(Customizer.withDefaults())
.csrf { it.disable() }
.build()@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
return http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/user").hasAnyRole("USER", "ADMIN")
.requestMatchers("/admin").hasRole("ADMIN")
.requestMatchers(HttpMethod.POST, "/public").hasAuthority("WRITE")
.requestMatchers("/secured").authenticated()
.requestMatchers(HttpMethod.GET, "/*").permitAll()
.anyRequest().denyAll()
)
.httpBasic(Customizer.withDefaults())
.csrf(csrf -> csrf.disable())
.build();
}Explanation:
hasRole("ADMIN")→ only users withROLE_ADMINcan access/admin.hasAuthority("WRITE")→ only users with this authority canPOST /public.authenticated()→ any logged-in user can access/secured.permitAll()→ allows publicGETaccess to/and/public.denyAll()→ blocks all other undefined requests.Order matters: specific matchers must come before more general ones to avoid incorrect access.
With this setup, your Spring Security configuration supports roles, authorities, and endpoint-specific authorization with HTTP Basic Auth.
Method-level authorization
In Spring Security, endpoint-level authorization controls access based on URL patterns. But for fine-grained control — like restricting specific service methods — method-level authorization is a better choice. Spring supports this through annotations such as @PreAuthorize, @PostAuthorize, @PreFilter, and @PostFilter.
To enable it, use @EnableMethodSecurity in your security configuration:
@Configuration
@EnableMethodSecurity
class SecurityConfig {
@Bean
fun userDetailsService(): UserDetailsService =
InMemoryUserDetailsManager(
User.withUsername("john").password("{noop}123").authorities("read").build(),
User.withUsername("jane").password("{noop}123").authorities("write").build()
)
}@Configuration
@EnableMethodSecurity
public class SecurityConfig {
@Bean
public UserDetailsService userDetailsService() {
return new InMemoryUserDetailsManager(
User.withUsername("john").password("{noop}123").authorities("read").build(),
User.withUsername("jane").password("{noop}123").authorities("write").build()
);
}
}@PreAuthorize
Evaluated before the method runs. The method executes only if the condition is true.
@GetMapping("/hello")
@PreAuthorize("hasAuthority('read')")
fun hello(): String = "Hello!"@GetMapping("/profile/{name}")
@PreAuthorize("authentication.name == #name")
fun profile(@PathVariable name: String): String = "You are $name"@GetMapping("/hello")
@PreAuthorize("hasAuthority('read')")
public String hello() {
return "Hello!";
}@GetMapping("/profile/{name}")
@PreAuthorize("authentication.name == #name")
public String profile(@PathVariable String name) {
return "You are " + name;
}@PostAuthorize
Evaluated after the method returns. Useful when access depends on the result.
@GetMapping("/details")
@PostAuthorize("@dm.decide(returnObject)")
fun getName(): UserDetails? =
SecurityContextHolder.getContext().authentication.principal as? UserDetails@Component("dm")
class DecisionMaker {
fun decide(userDetails: UserDetails?): Boolean =
userDetails?.username?.startsWith("jo") == true
}@GetMapping("/details")
@PostAuthorize("@dm.decide(returnObject)")
public UserDetails getName() {
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (principal instanceof UserDetails) {
return (UserDetails) principal;
}
return null;
}@Component("dm")
class DecisionMaker {
public boolean decide(UserDetails userDetails) {
return userDetails != null && userDetails.getUsername().startsWith("jo");
}
}@PreFilter
Filters input collections before method execution.
@Service
class DemoService {
@PreFilter(filterTarget = "names", value = "filterObject != authentication.name")
fun process(names: List<String>): List<String> =
names.map { it.uppercase() }
}@Service
public class DemoService {
@PreFilter(filterTarget = "names", value = "filterObject != authentication.name")
public List<String> process(List<String> names) {
return names.stream()
.map(String::toUpperCase)
.collect(Collectors.toList());
}
}@PostFilter
Filters return values after method execution.
@PostFilter("filterObject == authentication.name")
fun getNames(): List<String> = listOf("john", "jane", "bob")@PostFilter("filterObject == authentication.name")
public List<String> getNames() {
return List.of("john", "jane", "bob");
}These annotations support SpEL (Spring Expression Language) to create dynamic rules like:
@PreAuthorize("hasRole('ADMIN')")@PreAuthorize("#username == authentication.name")
Crypto
Spring Security’s Crypto module provides support for encryption, decryption, and password hashing. It is included by default with spring-boot-starter-security. If you're not using Spring Boot, you can include the spring-security-crypto dependency manually.
Encryption protects sensitive data (e.g., credit card numbers) and requires a key to both encrypt and decrypt. Symmetric encryption (like AES) uses the same key for both. Store keys securely and comply with standards like PCI DSS.
Hashing is used for passwords. Hashed passwords can't be reversed — users must reset them if forgotten. To defend against brute force and rainbow table attacks, use strong hashing algorithms (e.g., BCrypt) and salt (a unique, random string added to each password before hashing).
Spring Crypto provides two main interfaces for encryption: BytesEncryptor and TextEncryptor. The former works with byte arrays, while the latter is used for strings. You can create implementations of both using static methods from the Encryptors class. Here’s how to declare a BytesEncryptor bean:
@Bean
open fun aesBytesEncryptor(): BytesEncryptor {
val password = "hackme" // should be kept in a secure place and not be shared
val salt = "8560b4f4b3" // should be hex-encoded with even number of chars
return Encryptors.standard(password, salt)
}@Bean
public BytesEncryptor aesBytesEncryptor() {
String password = "hackme"; // should be kept in a secure place and not be shared
String salt = "8560b4f4b3"; // should be hex-encoded with even number of chars
return Encryptors.standard(password, salt);
}This encryptor uses a 256-bit AES symmetric encryption algorithm — one of the most secure standards approved for top-secret information.
Let’s see how it works:
val inputData = byteArrayOf(104, 121, 112, 101, 114, 115, 107, 105, 108, 108)
val encryptedData = bytesEncryptor.encrypt(inputData)
val decryptedData = bytesEncryptor.decrypt(encryptedData)
println("Input data: $inputData")
println("Encrypted data: $encryptedData")
println("Decrypted data: $decryptedData")byte[] inputData = new byte[]{104, 121, 112, 101, 114, 115, 107, 105, 108, 108};
byte[] encryptedData = bytesEncryptor.encrypt(inputData);
byte[] decryptedData = bytesEncryptor.decrypt(encryptedData);
System.out.printf("Input data: %s%n", Arrays.toString(inputData));
System.out.printf("Encrypted data: %s%n", Arrays.toString(encryptedData));
System.out.printf("Decrypted data: %s%n", Arrays.toString(decryptedData));If your data is in string format, it’s better to use TextEncryptor. There are two implementations: NoOpTextEncryptor, which does nothing and should be avoided, and HexEncodingTextEncryptor, which wraps BytesEncryptor and produces hex-encoded output that’s easier to store.
@Bean
open fun hexEncodingTextEncryptor(): TextEncryptor {
val password = "hackme"
val salt = "8560b4f4b3"
return Encryptors.text(password, salt)
}@Bean
public TextEncryptor hexEncodingTextEncryptor() {
String password = "hackme";
String salt = "8560b4f4b3";
return Encryptors.text(password, salt);
}Example usage:
val inputData = "hyperskill"
val encryptedData = textEncryptor.encrypt(inputData)
val decryptedData = textEncryptor.decrypt(encryptedData)
println("Input data: $inputData")
println("Encrypted data: $encryptedData")
println("Decrypted data: $decryptedData")String inputData = "hyperskill";
String encryptedData = textEncryptor.encrypt(inputData);
String decryptedData = textEncryptor.decrypt(encryptedData);
System.out.printf("Input data: %s%n", inputData);
System.out.printf("Encrypted data: %s%n", encryptedData);
System.out.printf("Decrypted data: %s%n", decryptedData);Instead of hardcoding the salt, you can use Spring’s built-in key generator to create a random one:
val salt = KeyGenerators.string().generateKey()String salt = KeyGenerators.string().generateKey();This will generate a hex-encoded string like: 861fddff2d08c730.
To hash passwords securely, use the PasswordEncoder interface. The simplest implementation is NoOpPasswordEncoder, which performs no hashing and should be used only for testing. For production, consider using these secure alternatives: BCryptPasswordEncoder, Pbkdf2PasswordEncoder, or SCryptPasswordEncoder.
Here’s an example with BCryptPasswordEncoder:
val strength = 7
val bCryptEncoder = BCryptPasswordEncoder(strength)
val rawPassword = "hackme"
val firstEncodedPassword = bCryptEncoder.encode(rawPassword)
val secondEncodedPassword = bCryptEncoder.encode(rawPassword)
println("First encoded password: $firstEncodedPassword")
println("Second encoded password: $secondEncodedPassword")int strength = 7;
PasswordEncoder bCryptEncoder = new BCryptPasswordEncoder(strength);
String rawPassword = "hackme";
String firstEncodedPassword = bCryptEncoder.encode(rawPassword);
String secondEncodedPassword = bCryptEncoder.encode(rawPassword);
System.out.printf("First encoded password: %s%n", firstEncodedPassword);
System.out.printf("Second encoded password: %s%n", secondEncodedPassword);Each hash will be different even for the same input, since BCrypt includes its own random salt internally. The strength parameter controls the computational cost — increasing it makes brute-force attacks harder, but also slows down processing.
Another option is DelegatingPasswordEncoder, which acts as a wrapper around multiple encoding algorithms. It stores the encoding algorithm along with the hash using a format like {id}encodedPassword. This allows you to change hashing algorithms in the future without breaking backward compatibility.
Accessing authenticated user information
After a user logs in, Spring Security creates an Authentication object that holds their details. You can use it to retrieve:
getName()— returns the username.getAuthorities()— returns user roles/authorities.getPrincipal()— returns the user object (typically aUserDetailsimplementation), which you may need to cast.
Spring Security lets you access the Authentication or Principal directly as controller method arguments:
@GetMapping("/username")
fun username(auth: Authentication) = println(auth.name)@GetMapping("/username")
public void username(Authentication auth) {
System.out.println(auth.getName());
}To get full user details:
@GetMapping("/details")
fun details(auth: Authentication) {
val user = auth.principal as UserDetails
println("Username: ${user.username}")
println("Authorities: ${user.authorities}")
}@GetMapping("/details")
public void details(Authentication auth) {
UserDetails user = (UserDetails) auth.getPrincipal();
System.out.printf("Username: %s%n", user.getUsername());
System.out.printf("Authorities: %s%n", user.getAuthorities());
}Using @AuthenticationPrincipal, you can inject the current UserDetails directly:
@GetMapping("/username")
fun username(@AuthenticationPrincipal user: UserDetails) = println(user.username)@GetMapping("/username")
public void username(@AuthenticationPrincipal UserDetails user) {
System.out.println(user.getUsername());
}This approach avoids casting and is more concise.
To access user details outside controller methods (e.g., in services), use SecurityContextHolder:
val auth = SecurityContextHolder.getContext().authenticationAuthentication auth = SecurityContextHolder.getContext().getAuthentication();This method is harder to test and should be used sparingly.
For example, consider a simple item tracker application, where users can add and retrieve items they have created. Each user's items are stored under their username. Only the items they created are returned.
@RestController
class ItemsController {
private val items = ConcurrentHashMap<String, MutableSet<String>>()
@PostMapping("/items")
fun addItem(@AuthenticationPrincipal user: UserDetails, @RequestParam item: String) {
items.computeIfAbsent(user.username) { HashSet() }.add(item)
}
@GetMapping("/items")
fun getItems(@AuthenticationPrincipal user: UserDetails): Set<String> =
items.getOrDefault(user.username, setOf())
}@RestController
public class ItemsController {
private final Map<String, Set<String>> items = new ConcurrentHashMap<>();
@PostMapping("/items")
public void addItem(@AuthenticationPrincipal UserDetails user, @RequestParam String item) {
items.computeIfAbsent(user.getUsername(), k -> new HashSet<>()).add(item);
}
@GetMapping("/items")
public Set<String> getItems(@AuthenticationPrincipal UserDetails user) {
return items.getOrDefault(user.getUsername(), Collections.emptySet());
}
}Final thoughts
In interviews, you'll be expected to explain how Spring handles web requests, how you manage input validation and errors, and how to implement secure authentication and authorization. Practicing secure REST API development in Spring Boot will help you demonstrate your readiness for real-world backend roles.
You don't need to memorize every annotation or class. It's better to focus on understanding how Spring Web and Spring Security work, and be able to explain the request lifecycle, security filters, and typical design patterns used in real-world projects.