Published on

Có Những Cách Nào Để Bảo Mật Các Thông Tin như PASSWORD, KEY, TOKEN, ID trong Ứng Dụng Spring?

Authors
  • avatar
    David Nguyen
Table of Contents

Đặt vấn đề:

=> Khi làm việc với các ứng dụng Spring nói chung và Spring Boot nói riêng mình thấy nhiều bạn vẫn hay để thông tin cấu hình "trần trụi" như hai files bên dưới.

spring:
  datasource:
    url: jdbc:postgresql://localhost:5432/app_prod_db
    username: app_admin
    password: s3cr3tP@ssw0rd!
    driver-class-name: org.postgresql.Driver
  jpa:
    hibernate:
      ddl-auto: update
    database-platform: org.hibernate.dialect.PostgreSQLDialect
    show-sql: true
  sql:
    init:
      mode: always

jwt:
  secret: 1c7f2a0f-0b4e-4f91-8c23-d18f6d94a9fd
  expiration: 7200000 # 2 hours
  issuer: my-secure-app

minio:
  url: http://localhost:9000
  access-key: AKIAIOSFODNN7EXAMPLE
  secret-key: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
  bucket: uploads

Hoặc là:

spring.datasource.url=jdbc:postgresql://localhost:5432/app_prod_db
spring.datasource.username=app_admin
spring.datasource.password=s3cr3tP@ssw0rd!
spring.datasource.driver-class-name=org.postgresql.Driver

spring.jpa.hibernate.ddl-auto=update
spring.jpa.database-platform=org.hibernate.dialect.PostgreSQLDialect
spring.jpa.show-sql=true
spring.sql.init.mode=always

jwt.secret=1c7f2a0f-0b4e-4f91-8c23-d18f6d94a9fd
jwt.expiration=7200000
jwt.issuer=my-secure-app

minio.url=http://localhost:9000
minio.access-key=AKIAIOSFODNN7EXAMPLE
minio.secret-key=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
minio.bucket=uploads
  • Cách làm này cũng không có gì sai, nhưng chỉ phù hợp cho các project demo hoặc ở môi trường dev. Trên thực tế, nếu các thông tin như password, key, token hay ID chúng ta không nên hardcode trực tiếp trong file cấu hình như vậy -> rất là kém bảo mật.

  • Vậy phải làm sao để bảo mật cũng như quản lý các thông tin này một cách đúng đắn?

=> Bài viết này mình sẽ cùng các bạn tìm hiểu một vài phương pháp hỗ trợ điều đó trong các ứng dụng Spring.

1. - Sử dụng biến môi trường (Environment Variables)

Khi xây dựng một hệ thống chúng ta thường chia ra nhiều môi trường, ví dụ:

local -> Là môi trường mỗi developer làm việc độc lập (thường là laptop, PC của developer đó)

dev -> Là môi trường các developers tích hợp tính năng, test với nhau

test -> Là môi trường cho QA, QC test tính năng

staging -> Là môi trường để khách hàng hoặc đội QA, QC test các tính năng trên dữ liệu thật (có thể hiểu staging gần giống như môi trường product)

product -> Là môi trường nơi mọi tính năng được release cho khách hàng hoặc người dùng cuối.

=> Để đơn giản, mình ví dụ có 2 môi trường devproduct và rõ ràng cấu hình ở hai môi trường này sẽ khác nhau. Khi đó, mình có thể sử dụng file cấu hình theo môi trường như sau:

configuration/
├── .idea/                     # IntelliJ project config
├── env/
│   ├── .env.dev               # Dev environment variables
│   └── .env.prod              # Prod environment variables
├── src/
│   ├── main/
│   │   ├── java/
│   │   │   └── com.davidnguyen.configuration/
│   │   │       ├── ConfigurationApplication.java
│   │   │       └── TestController.java
│   │   └── resources/
│   │       ├── application-dev.yml
│   │       └── application-prod.yml
├── .gitignore
├── pom.xml

Bước 1: Tạo folder env để chứa các file cấu hình theo từng môi trường, ví dụ:

  • Cho môi trường dev:
DB_URL=jdbc:h2:mem:test_db
DB_USERNAME=app_admin
DB_PASSWORD=s3cr3tP@ssw0rd!
  • Cho môi trường product:
DB_URL=jdbc:h2:mem:prod_db
DB_USERNAME=root
DB_PASSWORD=k3fr5tP@ssw0rd!%

Lưu ý: Các bạn nhớ thêm folder env này vào .gitignore để tránh trường hợp commit nhầm lên và khi build ra file .jar (đặc biệt ở môi trường product) cũng không được để lộ file .jar đó vì nếu có file .jar là có thể decode được thông tin cấu hình.

