티스토리 뷰
반응형
3줄 요약
PostgreSql 데이터의 암복화 기능이 필요
PostgreSql의 pgcrypto extension으로 가능
JPA 엔티티에 암복호화 설정도 가능
들어가기 앞서
본 글은 pg_crypto 모듈 기반 PostgreSql 설정 및 JPA 설정을 정리하기 위한 목적으로 작성되었다.
간단한 회원가입과 이름 조회 서비스 코드를 작성했고, 간단하게 통합테스트 코드를 통해 작동 방식을 확인했다.
(테스트 환경은 테스트 컨테이너를 통해 postgreSql 14 이미지로 컨테이너를 생성하고, flyway를 통해 데이터 스키마를 정의했다.)
실제 샘플 코드를 실행해보자 한다면 먼저 도커를 설치해 사용하기를 권장한다.
또한, 샘플 코드와 암호화 키 등은 이해를 돕기 위해 임의로 만든 것이니 실 사용 시 주의가 필요하다.
실제 프로젝트를 보고 싶다면 해당 레포에서 확인할 수 있다.
환경설정
1) build.gradle 설정
더보기
plugins {
id 'java'
id 'org.springframework.boot' version '3.4.5'
id 'io.spring.dependency-management' version '1.1.7'
id 'org.asciidoctor.jvm.convert' version '3.3.2'
}
group = 'com'
version = '0.0.1-SNAPSHOT'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.flywaydb:flyway-core'
implementation 'org.flywaydb:flyway-database-postgresql'
testImplementation 'org.testcontainers:junit-jupiter'
testImplementation 'org.springframework.boot:spring-boot-testcontainers'
testImplementation 'org.testcontainers:postgresql'
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-docker-compose'
runtimeOnly 'org.postgresql:postgresql'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
dependencyManagement {
imports {
mavenBom "org.testcontainers:testcontainers-bom:${testcontainersVersion}"
}
}
tasks.named('test') {
outputs.dir snippetsDir
useJUnitPlatform()
}
2) flyway 문서(V1__create_members_table.sql
더보기
-- Create pgcrypto extension
CREATE EXTENSION IF NOT EXISTS pgcrypto;
-- Create members table
DROP TABLE IF EXISTS members;
CREATE TABLE members (
id BIGSERIAL PRIMARY KEY,
name BYTEA NOT NULL,
email BYTEA NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL,
phone_number BYTEA NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
comment on table members is '회원';
comment on column members.id is '회원 ID';
comment on column members.name is '회원명';
comment on column members.email is '이메일';
comment on column members.password is '비밀번호';
comment on column members.phone_number is '전화번호';
comment on column members.created_at is '생성일시';
comment on column members.updated_at is '수정일시';
pg_crypto 및 JPA 엔티티 설정
1) PostgreSql에 pgcrypto을 설치한다.
CREATE EXTENSION IF NOT EXISTS pgcrypto;
2) 암호화할 테이블 칼럼 타입은 BYTEA로 한다.
CREATE TABLE members (
id BIGSERIAL PRIMARY KEY,
name BYTEA NOT NULL,
email BYTEA NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL,
phone_number BYTEA NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
comment on table members is '회원';
comment on column members.id is '회원 ID';
comment on column members.name is '회원명';
comment on column members.email is '이메일';
comment on column members.password is '비밀번호';
comment on column members.phone_number is '전화번호';
comment on column members.created_at is '생성일시';
comment on column members.updated_at is '수정일시';
3) application.yml 설정
spring.datasource.hikari.connection-init.sql:
security-properties.encryptionSecretKey:
- 애플리케이션이 뜨면서 HikariCP가 최소 유휴 커넥션 수(minimumIdle) 만큼 새 JDBC 커넥션을 “물리적으로” 연다.
- 각 커넥션을 열자마자 바로 다음 순서로 SQL을 실행
- 커넥션 열기 → select set_config('encryption.key', '${security-properties.encryptionSecretKey}', false) 실행 → 커넥션 유효성 검사 → 풀에 보관
- 빌려 쓰는 순간에는 connection-init-sql이 다시 실행되지 않는다.(한 커넥션당 딱 한 번)
- 임의의 암호화 키를 설정, 명명은 각자 커스텀해서 사용해도 무관
application.yml(예시)
더보기
spring:
datasource:
driver-class-name: org.testcontainers.jdbc.ContainerDatabaseDriver
url: jdbc:tc:postgresql:14:///test
hikari:
connection-init-sql: select set_config('encryption.key', '${security-properties.encryptionSecretKey}', false)
jpa:
hibernate:
ddl-auto: validate
show-sql: true
properties:
hibernate:
format_sql: true
dialect: org.hibernate.dialect.PostgreSQLDialect
flyway:
enabled: true
baseline-on-migrate: true
locations: classpath:db/migration
security-properties:
encryptionSecretKey: test-encryption-secret-key
JPA 엔티티 설정
- ColumnTransformer(read = "...", write = "..."): DB 데이터를 READ, WRITE 시 정의한 쿼리를 수행
options | description |
read | DB → 엔티티 값을 읽어 올 때 적용할 SQL 식 |
write | 엔티티 → DB 값을 저장·수정할 때 적용할 SQL 식 (?가 필드 값 자리) |
forColumn | (선택) 테이블에 같은 필드명이 여러 컬럼으로 맵핑될 때 어떤 컬럼에 적용할지 지정 |
1) Members(Entity) 설정
package com.sandbox.member;
import com.sandbox.BaseEntity;
import jakarta.persistence.*;
import jakarta.validation.constraints.NotNull;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.ColumnTransformer;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.nio.charset.StandardCharsets;
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
public class Members extends BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ColumnTransformer(
read = "pgp_sym_decrypt(" + "name, " + "current_setting('encryption.key')" + ")",
write = "pgp_sym_encrypt(" + "?::text," + "current_setting('encryption.key')" + ")")
@NotNull
@Column(name = "name", nullable = false, columnDefinition = "bytea")
private String name;
private String password;
@ColumnTransformer(
read = "pgp_sym_decrypt(" + "email, " + "current_setting('encryption.key')" + ")",
write = "pgp_sym_encrypt(" + "?::text," + "current_setting('encryption.key')" + ")")
@NotNull
@Column(name = "email", nullable = false, columnDefinition = "bytea")
private String email;
@ColumnTransformer(
read = "pgp_sym_decrypt(" + "phone_number, " + "current_setting('encryption.key')" + ")",
write = "pgp_sym_encrypt(" + "?::text," + "current_setting('encryption.key')" + ")")
@NotNull
@Column(name = "phone_number", nullable = false, columnDefinition = "bytea")
private String phoneNumber;
@Builder
private Members(String name, String email, String password, String phoneNumber) {
if (name == null || name.isEmpty()) {
throw new IllegalArgumentException("이름을 입력해주세요.");
}
this.name = name;
if (email == null || email.isEmpty()) {
throw new IllegalArgumentException("이메일을 입력해주세요.");
}
this.email = email;
if (password == null || password.isEmpty()) {
throw new IllegalArgumentException("비밀번호를 입력해주세요.");
}
this.password = hashPassword(password);
if (phoneNumber == null || phoneNumber.isEmpty()) {
throw new IllegalArgumentException("전화번호를 입력해주세요.");
}
this.phoneNumber = phoneNumber;
}
private String hashPassword(String password) {
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] encodedhash = digest.digest(password.getBytes(StandardCharsets.UTF_8));
StringBuilder hexString = new StringBuilder();
for (byte b : encodedhash) {
String hex = Integer.toHexString(0xff & b);
if (hex.length() == 1) hexString.append('0');
hexString.append(hex);
}
return hexString.toString();
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("Hash algorithm not found", e);
}
}
}
테스트 코드
package com.sandbox.member;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.ActiveProfiles;
import org.testcontainers.junit.jupiter.Testcontainers;
import static org.assertj.core.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.*;
@Testcontainers
@ActiveProfiles("test")
@SpringBootTest
class MemberServiceTest {
@Autowired
private MemberRepository memberRepository;
@Autowired
private MemberService memberService;
@AfterEach
void tearDown() {
memberRepository.deleteAllInBatch();
}
@DisplayName("회원가입을 하면 ID를 반환한다")
@Test
void register() {
// given
MemberRequestDto requestDto = new MemberRequestDto("name", "email", "password", "phoneNumber");
// when
Long savedMemberId = memberService.register(requestDto);
// then
assertAll(
() -> assertThat(savedMemberId).isNotNull(),
() -> assertThat(savedMemberId).isEqualTo(1L)
);
}
@DisplayName("회원가입시 이미 동일한 이메일로 가입된 회원이 있다면 예외가 발생한다.")
@Test
void registerDuplicateEmail() {
// given
MemberRequestDto requestDto = new MemberRequestDto("name", "email", "password", "phoneNumber");
memberRepository.save(requestDto.toEntity());
// when, then
Throwable throwable = assertThrows(IllegalArgumentException.class, () -> memberService.register(requestDto));
assertThat(throwable.getMessage()).isEqualTo("이미 해당 이메일로 가입된 회원이 존재합니다.");
}
@DisplayName("이메일로 회원정보를 조회할 수 있다.")
@Test
void findByEmail() {
// given
String targetEmail = "email";
MemberRequestDto requestDto = new MemberRequestDto("name", targetEmail, "password", "phoneNumber");
memberService.register(requestDto);
// when
Members foundMember = memberService.findByEmail(targetEmail);
// then
assertAll(
() -> assertThat(foundMember).isNotNull(),
() -> assertThat(foundMember.getEmail()).isEqualTo(targetEmail)
);
}
}
쿼리 결과
Hibernate:
select
m1_0.id
from
members m1_0
where
pgp_sym_decrypt(m1_0.email, current_setting('encryption.key'))=?
fetch
first ? rows only
Hibernate:
insert
into
members
(created_at, email, name, password, phone_number, updated_at)
values
(?, pgp_sym_encrypt(?::text, current_setting('encryption.key')), pgp_sym_encrypt(?::text, current_setting('encryption.key')), ?, pgp_sym_encrypt(?::text, current_setting('encryption.key')), ?)
Hibernate:
select
m1_0.id,
m1_0.created_at,
pgp_sym_decrypt(m1_0.email, current_setting('encryption.key')),
pgp_sym_decrypt(m1_0.name, current_setting('encryption.key')),
m1_0.password,
pgp_sym_decrypt(m1_0.phone_number, current_setting('encryption.key')),
m1_0.updated_at
from
members m1_0
where
pgp_sym_decrypt(m1_0.email, current_setting('encryption.key'))=?
Reference
https://dev.to/cristiancfm/encrypting-postgresql-database-columns-in-spring-boot-jpa-entities-4915
Encrypting PostgreSQL database columns in Spring Boot JPA entities
A simple tutorial on how to encrypt single columns in a PostgreSQL database using JPA entities in a...
dev.to
반응형
반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
- Total
- Today
- Yesterday
링크
TAG
- Spring
- 유데미
- BFS
- Java
- JPA
- 코드트리
- jeus
- JWT
- BufferedWriter
- 객체정렬
- 자바
- 회고록
- 알고리즘
- springboot
- script
- 전자정부프레임워크
- 재기동
- BufferedReader
- NLU
- 나만의챗봇
- dxdy
- 챗봇
- thymeleaf
- RASA
- 글또
- spring boot
- Comparable
- 백준
- Comparator
- PostgreSQL
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 |
글 보관함