Full Stack • Java • System Design • Cloud • AI Engineering

Secure Password Hashing with BCrypt

Learn how to securely store passwords in Java and Spring Boot using BCrypt. Understand hashing, salting, PasswordEncoder, registration flow, login verification, database storage, and production best practices.

Introduction

Password security is one of the most important parts of application security.

A common beginner mistake is storing passwords directly in the database.

That is dangerous.

If the database is leaked, every user's password is exposed.

In real applications, passwords must never be stored as plain text.

Instead, passwords should be converted into secure hashes using algorithms like BCrypt.

In this article, we will learn:

  • Why plain text passwords are dangerous
  • Difference between hashing and encryption
  • What BCrypt is
  • Why BCrypt is widely used
  • How Spring Security PasswordEncoder works
  • How to hash passwords during registration
  • How to verify passwords during login
  • How to store hashed passwords in a database
  • Production best practices

1. Plain Text Password Problem

Bad design:

Username: venu
Password: password123

If this value is stored directly in the database, anyone with database access can see the password.

flowchart LR

User["User enters password"]
--> App["Java Application"]
--> DB["Database stores plain password"]

DB --> Risk["Password exposed if DB leaks"]

Why This Is Dangerous

If passwords are stored as plain text:

  • Database admins can see passwords
  • Attackers can steal all passwords
  • Users may reuse same password across websites
  • Compliance violations can happen
  • Company reputation is damaged

2. What is Password Hashing?

Hashing converts a password into a one-way fixed output.

Example:

password123

becomes:

$2a$10$WzLkB5aXAcq...

Hashing is one-way.

That means the original password cannot be directly recovered from the hash.

flowchart LR

Password["password123"]
--> HashFunction["BCrypt Hash Function"]
--> Hash["$2a$10$WzLkB5aX..."]

3. Hashing vs Encryption

Many developers confuse hashing and encryption.

Concept Hashing Encryption
Purpose Verify data Protect and recover data
Reversible? No Yes
Uses Key? Usually no Yes
Password Storage Yes No
Example BCrypt AES

Simple Explanation

Hashing:

Password → Hash

Encryption:

Plain Text → Encrypted Text → Decrypted Text

For passwords, use hashing.

Do not encrypt passwords.


4. Why Not Use MD5 or SHA-256?

Algorithms like MD5 and SHA-256 are fast.

Fast is good for normal hashing, but bad for password hashing.

Attackers can try billions of password guesses quickly.

flowchart TB

WeakHash["Fast Hash: MD5 / SHA-256"]
--> FastAttack["Attacker can brute force quickly"]

BCrypt["Slow Hash: BCrypt"]
--> SlowAttack["Brute force becomes expensive"]

For password storage, use slow adaptive algorithms:

  • BCrypt
  • Argon2
  • PBKDF2
  • SCrypt

In Spring Boot applications, BCrypt is commonly used.


5. What is BCrypt?

BCrypt is a password hashing algorithm designed for secure password storage.

BCrypt provides:

  • Salt generation
  • Configurable strength
  • Slow hashing
  • Protection against brute-force attacks
flowchart LR

Password["Password"]
--> Salt["Random Salt"]
--> BCrypt["BCrypt Algorithm"]
--> Hash["Stored BCrypt Hash"]

6. What is Salt?

A salt is a random value added to the password before hashing.

Without salt:

password123 → same hash every time

With salt:

password123 + salt1 → hash1
password123 + salt2 → hash2

Same password produces different hashes.

flowchart TB

Password["password123"]

Password --> Salt1["Salt A"] --> Hash1["Hash A"]

Password --> Salt2["Salt B"] --> Hash2["Hash B"]

This prevents attackers from using precomputed hash tables.


7. BCrypt Hash Structure

A BCrypt hash usually looks like this:

$2a$10$KbQiHKq7IGRz3yLMhkG4nepWe7T1LxGr/ktS9OEPvXEG6Q8QXGd9e

It contains:

$2a$     → BCrypt version
10       → Strength / cost factor
Salt     → Random salt
Hash     → Final password hash
flowchart LR

Hash["BCrypt Hash"]
--> Version["Version"]
--> Cost["Cost Factor"]
--> Salt["Salt"]
--> Value["Hash Value"]

