| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 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 |
- 소수
- 멀티캠퍼스IT부트캠프
- 자바
- 정렬
- tanstack query
- 멀티캠퍼스IT부트캠프티
- 유레카 부트캠프
- 백준
- LG유플러스 유레카 3기 프론트엔드
- 별찍기10
- 애자일
- 부트캠프후기
- Do it! 자료구조와 함께 배우는 알고리즘 입문
- zod
- 프로세스
- LG유플러스 유레카 부트캠프
- LG유플러스 유레카 프론트엔드
- 브루트포스
- 시간 복잡도
- 프론트엔드 비대면반
- 프론트엔드
- LG유플러스 유레카 프론트엔드 개발자
- 2775번 문제
- Java
- git branch 협업
- 스레드
- 웹시큐리티
- 재귀
- 코딩
- 알고리즘
- Today
- Total
개발 일기
20251013 회원가입 암호화 본문
오늘은 회원가입 암호화에 대해 배웠다.
이전에 구현했던 앱은 웹페이지에서 회원가입한 정보를 DB에 저장하는데 그 데이터가 그대로 저장된다.
오늘 배운 내용으로 웹페이지에서 입력한 비밀번호를 암호화해서 DB에 저장하고 사용자가 입력한 비밀번호를 DB에 저장된 데이터로 입력하는 것이 아닌 가입할 때 입력한 비밀번호를 그대로 입력하고 로그인될 수 있도록 했다.
회원가입 시 PW를 암호화하여 DB에 저장하기
1. saltInfo table 생성 : 패스워드 해쉬에 사용할 salt를 저장할 테이블
drop table if exists saltInfo;
create table saltInfo(
id varchar(50) primary key,
salt varchar(256) );

