Appearance
Spring Security Part 2: Creating and Storing Users
Authentication and authorization can only happen after a user exists. So before the login methods, user creation.
The default user
Add only the security starter and start the application:
Using generated security password: dcfcaf08-8149-4e4f-86a6-e1dcb0f2d556At startup a user is created with the username user and a random password, new on every restart. It comes from SecurityProperties:
java
public static class User {
private String name = "user"; // default username
private String password = UUID.randomUUID().toString(); // random password
private List<String> roles = new ArrayList<>();
private boolean passwordGenerated = true;
// getters and setters
}and an auto-configuration (UserDetailsServiceAutoConfiguration) puts it into an InMemoryUserDetailsManager, which keeps users in a map in memory:
java
@Bean
public InMemoryUserDetailsManager inMemoryUserDetailsManager(SecurityProperties properties,
ObjectProvider<PasswordEncoder> passwordEncoder) {
SecurityProperties.User user = properties.getUser();
List<String> roles = user.getRoles();
return new InMemoryUserDetailsManager(
User.withUsername(user.getName())
.password(getOrDeducePassword(user, passwordEncoder.getIfAvailable()))
.roles(StringUtils.toStringArray(roles))
.build());
}Way 1: application.properties (development and testing only)
properties
spring.security.user.name=my_username
spring.security.user.password=my_password
spring.security.user.roles=ADMINThese are bound onto SecurityProperties.User through its setters, replacing the defaults. No random password is generated any more.
Way 2: your own InMemoryUserDetailsManager (development and testing only)
For several users, define the UserDetailsService bean yourself:
java
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public UserDetailsService userDetailsService() {
UserDetails user1 = User.withUsername("my_username_1")
.password("{noop}my_password_1") // {noop}: stored as plain text
.roles("ADMIN")
.build();
UserDetails user2 = User.withUsername("my_username_2")
.password("{noop}1234")
.roles("USER")
.build();
return new InMemoryUserDetailsManager(user1, user2);
}
}Once you define a UserDetailsService, Spring Boot no longer creates the default user.
Why {noop}?
Spring stores passwords in the format {id}encodedPassword. The {id} names the algorithm: {noop} (no encoding: plain text), {bcrypt}, {sha256}, and so on.
At login:
- The stored password is loaded.
- The
{id}is read off the front. {noop}: the rest is compared directly with the password the user typed.{bcrypt}: the typed password is checked with the BCrypt encoder against the stored hash.
Hashing is one-way: a hash cannot be turned back into the password. So checking a password means hashing what the user typed and comparing hashes.
To store a BCrypt hash:
java
UserDetails user1 = User.withUsername("my_username_1")
.password("{bcrypt}" + new BCryptPasswordEncoder().encode("my_password_1"))
.roles("ADMIN")
.build();Logging in with my_password_1 works. When no encoder is configured, Spring uses DelegatingPasswordEncoder: it looks at the {id} and delegates to the matching encoder.
Choosing one encoder
To avoid the {id} prefix, declare a single PasswordEncoder bean. All passwords then go straight to it, and none needs a prefix:
java
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public UserDetailsService userDetailsService() {
UserDetails user1 = User.withUsername("my_username_1")
.password(new BCryptPasswordEncoder().encode("my_password_1"))
.roles("ADMIN")
.build();
return new InMemoryUserDetailsManager(user1);
}
}The flip side: every stored password must now be a BCrypt hash. A {noop}1234 password fails to log in, with "Encoded password does not look like BCrypt" in the log.
Way 3: users in a database (for production)
In production, users are created at runtime (a sign-up API) and stored in a database, with hashed passwords.
The entity implements Spring Security's UserDetails. Every authentication method loads the user as a UserDetails; implementing it means no mapping from your entity is needed.
java
@Entity
@Table(name = "user_auth")
public class UserAuthEntity implements UserDetails {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false)
private String username;
@Column(nullable = false)
private String password;
private String role;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of(new SimpleGrantedAuthority(role));
}
@Override
public String getPassword() { return password; }
@Override
public String getUsername() { return username; }
// isAccountNonExpired, isAccountNonLocked, isCredentialsNonExpired, isEnabled:
// true by default in Spring Security 6; override to add such rules
// setters, getRole()
}The repository:
java
@Repository
public interface UserAuthEntityRepository extends JpaRepository<UserAuthEntity, Long> {
Optional<UserAuthEntity> findByUsername(String username);
}The service implements UserDetailsService. During authentication Spring Security loads the user by name, but it does not know your table, so you implement loadUserByUsername:
java
@Service
public class UserAuthEntityService implements UserDetailsService {
@Autowired
private UserAuthEntityRepository userAuthEntityRepository;
public UserDetails save(UserAuthEntity userAuth) {
return userAuthEntityRepository.save(userAuth);
}
@Override
public UserAuthEntity loadUserByUsername(String username) throws UsernameNotFoundException {
return userAuthEntityRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
}
}The register API hashes the password before saving:
java
@RestController
@RequestMapping("/auth")
public class UserAuthController {
@Autowired
private UserAuthEntityService userAuthEntityService;
@Autowired
private PasswordEncoder passwordEncoder;
@PostMapping("/register")
public ResponseEntity<String> register(@RequestBody UserAuthEntity userAuthDetails) {
userAuthDetails.setPassword(passwordEncoder.encode(userAuthDetails.getPassword()));
userAuthEntityService.save(userAuthDetails);
return ResponseEntity.ok("User registered successfully!");
}
}The security configuration. By default every endpoint requires authentication, including /auth/register, which is how a user gets credentials in the first place. So registration must be open to everyone, which is standard practice:
java
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/auth/register").permitAll() // no authentication
.anyRequest().authenticated() // everything else
)
.csrf(csrf -> csrf.disable()) // explained in the next parts
.httpBasic(Customizer.withDefaults());
return http.build();
}
}json
POST /auth/register
{"username": "U1", "password": "123", "role": "ROLE_USER"}U1 / 123 can now log in. The table holds a BCrypt hash such as $2a$10$ZJe1He102I7h12jaPoFLke9Z0rxAkyVNpv2c.qWDwBW/BacxMlKdi: $2a is the BCrypt version, 10 the cost factor. BCrypt adds a random salt to each hash, so two users with the same password get different hashes.
Store roles with the ROLE_ prefix. hasRole("USER") (next part) checks for the authority ROLE_USER. A user registered with "role": "USER" logs in, but gets 403 on endpoints that require the USER role. (User.withUsername(...).roles("USER") adds the prefix for you; a raw SimpleGrantedAuthority does not.)
Next: With users stored, part 3 adds the first login method: form login, Spring Security's default, which keeps the user logged in with a session.
Check yourself
- With only the security starter, what user exists and what is its password?
- How do you set the username, password and roles in
application.properties? Is that fine for production? - What happens to the default user when you define your own
UserDetailsService? - What does the
{id}in{bcrypt}$2a$...mean, and which encoder reads it? - How is a login password checked against a BCrypt hash?
- After declaring a
BCryptPasswordEncoderbean, a user stored as{noop}1234cannot log in. Why? - Why does the entity implement
UserDetails, and why does the service implementUserDetailsService? - Why must
/auth/registerbepermitAll()? - Why do two users with the same password have different hashes?
- A user is stored with role
USERand gets 403 on an endpoint requiringhasRole("USER"). Why?
Answers
user, with a random password printed at startup (new each restart).spring.security.user.name,.password,.roles. No, only for development and testing.- It is no longer created.
- The algorithm the password was encoded with;
DelegatingPasswordEncoder(the default) delegates to that encoder. - The typed password is hashed with BCrypt (using the salt from the stored hash) and compared; hashing is one-way.
- That encoder is now used for every password and expects a BCrypt hash ("Encoded password does not look like BCrypt").
- Spring Security works with
UserDetailsobjects, so no mapping is needed; and it needsloadUserByUsernameto fetch users from your table. - Every endpoint needs authentication by default, and a new user has no credentials yet.
- BCrypt adds a random salt to each hash.
hasRole("USER")checks for the authorityROLE_USER; store the role asROLE_USER.