8. Registration Flow

During registration:

  1. User sends username and password
  2. Application validates input
  3. Password is hashed using BCrypt
  4. Hashed password is stored in database
  5. Plain password is never stored
sequenceDiagram

participant User
participant API
participant PasswordEncoder
participant Database

User->>API: Register username/password
API->>API: Validate request
API->>PasswordEncoder: Encode raw password
PasswordEncoder-->>API: BCrypt hash
API->>Database: Save user with hashed password
Database-->>API: User saved
API-->>User: Registration success

9. Login Flow

During login:

  1. User enters username and password
  2. Application finds user by username
  3. Application compares raw password with stored hash
  4. If matched, login is successful
  5. If not matched, login fails
sequenceDiagram

participant User
participant API
participant Database
participant PasswordEncoder

User->>API: Login username/password
API->>Database: Find user by username
Database-->>API: User with hashed password
API->>PasswordEncoder: matches(rawPassword, storedHash)
PasswordEncoder-->>API: true or false
API-->>User: Login success/failure

Important:

The application does not decrypt the password.

It compares the raw password against the stored hash.


10. Spring Boot Project Setup

Required Dependencies

<dependencies>

    <!-- Spring Web for REST APIs -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- Spring Security for PasswordEncoder and security support -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>

    <!-- Spring Data JPA for database access -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>

    <!-- H2 Database for local testing -->
    <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
        <scope>runtime</scope>
    </dependency>

    <!-- Validation for request validation -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>

</dependencies>

11. Project Structure

secure-password-demo
└── src
    └── main
        └── java
            └── com.codewithvenu.security
                ├── controller
                │   └── AuthController.java
                ├── dto
                │   ├── RegisterRequest.java
                │   ├── LoginRequest.java
                │   └── AuthResponse.java
                ├── entity
                │   └── AppUser.java
                ├── repository
                │   └── AppUserRepository.java
                ├── service
                │   └── AuthService.java
                ├── config
                │   ├── PasswordConfig.java
                │   └── SecurityConfig.java
                └── SecurePasswordDemoApplication.java

12. application.yml

spring:
  datasource:
    url: jdbc:h2:mem:securitydb
    driver-class-name: org.h2.Driver
    username: sa
    password:

  h2:
    console:
      enabled: true

  jpa:
    hibernate:
      ddl-auto: update
    show-sql: true

Explanation

jdbc:h2:mem:securitydb

Creates an in-memory database.

ddl-auto: update

Automatically creates or updates database tables.

show-sql: true

Prints generated SQL queries in the console.


13. Create User Entity

package com.codewithvenu.security.entity;

import jakarta.persistence.*;

@Entity
@Table(name = "app_users")
public class AppUser {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, unique = true)
    private String username;

    @Column(nullable = false)
    private String password;

    @Column(nullable = false)
    private String role;

    protected AppUser() {
    }

    public AppUser(String username, String password, String role) {
        this.username = username;
        this.password = password;
        this.role = role;
    }

    public Long getId() {
        return id;
    }

    public String getUsername() {
        return username;
    }

    public String getPassword() {
        return password;
    }

    public String getRole() {
        return role;
    }
}

Code Explanation

@Entity

Marks this class as a JPA entity.

@Table(name = "app_users")

Maps the entity to the app_users table.

@Id

Marks the primary key.

@GeneratedValue

Automatically generates ID values.

@Column(nullable = false, unique = true)

Username is required and must be unique.

password

Stores the BCrypt hashed password, not the raw password.


14. Create Repository

package com.codewithvenu.security.repository;

import com.codewithvenu.security.entity.AppUser;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface AppUserRepository extends JpaRepository<AppUser, Long> {

    Optional<AppUser> findByUsername(String username);
}

Explanation

JpaRepository<AppUser, Long>

Provides CRUD operations for AppUser.

findByUsername

Used during login to find the user record.


15. Create DTOs

RegisterRequest

package com.codewithvenu.security.dto;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;

public record RegisterRequest(

        @NotBlank(message = "Username is required")
        String username,

        @NotBlank(message = "Password is required")
        @Size(min = 8, message = "Password must be at least 8 characters")
        String password

) {
}

LoginRequest

