Write and Publish a Tutorial!
Do you have good notes or papers written by you and seeking for a
platform to publish? We provide the platform to publish your tutorials
in your name. If you wish to publish your tutorial in your name to
help the readers, Please contact us by sending an email to
publish@tools4testing.com or publish@java4coding.com The main way that
others learn about your work is through your published tutorials. If
you don’t publish, it will be as if you never did the work. Your notes
can help the readers only when you share it.
Password Encoding in Spring Security
In real-world projects, storing passwords as plain text is a big security risk because they can be easily stolen if someone gains unauthorized access to the database. To mitigate this risk, passwords are usually transformed in a way that makes it difficult for attackers to read or steal them.
In Spring Security, the PasswordEncoder contract is used to manage password encryption and validation. It's essentially a blueprint that defines how to handle passwords securely. Here's a breakdown of the PasswordEncoder contract:
public interface PasswordEncoder {
String encode(CharSequence rawPassword);
boolean matches(CharSequence rawPassword, String encodedPassword);
default boolean upgradeEncoding(String encodedPassword) {
return false;
}
}
encode(CharSequence rawPassword): This method takes a raw password (in plain text) and transforms it into a secure format, typically through encryption or hashing. This ensures that the password is not stored as plain text in the database, making it more secure against unauthorized access.
matches(CharSequence rawPassword, String encodedPassword): This method is used to check if a provided raw password matches the encoded password stored in the database. During the authentication process, the user-supplied password is compared against the stored encoded password using this method. If they match, the user is authenticated.
upgradeEncoding(CharSequence encodedPassword): This method, by default, returns false in the contract. It's used to improve password security by re-encoding an already encoded password. If you override this method to return true, it indicates that the encoded password should be re-encoded for better security. This can be useful if your application's security requirements change over time.
In summary, the PasswordEncoder contract provides a standardized way to handle password encryption and validation in Spring Security. By implementing this contract, you ensure that passwords are securely managed within your application, reducing the risk of unauthorized access and data breaches.
In the below implementation we are just returning same password without encoding from encode method. This implementation is same as the NoOpPasswordEncoder which we used in previous chapters.
public class PlainTextPasswordEncoder implements PasswordEncoder {
@Override
public String encode(CharSequence rawPassword) {
return rawPassword.toString();
}
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return rawPassword.equals(encodedPassword);
}
}
Password Encoding Example in Spring Security
In the example below, we utilize AES encryption to encrypt passwords. The AES encryption algorithm retrieves the encryption secret key from the application.properties file. In real-world projects, it is common practice to securely manage secrets. During the initialization of the Spring Boot application, we insert same records into the database, wherein passwords are encrypted using the same secret key.
pom.xml<?xml version="1.0" encoding="UTF-8"?> <project xmlns="https://maven.apache.org/POM/4.0.0" xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="https://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.6.0</version> <relativePath/> <!-- lookup parent from repository, not local --> </parent> <groupId>com.java4coding</groupId> <artifactId> PasswordEncodingInSpringSecurity</artifactId> <version>0.0.1-SNAPSHOT</version> <name> PasswordEncodingInSpringSecurity</name> <description>Managing Users in Spring Security</description> <properties> <java.version>11</java.version> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <spring-boot.version>2.6.0</spring-boot.version> <lombok.version>1.18.28</lombok.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>${lombok.version}</version> <scope>provided</scope> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>${spring-boot.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>
<build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <version>${spring-boot.version}</version> <executions> <execution> <id>build-info</id> <goals> <goal>build-info</goal> <goal>repackage</goal> </goals> </execution> </executions> </plugin> </plugins> </build>
</project> |
SpringBootDemo.javapackage com.java4coding;
import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.domain.EntityScan; import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
@SpringBootApplication @EntityScan @EnableJpaRepositories public class SpringBootDemo { public static void main(String[] args) { SpringApplication.run(SpringBootDemo.class, args); } } |
ApplicationConfig.javapackage com.java4coding.config;
import com.java4coding.repository.UserRepository; import com.java4coding.security.AESEncryptor; import com.java4coding.security.PasswordEncoderImpl; import com.java4coding.security.UserDetailsServiceImpl; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration public class ApplicationConfig {
@Bean public UserDetailsService userDetailsService(UserRepository userRepository) { return new UserDetailsServiceImpl(userRepository); }
@Bean public PasswordEncoder passwordEncoder(AESEncryptor aesEncryptor) { PasswordEncoderImpl passwordEncoder = new PasswordEncoderImpl(); passwordEncoder.setAesEncryptor(aesEncryptor); return passwordEncoder; } } |
ApplicationWebSecurityConfigurerAdapter.javapackage com.java4coding.config;
import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.WebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@Configuration public class ApplicationWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {
/* Since H2 has its own authentication provider, you can skip the Spring Security for the path of h2 console entirely in the same way that you do for your static content. In order to do that, in your Spring security config, you have to override the configuration method which takes an instance of org.springframework.security.config.annotation.web.builders.WebSecurity as a parameter instead of the one which takes an instance of org.springframework.security.config.annotation.web.builders.HttpSecurity */ @Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers("/h2-console/**"); }
/* Spring Security enables Cross-Site Request Forgery (CSRF) protection by default. CSRF is an attack that tricks the victim into submitting a malicious request and uses the identity of the victim to perform an undesired function on their behalf. If the CSRF token, which is used to protect against this type of attack, is missing or incorrect, the server may also respond with error 403. */ @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .anyRequest() .authenticated() .and() .httpBasic(); http.csrf().disable(); }
} |
UserController.javapackage com.java4coding.controller;
import com.java4coding.dto.ResponseDto; import com.java4coding.dto.UserDto; import com.java4coding.entity.User; import com.java4coding.repository.UserRepository; import com.java4coding.security.AESEncryptor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController public class UserController {
@Autowired private UserRepository userRepository; @Autowired private AESEncryptor aesEncryptor;
@GetMapping(value = "/testAccess") public String testAccess() { return "Hurray! You are Authorized."; }
@PostMapping(value = "/addUser") public ResponseEntity<ResponseDto> addUser(@RequestBody UserDto userDto) { User user = new User(); user.setPassword(aesEncryptor.encrypt(userDto.getPassword())); user.setUsername(userDto.getUsername()); user.setAuthority(userDto.getAuthority()); userRepository.save(user); return ResponseEntity.ok(ResponseDto.builder().result("Success").code("200").build()); }
@PostMapping(value = "/updateUser") public ResponseEntity<ResponseDto> updateUser(@RequestBody UserDto userDto) { List<User> users = userRepository.findByUsername(userDto.getUsername()); if (users == null || users.isEmpty()) { return ResponseEntity.ok(ResponseDto.builder().result("No User Exists").code("400").build()); } User existingUser = users.get(0); existingUser.setPassword(aesEncryptor.encrypt(userDto.getUsername())); existingUser.setUsername(userDto.getNewUsername()); existingUser.setAuthority(userDto.getAuthority()); userRepository.save(existingUser); return ResponseEntity.ok(ResponseDto.builder().result("Success").code("200").build()); }
@GetMapping(value = "/deleteUser/{userName}") public ResponseEntity<ResponseDto> deleteUser(@PathVariable("userName") String userName) { List<User> users = userRepository.findByUsername(userName); if (users == null || users.isEmpty()) { return ResponseEntity.ok(ResponseDto.builder().result("No User Exists").code("400").build()); } User existingUser = users.get(0); userRepository.delete(existingUser); return ResponseEntity.ok(ResponseDto.builder().result("Success").code("200").build()); } } |
ResponseDto.javapackage com.java4coding.dto;
import lombok.Builder; import lombok.Getter; import lombok.Setter;
@Setter @Getter @Builder public class ResponseDto { private String result; private String code; } |
UserDto.javapackage com.java4coding.dto;
import lombok.Getter; import lombok.Setter;
@Setter @Getter public class UserDto { private String username; private String newUsername; private String password; private String authority; } |
User.javapackage com.java4coding.entity;
import lombok.Getter; import lombok.Setter;
import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id;
@Entity @Setter @Getter public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String username; private String password; private String authority; } |
UserRepository.javapackage com.java4coding.repository;
import com.java4coding.entity.User; import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface UserRepository extends JpaRepository<User, Long> { List<User> findByUsername(String userName); } |
AESEncryptor.javapackage com.java4coding.security;
import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component;
import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Arrays; import java.util.Base64;
import javax.crypto.Cipher; import javax.crypto.spec.SecretKeySpec;
@Component public class AESEncryptor { private static SecretKeySpec secretKey; private static byte[] key; private static final String ALGORITHM = "AES"; @Value("${spring.security.secret}") private String secret;
public void prepareSecreteKey(String myKey) { MessageDigest sha = null; try { key = myKey.getBytes(StandardCharsets.UTF_8); sha = MessageDigest.getInstance("SHA-1"); key = sha.digest(key); key = Arrays.copyOf(key, 16); secretKey = new SecretKeySpec(key, ALGORITHM); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } }
public String encrypt(String strToEncrypt) { try { prepareSecreteKey(secret); Cipher cipher = Cipher.getInstance(ALGORITHM); cipher.init(Cipher.ENCRYPT_MODE, secretKey); return Base64.getEncoder().encodeToString(cipher.doFinal(strToEncrypt.getBytes("UTF-8"))); } catch (Exception e) { System.out.println("Error while encrypting: " + e.toString()); } return null; }
/* public String decrypt(String strToDecrypt, String secret) { try { prepareSecreteKey(secret); Cipher cipher = Cipher.getInstance(ALGORITHM); cipher.init(Cipher.DECRYPT_MODE, secretKey); return new String(cipher.doFinal(Base64.getDecoder().decode(strToDecrypt))); } catch (Exception e) { System.out.println("Error while decrypting: " + e.toString()); } return null; }
*/ } |
PasswordEncoderImpl.javapackage com.java4coding.security;
import lombok.Getter; import lombok.Setter; import org.springframework.security.crypto.password.PasswordEncoder;
@Setter @Getter public class PasswordEncoderImpl implements PasswordEncoder {
private AESEncryptor aesEncryptor;
@Override public String encode(CharSequence rawPassword) { return aesEncryptor.encrypt(rawPassword.toString()); }
@Override public boolean matches(CharSequence rawPassword, String encodedPassword) { String hashedPassword = encode(rawPassword); return encodedPassword.equals(hashedPassword); } } |
SecurityUser.java package com.java4coding.security;
import com.java4coding.entity.User; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection; import java.util.List;
public class SecurityUser implements UserDetails { private final User user;
public SecurityUser(User user) { this.user = user; }
@Override public String getUsername() { return user.getUsername(); }
@Override public String getPassword() { return user.getPassword(); }
@Override public Collection<? extends GrantedAuthority> getAuthorities() { return List.of(() -> user.getAuthority()); }
@Override public boolean isAccountNonExpired() { return true; }
@Override public boolean isAccountNonLocked() { return true; }
@Override public boolean isCredentialsNonExpired() { return true; }
@Override public boolean isEnabled() { return true; } } |
UserDetailsServiceImpl.javapackage com.java4coding.security;
import com.java4coding.entity.User; import com.java4coding.repository.UserRepository; import lombok.AllArgsConstructor; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException;
import java.util.List;
@AllArgsConstructor public class UserDetailsServiceImpl implements UserDetailsService { private UserRepository userRepository;
@Override public SecurityUser loadUserByUsername(String username) throws UsernameNotFoundException { List<User> usersByUserName = userRepository.findByUsername(username); return new SecurityUser(usersByUserName.get(0)); } } |
application.propertiesspring.h2.console.enabled=true spring.h2.console.path=/h2-console/
spring.datasource.url=jdbc:h2:mem:testdb spring.datasource.driverClassName=org.h2.Driver spring.datasource.username=sa spring.datasource.password=pass spring.jpa.database-platform=org.hibernate.dialect.H2Dialect #spring.jpa.hibernate.ddl-auto=create-drop spring.jpa.hibernate.ddl-auto=none
spring.jpa.properties.hibernate.show_sql=true spring.jpa.properties.hibernate.use_sql_comments=true spring.jpa.properties.hibernate.format_sql=true |
schema.sqlcreate table user ( id bigint generated by default as identity, authority varchar(255), password varchar(255), username varchar(255), primary key (id) ); |
data.sqlINSERT INTO User (id, username, password, authority) VALUES (1, 'manu', '3BayxFSV4Pu9HNSwNrSe5w==', 'read'); INSERT INTO User (id, username, password, authority) VALUES (2, 'advith', 'Nqu2KPw66lw57/7TzwadEg==', 'read'); INSERT INTO User (id, username, password, authority) VALUES (3, 'aashvith', 'Nqu2KPw66lw57/7TzwadEg==', 'read'); |
Project Structure
Output
Choosing the Right PasswordEncoder Implementation in Spring Security
When selecting a PasswordEncoder implementation in Spring Security, it's important to choose the most suitable one for your application. Here's a brief overview of the provided implementations:
NoOpPasswordEncoder: This implementation doesn't actually encode the password; it stores it in plain text. It's only intended for examples and should never be used in real-world scenarios due to its inherent security risks.
StandardPasswordEncoder: This implementation uses SHA-256 to hash passwords. However, it's deprecated because SHA-256 is considered less secure compared to newer hashing algorithms.
Pbkdf2PasswordEncoder: This implementation uses the Password-Based Key Derivation Function 2 (PBKDF2), which is a strong cryptographic function designed for securely hashing passwords.
BCryptPasswordEncoder: This implementation uses the bcrypt algorithm, which is a widely recommended and secure hashing function for password storage. It's suitable for most applications and provides a good balance between security and performance.
SCryptPasswordEncoder: This implementation uses the scrypt hashing function, which is designed to be memory-intensive and resistant to brute-force attacks. It offers strong security guarantees but may be slower in terms of computation compared to other algorithms.
When choosing a PasswordEncoder implementation, it's generally recommended to use one of the secure options like PBKDF2PasswordEncoder, BCryptPasswordEncoder, or SCryptPasswordEncoder, depending on your specific security requirements and performance considerations. Avoid using deprecated or insecure implementations like StandardPasswordEncoder or NoOpPasswordEncoder in production environments.