Bước 2: Tạo file .properties hoặc yml với thông tin đọc từ file .env, ví dụ:

  • Cho môi trường dev -> application-dev.yml:
spring:
  datasource:
    url: ${DB_URL}
    username: ${DB_USERNAME}
    password: ${DB_PASSWORD}
    driver-class-name: org.h2.Driver
  jpa:
    hibernate:
      ddl-auto: update
    database-platform: org.hibernate.dialect.H2Dialect
    show-sql: true
  sql:
    init:
      mode: always
server:
  port: ${PORT:8080}

=> Các thông tin database url, username, password mình đã cấu hình để đọc từ biến môi trường. Lúc này kể các nếu bạn có source code (ở môi trường dev) bạn cũng không thể đọc được cấu hình thông tin database ở môi trường product, từ đó giúp hệ thống bảo mật hơn.

Bước 3: Làm sao để đọc cấu hình tương ứng cho từng môi trường?

Môi trường local nếu các bạn sử dụng IDE (ví dụ như IntelliJ) thì có thể thêm cấu hình như sau:

Run -> Edit Configurations -> Modify options -> Environment variables:

Thêm dòng sau:

DB_URL=jdbc:h2:mem:test_db;
DB_USERNAME=app_admin;
DB_PASSWORD=s3cr3tP@ssw0rd!

Các môi trường dev, test, staging hoặc product chạy trên các server, ví dụ:

  • Export các biến môi trường (ví dụ môi trường product)
export $(cat ./env/.env.prod | xargs)
  • Build project:
mvn clean package
  • Chạy ứng dụng với file .jar vừa build.
java -jar target/configuration-0.0.1-SNAPSHOT.jar

2. - Sử dụng tham số dòng lệnh (Command-Line Arguments)

Đây cũng là một phương pháp mình thấy nhiều dự án triển khai, đặc biệt là các dự án cũ triển khai theo mô hình monolithic và deploy thông qua file .jar lên các server.

Bước 1: Cấu hình file application.properties hoặc application.yml và ẩn các thông tin secret đi.

spring.datasource.url=${DB_URL}
spring.datasource.username=${DB_USERNAME}
spring.datasource.password=${DB_PASSWORD}
spring.datasource.driver-class-name=org.h2.Driver

Bước 2: Tạo một class config để đọc các thông tin cấu hình từ tham số dòng lệnh:

  • Thêm dependency sau vào file pom.xml để có thể đọc được cấu hình từ file .java.
<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-configuration-processor</artifactId>
		<optional>true</optional>
</dependency>
  • Tạo file .java để đọc cấu hình, chúng ta có thể tạo nhiều file để đọc các cấu hình khác nhau từ file application.properties hoặc application.yml.

  • Cú pháp các bạn cần lưu ý, nếu file application.properties mình định nghĩa như bên trên thì các fields phải tuân thủ theo rule bên dưới.

@Component
@ConfigurationProperties("spring.datasource")
public class ConfigProperties {
    private String url;
    private String username;
    private String password;

    // setter,getter
}
  • Thêm annotaion @EnableConfigurationProperties để đánh dấu class hiện tại sẽ đọc cấu hình từ file ConfigProperties.
@SpringBootApplication
@EnableConfigurationProperties(ConfigProperties.class)
public class ConfigurationApplication implements CommandLineRunner {
	private final ConfigProperties configProperties;

	public ConfigurationApplication(ConfigProperties configProperties) {
		this.configProperties = configProperties;
	}

	private static final Logger log = LoggerFactory.getLogger(ConfigurationApplication.class);

	public static void main(String[] args) {
		SpringApplication.run(ConfigurationApplication.class, args);
	}

	@Override
	public void run(String... args) throws Exception {
		log.info("---------properties---------");
		log.info("URL {}", configProperties.getUrl());
		log.info("USERNAME {}", configProperties.getUsername());
		log.info("PASSWORD {}", configProperties.getPassword());
	}
}

Bước 3: Quan trọng nhất là các biến ${DB_URL}, ${DB_USERNAME}, ${DB_PASSWORD} chúng ta sẽ đọc từ đâu?

  • Môi trường local -> có thể setup trên IDE, ví dụ với IntelliJ IDEA:

Run -> Edit Configurations -> Modify options -> Program agruments.

Sau đó điền các tham số dưới dạng key-value:

--DB_URL=jdbc:h2:mem:test_db
--DB_USERNAME=app_admin
--DB_PASSWORD=s3cr3tP@ssw0rd!
  • Các môi trường dev, test, staging, product nếu chạy trên server thì chúng ta thường chạy qua file .jar, khi này sẽ truyền trực tiếp vào lệnh chạy như sau:
