🚀 Part 3: User Registration & Default Roles in Spring Security

Welcome back to our “Building Secure Java Web Apps with Spring Security” series! In Part 2, we explored why char[] is preferred over String for handling passwords in memory — emphasizing immutability risks and memory exposure. Today, we dive into the user registration workflow, where new users are automatically assigned the ROLE_SUBSCRIBER and gain access to core features while maintaining a secure, production-ready foundation.

This step is critical: it’s the first interaction users have with your system, and doing it right ensures security, scalability, and a smooth UX — all while aligning with real-world deployment standards.

🔗 GitHub Repo: https://github.com/manueltechlabs/java-blog-project/tree/main

🧩 1. Registering a New User: The Full Flow

Our registration process includes:

  • Public /register endpoint
  • Email uniqueness check
  • Default role assignment (ROLE_SUBSCRIBER)
  • Secure password hashing
  • Profile picture upload
  • Automatic timestamps

Let’s walk through the full implementation.

🔧 2. Security Configuration: Permitting Public Access

In SecurityConfig, we allow unauthenticated access to the registration page and static assets:

java
123456789101112131415
      @Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
        .authorizeHttpRequests(auth -> auth
            .requestMatchers("/css/**", "/js/**", "/img/**", "/register").permitAll()
            .anyRequest().authenticated()
        )
        .formLogin(form -> form
            .loginPage("/login")
            .permitAll()
        )
        .csrf(csrf -> csrf.ignoringRequestMatchers("/register")); // Or enable CSRF safely

    return http.build();
}
    

✅ Best Practice: Always explicitly permit /register. CSRF can remain enabled — just include the token in your Thymeleaf form (more below).

🖥️ 3. Registration Form (Thymeleaf + HTML)

register.html uses Thymeleaf for model binding and validation:

html
12345678910111213141516171819202122232425262728
      <form th:action="@{/register}" method="post" enctype="multipart/form-data">
    <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />

    <div class="form-group">
        <label>First Name</label>
        <input type="text" class="form-control" th:field="*{account.firstName}" required />
        <p class="alert alert-danger" th:if="${#fields.hasErrors('account.firstName')}" th:errors="*{account.firstName}">Error</p>
    </div>

    <div class="form-group">
        <label>Email</label>
        <input type="email" class="form-control" th:field="*{account.email}" required />
        <p class="alert alert-danger" th:if="${#fields.hasErrors('account.email')}" th:errors="*{account.email}">Error</p>
    </div>

    <div class="form-group">
        <label>Password</label>
        <input type="password" class="form-control" th:field="*{password.word}" required />
        <p class="alert alert-danger" th:if="${#fields.hasErrors('password.word')}" th:errors="*{password.word}">Error</p>
    </div>

    <div class="form-group">
        <label>Profile Picture</label>
        <input type="file" name="file" class="form-control" />
    </div>

    <button type="submit" class="btn btn-primary">Register</button>
</form>
    

🔐 Security Note: Always include CSRF token:

html
1
      <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
    

⚙️ 4. Controller: Handling Registration Logic

AccountController.java manages the registration flow with validation and file handling:

java
123456789101112131415161718192021222324252627282930313233
      @PostMapping("/register")
public String registerPost(
    @RequestParam("file") MultipartFile file,
    @Valid @ModelAttribute Account account,
    BindingResult resultAccount,
    @Valid @ModelAttribute Password password,
    BindingResult resultPassword
) {
    // Prevent duplicate emails
    if (accountService.findOneByEmail(account.getEmail()).isPresent()) {
        resultAccount.rejectValue("email", "error.email", "Email already in use");
    }

    if (resultAccount.hasErrors() || resultPassword.hasErrors()) {
        return "accountView/register";
    }

    // Handle profile picture
    if (!file.isEmpty()) {
        String filename = FileStorage.save(file, "userImg");
        account.setProfilePicture("/img/userImg/" + filename);
    }

    // Save password and assign to account
    passwordService.save(password);
    account.setPassword(password);

    // Auto-assign ROLE_SUBSCRIBER
    account.setRoles(new HashSet<>(Arrays.asList(roleService.findByName("ROLE_SUBSCRIBER"))));

    accountService.save(account);
    return "redirect:/login?registered";
}
    

✅ Key Feature: New users get ROLE_SUBSCRIBER by default — perfect for comment access without admin privileges.

🛡️ 5. Secure File Upload & Validation

FileValidator class:

FeatureImplementation
File Type ValidationOnly allows image/jpeg, image/png
Size LimitEnforces 500KB max (MAX_FILE_SIZE = 500*1024)
Filename SanitizationStrips .., /, & to block path traversal
Directory Escape PreventionUses resolvePath() with canonical path check

🔍 Key Code: FileValidator.validateFile()

java
12345678910111213141516
      public static String validateFile(MultipartFile file) {
    if (file == null || file.isEmpty()) {
        return "File is empty!";
    }

    String contentType = file.getContentType();
    if (!ALLOWED_FILE_TYPES.contains(contentType)) {
        return "File type not valid!";
    }

    if (file.getSize() > MAX_FILE_SIZE) {
        return "File size must be less than 500KB!";
    }

    return "OK";
}
    

💾 6. Service Layer: Auto-Assign Role & Timestamps

In AccountServiceImpl.save(), we ensure defaults are applied:

AccountServiceImpl.java

java
1234567891011121314151617
      @Override
public Account save(Account account) {
    if (account.getRegisteredAt() == null) {
        account.setRegisteredAt(LocalDateTime.now());
    }

    if (account.getProfilePicture() == null) {
        account.setProfilePicture("/img/userImg/default.svg");
    }

    if (account.getRoles() == null || account.getRoles().isEmpty()) {
        Role subscriber = roleService.findByName("ROLE_SUBSCRIBER");
        account.setRoles(new HashSet<>(Arrays.asList(subscriber)));
    }

    return accountRepository.save(account);
}
    

This ensures every user starts with:

  • Registration timestamp
  • Default avatar
  • ROLE_SUBSCRIBER role

🔗 GitHub Repo: https://github.com/manueltechlabs/java-blog-project/tree/main

📚 What’s Next?

In Part 4: Password Recovery via Email, we’ll implement a secure password reset flow using:

  • Token generation with expiry
  • JavaMailSender integration
  • Thymeleaf email templates
  • Secure reset endpoint validation

We’ll also cover how to prevent token reuse and lockout mechanisms — essential for any production system.

💡 Final Thought:

User registration isn’t just a form — it’s the foundation of your security model. By combining Spring Security, proper defaults, and server-side enforcement, you create a system that’s both user-friendly and resilient.

Keep coding securely — and see you in Part 4! 🔐