package com.codewithvenu.security.dto;

import jakarta.validation.constraints.NotBlank;

public record LoginRequest(

        @NotBlank(message = "Username is required")
        String username,

        @NotBlank(message = "Password is required")
        String password

) {
}

AuthResponse

package com.codewithvenu.security.dto;

public record AuthResponse(
        String message
) {
}

16. Create PasswordEncoder Bean

package com.codewithvenu.security.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
public class PasswordConfig {

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

Code Explanation

PasswordEncoder

Spring Security interface for password hashing and verification.

BCryptPasswordEncoder(12)

Uses BCrypt strength 12.

Higher strength means stronger but slower hashing.

Common values:

Strength Usage
10 Default learning/demo
12 Good production starting point
14+ Stronger but slower

17. Create Security Configuration

For this article, we allow register and login APIs publicly.

package com.codewithvenu.security.config;

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.web.SecurityFilterChain;

@Configuration
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http)
            throws Exception {

        http
            .csrf(csrf -> csrf.disable())
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/api/auth/register", "/api/auth/login").permitAll()
                .requestMatchers("/h2-console/**").permitAll()
                .anyRequest().authenticated()
            );

        http.headers(headers -> headers.frameOptions(frame -> frame.disable()));

        return http.build();
    }
}

Explanation

csrf.disable()

Disabled for this REST API demo.

permitAll()

Allows registration and login without authentication.

h2-console

Allowed for local learning.

frameOptions.disable()

Required for H2 console iframe.

Do not expose H2 console in production.


18. Create AuthService

package com.codewithvenu.security.service;

import com.codewithvenu.security.dto.AuthResponse;
import com.codewithvenu.security.dto.LoginRequest;
import com.codewithvenu.security.dto.RegisterRequest;
import com.codewithvenu.security.entity.AppUser;
import com.codewithvenu.security.repository.AppUserRepository;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

@Service
public class AuthService {

    private final AppUserRepository repository;
    private final PasswordEncoder passwordEncoder;

    public AuthService(AppUserRepository repository,
                       PasswordEncoder passwordEncoder) {
        this.repository = repository;
        this.passwordEncoder = passwordEncoder;
    }

    public AuthResponse register(RegisterRequest request) {

        boolean userExists = repository
                .findByUsername(request.username())
                .isPresent();

        if (userExists) {
            return new AuthResponse("Username already exists");
        }

        String hashedPassword = passwordEncoder.encode(request.password());

        AppUser user = new AppUser(
                request.username(),
                hashedPassword,
                "USER"
        );

        repository.save(user);

        return new AuthResponse("User registered successfully");
    }

    public AuthResponse login(LoginRequest request) {

        AppUser user = repository
                .findByUsername(request.username())
                .orElseThrow(() -> new RuntimeException("Invalid username or password"));

        boolean passwordMatches = passwordEncoder.matches(
                request.password(),
                user.getPassword()
        );

        if (!passwordMatches) {
            throw new RuntimeException("Invalid username or password");
        }

        return new AuthResponse("Login successful");
    }
}

Code Explanation

repository.findByUsername(request.username())

Checks whether the username already exists.

passwordEncoder.encode(request.password())

Hashes the raw password using BCrypt.

repository.save(user)

Saves the user with hashed password.

passwordEncoder.matches(rawPassword, storedHash)

Compares raw login password with stored BCrypt hash.


19. Create AuthController

package com.codewithvenu.security.controller;

import com.codewithvenu.security.dto.AuthResponse;
import com.codewithvenu.security.dto.LoginRequest;
import com.codewithvenu.security.dto.RegisterRequest;
import com.codewithvenu.security.service.AuthService;
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/auth")
public class AuthController {

    private final AuthService service;

    public AuthController(AuthService service) {
        this.service = service;
    }

    @PostMapping("/register")
    public AuthResponse register(@Valid @RequestBody RegisterRequest request) {
        return service.register(request);
    }

    @PostMapping("/login")
    public AuthResponse login(@Valid @RequestBody LoginRequest request) {
        return service.login(request);
    }
}

Explanation

@RestController

Creates REST APIs.

@RequestMapping("/api/auth")

Base path for authentication APIs.

@PostMapping("/register")