2. SaltInfo.java 생성
get, set, toString 메서드들을 자동생성해주고 연결하는 롬복을 사용했다.
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class SaltInfo {
private String id, salt;
}
3. SaltDao.java 생성
SaltDao는 SaltInfo 데이터를 DB에 저장하기 위한 매퍼 인터페이스이다.
import org.apache.ibatis.annotations.Mapper;
import com.ureca.web.model.dto.SaltInfo;
@Mapper
public interface SaltDao {
public void insertSalt(SaltInfo saltInfo);
}
4. salt.xml
SaltDao 인터페이스의 메서드와 데이터베이스 쿼리를 연결해주는 설정 파일이다.
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.shop.cafe.dao.SaltDao">
<insert id="insertSalt" parameterType="SaltInfo">
INSERT INTO saltInfo (email, salt)
VALUES (#{email}, #{salt})
</insert>
</mapper>
5. OpenCrypt.java
암호화하는 클래스 생성
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
public class OpenCrypt {
public static byte[] getSHA256(String source, String salt) {
byte byteData[]=null;
try{
MessageDigest md = MessageDigest.getInstance("SHA-256");
md.update(source.getBytes());
md.update(salt.getBytes());
byteData= md.digest();
System.out.println("원문: "+source+ " SHA-256: "+
byteData.length+","+byteArrayToHex(byteData));
}catch(NoSuchAlgorithmException e){
e.printStackTrace();
}
return byteData;
}
public static byte[] generateKey(String algorithm,int keySize) throws NoSuchAlgorithmException {
KeyGenerator keyGenerator = KeyGenerator.getInstance(algorithm);
keyGenerator.init(keySize);
SecretKey key = keyGenerator.generateKey();
return key.getEncoded();
}
public static String aesEncrypt(String msg, byte[] key) throws Exception {
SecretKeySpec skeySpec = new SecretKeySpec(key, "AES");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
String iv = "AAAAAAAAAAAAAAAA";
cipher.init(Cipher.ENCRYPT_MODE,
skeySpec,
new IvParameterSpec(iv.getBytes()));
byte[] encrypted = cipher.doFinal(msg.getBytes());
return byteArrayToHex(encrypted);
}
public static String aesDecrypt(String msg,byte[] key ) throws Exception {
SecretKeySpec skeySpec = new SecretKeySpec(key, "AES");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
String iv = "AAAAAAAAAAAAAAAA";
cipher.init(Cipher.DECRYPT_MODE,
skeySpec,
new IvParameterSpec(iv.getBytes()));
byte[] encrypted = hexToByteArray(msg);
byte[] original = cipher.doFinal(encrypted);
return new String(original);
}
public static byte[] hexToByteArray(String hex) {
if (hex == null || hex.length() == 0) {
return null;
}
byte[] ba = new byte[hex.length() / 2];
for (int i = 0; i < ba.length; i++) {
ba[i] = (byte) Integer.parseInt(hex.substring(2 * i, 2 * i + 2), 16);
}
return ba;
}
// byte[] to hex
public static String byteArrayToHex(byte[] ba) {
if (ba == null || ba.length == 0) {
return null;
}
StringBuffer sb = new StringBuffer(ba.length * 2);
String hexNumber;
for (int x = 0; x < ba.length; x++) {
hexNumber = "0" + Integer.toHexString(0xff & ba[x]);
sb.append(hexNumber.substring(hexNumber.length() - 2));
}
return sb.toString();
}
}
6. MemberService.java
회원가입 시 salt를 생성하고 pw 암호화하는 과정을 거치는 코드를 추가한다.
@Autowired
SaltDao saltDao;
// 회원가입
@Transactional(rollbackFor = Exception.class)
public void registerMember(Member member) throws UplusException {
try {
// 유효성 검사
// salt 생성
String salt = UUID.randomUUID().toString();
SaltInfo saltInfo = new SaltInfo(member.getId(), salt);
saltDao.insertSalt(saltInfo);
// pw 암호화
String pwdHash = OpenCrypt.byteArrayToHex(OpenCrypt.getSHA256(member.getPw(), salt));
member.setPw(pwdHash);
memberDao.registerMember(member);
} catch (DuplicateKeyException e) {
throw new UplusException("id나 email이 이미 사용중입니다");
} catch (Exception e) {
e.printStackTrace();
throw new UplusException("잠시 후 다시 시도해 주세요");
}
}
실행테스트 결과


암호화된 pw가 들어온 것을 볼 수 있다.
테스트 시에 원래 pw가 무엇인지 알 수 없기 때문에 따로 저장해두는 것이 좋을 것 같다.
하지만 이 상태로 로그인을 시도하면 pw가 위처럼 원래 비밀번호 상태가 아니기 때문에 로그인이 아직 불가능하다.
그래서 로그인을 위한 코드도 만들어줘야된다.
로그인 기능 수정 구현
1. SaltDao.java 코드 추가
import org.apache.ibatis.annotations.Mapper;
import com.shop.cafe.dto.SaltInfo;
@Mapper
public interface SaltDao {
public void insertSalt(SaltInfo saltInfo) throws Exception;
public SaltInfo selectSalt(String email) throws Exception;
}
2. salt.xml 코드 추가
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.shop.cafe.dao.SaltDao">
<insert id="insertSalt" parameterType="SaltInfo">
INSERT INTO saltInfo (email, salt)
VALUES (#{email}, #{salt})
</insert>
<select id="selectSalt" parameterType="String" resultType="SaltInfo">
select * from saltInfo where email=#{email}
</select>
</mapper>
3. MemberService.java
따로 저장한 salt DB에서 해당 사용자의 salt를 가져온다.
// 유플러스 id로 로그인
public Member upidLogin(Member m) throws UplusException {
try {
SaltInfo saltInfo = saltDao.selectSalt(m.getId());
byte[] pwdHash = OpenCrypt.getSHA256(m.getPw(), saltInfo.getSalt());
String pwdHashHex = OpenCrypt.byteArrayToHex(pwdHash);
m.setPw(pwdHashHex);
return memberDao.upidLogin(m);
} catch (Exception e) {
throw new UplusException("잠시 후 다시 시도해 주세요.");
}
}
실행테스트 결과

위의 회원가입한 아이디로 로그인이 잘 되는 것을 볼 수 있다. 비밀번호도 보이진 않지만 위의 가입할 때 비밀번호로 로그인을 진행했다.
위의 코드는 강사님이 준비해주신 코드이기 때문에 실습을 하고 기능구현 자체에는 어려움이 없었다.
그래서 암호화하는 OpenCrypt 클래스 코드를 살펴봤다.
OpenCrypt
- getSHA256()
public static byte[] getSHA256(String source, String salt) {
byte byteData[]=null;
try{
MessageDigest md = MessageDigest.getInstance("SHA-256");
md.update(source.getBytes());
md.update(salt.getBytes());
byteData= md.digest();
System.out.println("원문: "+source+ " SHA-256: "+
byteData.length+","+byteArrayToHex(byteData));
}catch(NoSuchAlgorithmException e){
e.printStackTrace();
}
return byteData;
}
source(평문 비밀번호)와 salt를 결합해 SHA-256 해시(바이트 배열)를 계산해서 반환하는 메서드다.
위 코드를 테스트 해보면

source가 1234이고 암호화된 문자열의 길이가 32이고 암호화된 문자들을 볼 수 있다.

이는 실습 때 사용한 비밀번호를 실행해본 결과인데 위의 DB의 값과 다른 것을 볼 수 있다.
그 이유는 회원가입 때 UUID.randomUUID()로 salt를 새로 만들기 때문에 같은 비밀번호라도 사용자별로 salt가 달라지면서 해시가 달라지기 때문이다.
- generateKey()
public static byte[] generateKey(String algorithm,int keySize) throws NoSuchAlgorithmException {
KeyGenerator keyGenerator = KeyGenerator.getInstance(algorithm);
keyGenerator.init(keySize);
SecretKey key = keyGenerator.generateKey();
return key.getEncoded();
}
지정한 알고리즘과 키 길이를 사용해 임의의 대칭키를 생성해 반환하는 메서드다.
- aesEncrypt()
public static String aesEncrypt(String msg, byte[] key) throws Exception {
SecretKeySpec skeySpec = new SecretKeySpec(key, "AES");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
String iv = "AAAAAAAAAAAAAAAA";
cipher.init(Cipher.ENCRYPT_MODE,
skeySpec,
new IvParameterSpec(iv.getBytes()));
byte[] encrypted = cipher.doFinal(msg.getBytes());
return byteArrayToHex(encrypted);
}
주어진 평문 문자열을 AES 알고리즘으로 암호화해서 16진수 문자열로 반환하는 메서드이다.
iv가 고정되어 있기 때문에 같은 키와 같은 평문이면 항상 같은 암호문이 나온다.
- aesDecrypt()
public static String aesDecrypt(String msg,byte[] key ) throws Exception {
SecretKeySpec skeySpec = new SecretKeySpec(key, "AES");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
String iv = "AAAAAAAAAAAAAAAA";
cipher.init(Cipher.DECRYPT_MODE,
skeySpec,
new IvParameterSpec(iv.getBytes()));
byte[] encrypted = hexToByteArray(msg);
byte[] original = cipher.doFinal(encrypted);
return new String(original);
}
16진수로 인코딩된 암호문과 key를 받아 AES/CBC/PKCS5Padding 방식으로 복호화해서 원문 문자열을 반환한다.
테스트 코드
try {
// 1) 테스트할 평문
String plain = "Hello, Uplus! 이것은 테스트 문자열입니다. 1234!@#";
// 2) AES 키 생성 (128비트 사용; 필요하면 256으로 변경)
byte[] key = OpenCrypt.generateKey("AES", 128); // throws NoSuchAlgorithmException
// 3) 암호화
String encryptedHex = OpenCrypt.aesEncrypt(plain, key);
System.out.println("Encrypted (hex): " + encryptedHex);
// 4) 복호화
String decrypted = OpenCrypt.aesDecrypt(encryptedHex, key);
System.out.println("Decrypted : " + decrypted);
// 5) 확인: 원문 == 복원된 문자열
if (plain.equals(decrypted)) {
System.out.println("성공: 복호화된 문자열이 원문과 동일합니다.");
} else {
System.out.println("실패: 복호화된 문자열이 원문과 다릅니다!");
}
// (선택) 키를 Base64로 출력해서 복사/저장 테스트
String keyBase64 = java.util.Base64.getEncoder().encodeToString(key);
System.out.println("Key (Base64): " + keyBase64);
} catch (Exception e) {
e.printStackTrace();
}
실행 결과

느낀 점
보안을 위해 암호를 암호화하고 복호화하는 과정들이 재밌어보였다. 그래서 코드를 작성하고 배우는 데 지루하지 않았다.
자바라는 언어 자체에 참 많은 클래스들이 준비되어 있어서 대단한 언어라고 느껴졌고 자바스크립트랑 비교가 되기도 했다.
앞으로 개인 프로젝트든 팀 프로젝트든 보안적인 측면을 항상 고려해야겠다.
'TIL' 카테고리의 다른 글
| 20251028 사용자 경험(CX) 디자인 (0) | 2025.10.28 |
|---|---|
| JWT와 Session의 개념 및 차이 (0) | 2025.10.20 |
| 20251001 uplus clone site 다시 만들기2 (0) | 2025.10.01 |
| 20250930 uplus clone site 다시 만들기 (0) | 2025.09.30 |
| 20250925 DB (1) | 2025.09.25 |