개발 일기

20251013 회원가입 암호화 본문

TIL

20251013 회원가입 암호화

종현종현 2025. 10. 13. 18:15

오늘은 회원가입 암호화에 대해 배웠다.

이전에 구현했던 앱은 웹페이지에서 회원가입한 정보를 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
Comments