- 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
- 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
hayID
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 dev
và product
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ừ fileapplication.properties
hoặcapplication.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ừ fileConfigProperties
.
@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...
=> 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!