mvn clean package -D DB_URL=jdbc:h2:mem:test_db -D DB_USERNAME=app_admin -D DB_PASSWORD=s3cr3tP@ssw0rd!
java -jar target/configuration-0.0.1-SNAPSHOT.jar --DB_URL=jdbc:h2:mem:test_db --DB_USERNAME=app_admin --DB_PASSWORD=s3cr3tP@ssw0rd!

=> Check logs các bạn sẽ thấy các thông tin được logs ra:

2025-05-03T22:52:17.770+07:00  INFO 69264 --- [           main] c.d.c.ConfigurationApplication           : ---------properties---------
2025-05-03T22:52:17.770+07:00  INFO 69264 --- [           main] c.d.c.ConfigurationApplication           : URL jdbc:h2:mem:test_db
2025-05-03T22:52:17.771+07:00  INFO 69264 --- [           main] c.d.c.ConfigurationApplication           : USERNAME app_admin
2025-05-03T22:52:17.771+07:00  INFO 69264 --- [           main] c.d.c.ConfigurationApplication           : PASSWORD s3cr3tP@ssw0rd!
  • Nhận xét:

Với phương pháp này, mỗi môi trường khi chạy sẽ setup những tham số riêng. Ưu điểm là có thể tách biệt các thông tin bảo mật ở các môi trường khác nhau. Ví dụ, developer chỉ có quyền query vào database dev hoặc test thì có thể cấu hình tham số tương ứng.

Nhưng nhược điểm là cấu hình khá thủ công, nếu thông tin cấu hình bị thay đổi sẽ phải thay đổi lệnh (có thể là trên nhiều server, nhiều instances) -> tốn thời gian và rủi ro.

3. - Sử dụng file cấu hình bên ngoài (External Config Files)

Về cơ bản, phương pháp này chúng ta sẽ tạo một file secrets.properties tương tự như file .env nhưng chủ yếu dùng trong các trường hợp cấu hình key, token ở các môi trường staging, product (vì môi trường local, dev thường bypass để test cho nhanh).

Lưu ý:

  • File secrets.properties này chỉ nên để trên server, hạn chế quyền truy cập và phải thêm vào .gitignore -> Tóm lại là phải giấu chỉ có ông nào cầm production biết thôi.

  • Các bạn có thể tham khảo source code tại đây.

Bước 1: Tạo file secrets.properties trong thư muc src/main/resources, ví dụ:

PARTNER_NAME=PARTNER_A
PARTNER_KEY_VALUE=k3fr5tP@ssw0rd!%

Bước 2: Làm sao để đọc thông tin từ file secrets.properties?

  • Môi trường local (nếu các cần để test) -> có thể setup trên IDE, ví dụ với IntelliJ IDEA:

Run -> Edit Configurations -> Modify options -> Program agruments

Thêm tham số sau: --spring.config.additional-location=classpath:secrets.properties

  • Các môi trường dev, test, staging, product nếu chạy trên server thì chúng ta thường chạy qua file .jar, khi này sẽ truyền trực tiếp vào lệnh chạy như sau:

Build code ra file .jar:

mvn clean package -D --spring.config.additional-location=classpath:secrets.properties

Chạy code với file .jar:

java -jar target/configuration-0.0.1-SNAPSHOT.jar --spring.config.additional-location=classpath:secrets.properties

4. - Sử dụng Secret Management System (SMS)

Hiện nay có nhiều SMS như HashiCorp Vault, AWS Secrets Manager, Azure Key Vault, Google Secret Manager, Docker Secrets, Kubernetes Secrets...

Alt text

=> Nhưng trong bài viết này mình sẽ cùng các bạn tìm hiểu về HashiCorp Vault (Vault) - một trong những SMS được sử dụng nhiều cho các ứng dụng Spring và Spring Cloud Vault là một module hỗ trợ việc tích hợp Vault trong ứng dụng Spring Boot một cách dễ dàng hơn.

Tích hợp Vault trong ứng dụng Spring Boot

Bước 1: Chạy Vault server thông qua Docker

Như mình đã đề cập, Vault là một server chạy độc lập so với ứng dụng -> Chúng ta sẽ đẩy các thông tin bảo mật vào Vault -> Sau đó, ứng dụng sẽ đọc những thông tin này từ Vault thay vì tự quản lý.

services:
  vault:
    container_name: 'vault'
    image: hashicorp/vault:latest
    environment:
      VAULT_DEV_ROOT_TOKEN_ID: '00000000-0000-0000-0000-000000000000'
    ports:
      - '8200:8200'

Trong bài viết này, việc setup Vault server thông qua Docker compose bằng cách sử dụng file compose.yml được thêm vào root folder.

Chạy lệnh bên dưới để khởi chạy Vault server thông qua Docker.

docker-compose up -d

Truy xuất vào Vault container:

docker exec -it guide-vault sh

Thiết lập biến môi trường (Vault endpoint và authentication token):