Registration API.

@PostMapping("/login")

Login API.

@Valid

Validates request body.


20. Test Registration API

curl -X POST http://localhost:8080/api/auth/register \
-H "Content-Type: application/json" \
-d '{
  "username": "venu",
  "password": "password123"
}'

Expected response:

{
  "message": "User registered successfully"
}

21. Check Stored Password in H2

Open:

http://localhost:8080/h2-console

Run:

select * from app_users;

You should see password like:

$2a$12$hY43nLmkVqA7bk...

Not:

password123
flowchart LR

Raw["password123"]
--> BCrypt["BCrypt"]
--> Stored["$2a$12$..."]

22. Test Login API

curl -X POST http://localhost:8080/api/auth/login \
-H "Content-Type: application/json" \
-d '{
  "username": "venu",
  "password": "password123"
}'

Expected response:

{
  "message": "Login successful"
}

Wrong password:

curl -X POST http://localhost:8080/api/auth/login \
-H "Content-Type: application/json" \
-d '{
  "username": "venu",
  "password": "wrongpassword"
}'

Expected result:

Invalid username or password

23. Why BCrypt Produces Different Hashes Every Time

Run this:

PasswordEncoder encoder = new BCryptPasswordEncoder();

System.out.println(encoder.encode("password123"));
System.out.println(encoder.encode("password123"));
System.out.println(encoder.encode("password123"));

Output will be different each time:

$2a$10$abc...
$2a$10$xyz...
$2a$10$pqr...

Because BCrypt generates a new salt for each hash.

But all hashes still match the same raw password.

encoder.matches("password123", storedHash);

24. Password Verification Flow

flowchart TB

Login["User Login"]
--> RawPassword["Raw Password"]

RawPassword --> StoredHash["Stored BCrypt Hash"]

StoredHash --> BCryptCheck["BCrypt matches()"]

BCryptCheck --> Result{"Match?"}

Result -- Yes --> Success["Login Success"]

Result -- No --> Failure["Login Failed"]

25. Production Best Practices

Do

  • Use BCrypt, Argon2, PBKDF2, or SCrypt
  • Use PasswordEncoder
  • Use minimum password length
  • Return generic login error
  • Use HTTPS
  • Rate limit login attempts
  • Monitor failed login attempts
  • Store only password hashes
  • Rotate compromised passwords

Do Not

  • Do not store plain passwords
  • Do not encrypt passwords
  • Do not log passwords
  • Do not expose password hash in API responses
  • Do not commit secrets to GitHub
  • Do not use {noop} in production

26. Common Mistakes

Mistake Problem
Storing plain password Complete password exposure
Using MD5/SHA-1 Too fast and weak
Logging password Sensitive data leak
Returning "username not found" User enumeration risk
Using same password hash manually Missing salt benefits
Disabling HTTPS Credentials can be intercepted

27. Better Error Handling

Bad:

Username not found

Bad:

Password incorrect

Good:

Invalid username or password

Why?

Because attackers should not know whether username exists.


28. Interview Questions

Q1. Why should passwords be hashed?

Because passwords should not be readable even if the database is compromised.

Q2. Is hashing reversible?

No. Hashing is one-way.

Q3. Should passwords be encrypted?

No. Passwords should be hashed, not encrypted.

Q4. What is salt?

A salt is a random value added before hashing to produce unique hashes.

Q5. Why does BCrypt produce different hashes for the same password?

Because BCrypt generates a new salt each time.

Q6. What does matches() do?

It compares a raw password with a stored hash.

Q7. What is BCrypt strength?

Strength controls hashing cost. Higher strength means slower and harder to brute force.

Q8. Why avoid MD5 or SHA-256 for passwords?

They are too fast and easier to brute force.


29. Key Takeaways

  • Never store plain text passwords.
  • Passwords should be hashed, not encrypted.
  • BCrypt is a strong password hashing algorithm.
  • BCrypt automatically uses salt.
  • Spring Security provides PasswordEncoder.
  • Use encode() during registration.
  • Use matches() during login.
  • Store only hashed passwords in the database.
  • Never log passwords or password hashes.
  • Use HTTPS and rate limiting in production.

Next Article

➡️ Role-Based Access Control with Spring Security