export VAULT_TOKEN="00000000-0000-0000-0000-000000000000"
export VAULT_ADDR="http://127.0.0.1:8200"

Lưu các thông tin cấu hình, thông tin bảo mật dưới dạng key-value:

vault kv put secret/gs-vault-config database.url=jdbc:h2:mem:test_db database.username=root database.password=k3fr5tP@ssw0rd!% database.driver=org.h2.Driver

Bước 2: Cấu hình Vault trong ứng dụng Spring Boot.

Để tích hợp Vault trong ứng dụng Spring chúng ta thêm các dependencies sau vào file pom.xml:

<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-starter-vault-config</artifactId>
</dependency>
<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-vault-config-databases</artifactId>
</dependency>

File application.properties:

spring.application.name=gs-vault-config

spring.cloud.vault.token=${VAULT_TOKEN}
spring.cloud.vault.scheme=http
spring.cloud.vault.kv.enabled=true
spring.config.import=vault://

spring.datasource.url=${database.url}
spring.datasource.username=${database.username}
spring.datasource.password=${database.password}
spring.datasource.driver-class-name=${database.driver}

Tạo một .java class để mapping với các thông tin cấu hình. Các bạn lưu ý phải có annotation @ConfigurationProperties("database") để đảm bảo đọc được thông tin.

@ConfigurationProperties("database")
public class DatabaseConfigProperties {
    private String url;
    private String username;
    private String password;
    private String driver;

    // setter,getter
}

Đọc thông tin cấu hình từ ứng dụng:

@SpringBootApplication
@EnableConfigurationProperties(DatabaseConfigProperties.class)
public class ConfigurationApplication implements CommandLineRunner {
	private static final Logger log = LoggerFactory.getLogger(ConfigurationApplication.class);
	private final DatabaseConfigProperties databaseConfigProperties;

	public ConfigurationApplication(DatabaseConfigProperties databaseConfigProperties) {
		this.databaseConfigProperties = databaseConfigProperties;
	}

	public static void main(String[] args) {
		SpringApplication.run(ConfigurationApplication.class, args);
	}

	@Override
	public void run(String... args) throws Exception {
		log.info("---------Database Configuration---------");
		log.info("URL: {}", databaseConfigProperties.getUrl());
		log.info("Username: {}", databaseConfigProperties.getUsername());
		log.info("Password: {}", databaseConfigProperties.getPassword() != null ? "*****" : "null");
	}
}

Bước 3: Làm sao đọc ở các môi trường khác nhau.

  • Nếu các bạn đang chạy ở môi trường local, thông qua IDE như IntelliJ IDEA thì cấu hình biến môi trường như sau:

Run -> Edit Configurations -> Modify options -> Environment variables:

Thêm dòng sau: VAULT_TOKEN=00000000-0000-0000-0000-000000000000

Trên các môi trường khác nhau (local, dev, test, product) -> Authentication token khác nhau -> Setup thông qua biến môi trường.

  • Nếu chạy trên server:

Build project ra file .jar:

VAULT_TOKEN=00000000-0000-0000-0000-000000000000 mvn clean package

Chạy project thông qua file .jar:

VAULT_TOKEN=00000000-0000-0000-0000-000000000000 java -jar target/configuration-0.0.1-SNAPSHOT.jar

Output:

2025-05-04T15:54:23.101+07:00  INFO 86505 --- [gs-vault-config] [           main] c.d.c.ConfigurationApplication           : ---------Database Configuration---------
2025-05-04T15:54:23.101+07:00  INFO 86505 --- [gs-vault-config] [           main] c.d.c.ConfigurationApplication           : URL: jdbc:h2:mem:test_db
2025-05-04T15:54:23.101+07:00  INFO 86505 --- [gs-vault-config] [           main] c.d.c.ConfigurationApplication           : Username: root
2025-05-04T15:54:23.101+07:00  INFO 86505 --- [gs-vault-config] [           main] c.d.c.ConfigurationApplication           : Password: *****
2025-05-04T15:54:23.102+07:00  INFO 86505 --- [gs-vault-config] [           main] c.d.c.ConfigurationApplication           : Driver: org.h2.Driver

Các bạn có thể tham khảo source code của mình tại đây

5. Tổng Kết

Vậy là trong bài viết này mình đã cùng các bạn tìm hiểu về các phương pháp để cấu hình cũng như quản lý các thông tin bảo mật khi làm việc với các ứng dụng Spring Boot nói riêng.

Mỗi phương pháp đều có ưu điểm, nhược điểm riêng. Việc lựa chọn phương pháp nào tuỳ thuộc vào từng bài toán và môi trường cụ thể. Rất mong bài viết sẽ mang đến cho các bạn những kiến thức hữu ích.

Hẹn gặp lại các bạn trong các bài viết tiếp theo. Happy Coding!