[Spring] 이메일 양방향 암호화, 비밀번호 단방향 암호화 & 회원가입, 로그인

Miin·2023년 9월 16일
0

Spring

목록 보기
1/17

나의 첫 스프링 프로젝트 시작!! 🌱

나의 프로젝트 주제는 '지속가능한 여행'이다. 심각한 문제로 계속해서 대두되고 있는 지구온난화 문제를 계기로 생각한 주제이며, 사용자들에게 친환경 생태 관광지와 비건 식당, 친환경 호텔 정보를 제공해주는 어플리케이션을 만들 예정이다. 그리고 이 앱을 플레이스토어에 등록하는 것까지가 우리 팀의 목표이다!


스프링에 대해 깊이 공부하지 못했지만, 이렇게 프로젝트를 무작정 시작해보게 되었다. 프로젝트를 진행하면서 나는 많은 것을 얻을 수 있을 것이라 믿는다. 사실 프로젝트를 진행한 지는 꽤 되었으나 개발 일정을 맞추는 데 급급해 포스트 작성을 이제야 한다..!🥹 부족하지만 나에게 좋은 밑거름이 되길 바란다.


이메일 정보 양방향 암호화, 비밀번호 정보 단방향 암호화하여 회원가입하기

앱을 플레이 스토어에 등록하기 위해서는 개인정보 보호에 관해 신경쓸 것이 꽤나 많았다.
https://www.privacy.go.kr/front/bbs/bbsList.do?bbsNo=BBSMSTR_000000000049
개인정보 포털 사이트에서 개인정보 보호법에 대한 내용을 자세히 볼 수 있었다.

  • 안전한 암호 알고리즘 예시
구분알고리즘명칭
대칭키 암호 알고리즘SEED, ARIA-128/192/256, AES-128/192/256, HIGHT, LEA 등
공개키 암호 알고리즘RSAES-OAEP, RSAES-PKCS1 등
일방향 암호 알고리즘SHA-256/384/512 등



  • 이메일 : 양방향 암호화 (암호화된 암호문 복호화 가능)
  • 비밀번호 : 단방향 암호화 (암호화된 암호문 복호화 불가능)


암호 알고리즘 종류는 매우 많지만, 일반적으로 SHA-256(단방향)이나 AES-256(양방향) 이상을 사용한다고 한다.

나는 스프링 시큐리티를 이용해 암호화를 진행하였다.

build.gradle에 의존성 추가

implementation 'org.springframework.boot:spring-boot-starter-security'

SecurityConfig

// @Configuration annotation이 @EnableWebSecurity에 포함되어 있음
@EnableWebSecurity
public class SecurityConfig {
	// 대칭키
    @Value("${symmetric.key}")
    private String symmetrickey;

    // PasswordEncoder interface의 구현체가 BCryptPasswordEncoder임을 수동 빈 등록을 통해 명시
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

	// AesBytesEncryptor 사용을 위한 Bean등록
    @Bean
    public AesBytesEncryptor aesBytesEncryptor() {
        return new AesBytesEncryptor(symmetrickey,"70726574657374");
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .csrf().disable();
        // CSRF(크로스 사이트 요청 위조)라는 인증된 사용자를 이용해
        // 서버에 위험을 끼치는 요청들을 보내는 공격을 방어하기 위해 post 요청마다 token이 필요한 과정을 생략하겠음을 의미
        return http.build();
    }
}

symmetrickey는 암호화와 복호화를 해줄 때 필요한 대칭키이다. 키 값은 yml 파일에 저장하여 꺼내 사용하였다.
그 옆의 문자열은 salt이다. salt는 암호화해줄 문자열에 임의의 문자열을 덧붙여주어, 같은 비밀번호의 회원이 여러 명 있을지라도 안전하게 저장된다.

이메일 양방향 암호화

AesBytesEncryptor

EncryptService

@Service
@RequiredArgsConstructor
public class EncryptService {

    private final AesBytesEncryptor encryptor;

	// 암호화
    public String encryptEmail(String email) {
        byte[] encrypt = encryptor.encrypt(email.getBytes(StandardCharsets.UTF_8));
        return byteArrayToString(encrypt);
    }
	
    // 복호화
    public String decryptEmail(String encryptString) {
        byte[] decryptBytes = stringToByteArray(encryptString);
        byte[] decrypt = encryptor.decrypt(decryptBytes);
        return new String(decrypt, StandardCharsets.UTF_8);
    }

	// byte -> String
    public String byteArrayToString(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (byte abyte : bytes) {
            sb.append(abyte);
            sb.append(" ");
        }
        return sb.toString();
    }

	// String -> byte
    public byte[] stringToByteArray(String byteString) {
        String[] split = byteString.split("\\s");
        ByteBuffer buffer = ByteBuffer.allocate(split.length);
        for (String s : split) {
            buffer.put((byte) Integer.parseInt(s));
        }
        return buffer.array();
    }
}

비밀번호 단방향 암호화 & 회원가입

BCryptPasswordEncoder

memberService

	@Autowired
    private final MemberRepository memberRepository;
    @Autowired
    private final EncryptService encryptService;
    @Autowired
    private PasswordEncoder passwordEncoder;

    public void registerUser(MemberDTO memberDTO) {
        // 이메일 양방향 암호화
        String encodedEmail = encryptService.encryptEmail(memberDTO.getUserEmail());
        memberDTO.setUserEmail(encodedEmail);

        // 비밀번호 단방향 암호화
        String encodedPassword = passwordEncoder.encode(memberDTO.getUserPassword());
        memberDTO.setUserPassword(encodedPassword);

        String nickname = memberDTO.getUserNickname();
        memberDTO.setUserNickname(nickname);

        MemberEntity memberEntity = MemberEntity.toMemberEntity(memberDTO);
        memberRepository.save(memberEntity);
    }

memberController

	@PostMapping("/signup")
    public HashMap<String, Object> signUp(@RequestBody MemberDTO memberDTO) {

        HashMap<String, Object> map = new HashMap<>();

        try {
            memberService.registerUser(memberDTO);
            map.put("success", Boolean.TRUE);
        } catch (Exception e) {
            map.put("success", Boolean.FALSE);
            map.put("error", e.getMessage());
        }

        return map;
    }

로그인

memberService

	public Optional<MemberEntity> userLogin(MemberDTO memberDTO) throws Exception {
        String nickname = memberDTO.getUserNickname();

		// 닉네임으로 유저를 찾음
        Optional<MemberEntity> user = memberRepository.findByUserNickname(nickname);

        if (user.isPresent()) { // 조회 결과가 있다(해당 닉네임을 가진 회원 정보가 있다.)
            // passwordEncoder를 이용한 암호 비교 로직
            if (passwordEncoder.matches(memberDTO.getUserPassword(), user.get().getUserPassword()) == true) {
                return user;
            }
            else { // 비밀번호 불일치
                return null;
            }
        }
        else { // 해당 닉네임을 가진 회원 정보가 없다
            return null;
        }

    }

memberController

	@PostMapping("/userLogin")
    public HashMap<String, Object> userLogin(@RequestBody MemberDTO memberDTO) {

        HashMap<String, Object> map = new HashMap<>();

        try {
            Optional<MemberEntity> loginResult = memberService.userLogin(memberDTO);

            // 이메일 복호화 테스트 부분
            String nickname = memberDTO.getUserNickname();
            Optional<MemberEntity> user = memberRepository.findByUserNickname(nickname);
            String decodedEmail = encryptService.decryptEmail(user.get().getUserEmail());

            map.put("success", Boolean.TRUE);
            map.put("userId", loginResult.get().getUserId());

            map.put("userEmail", decodedEmail); // 이메일 복호화 테스트 부분
        } catch (Exception e) {
            map.put("success", Boolean.FALSE);
            map.put("message", e.getMessage());
        }

        return map;
    }

이메일 정보 복호화도 정상적으로 되는 것을 함께 확인하였다.



💡 회원가입을 할 때 유저에게서 닉네임, 이메일, 비밀번호를 받고나서, 원래는 [이메일과 비밀번호]를 통해 로그인을 구현하려고 하였다. 그러나 양방향 암호화가 되어있는 이메일로 유저를 찾는 것보다는 닉네임을 아이디 역할로 바꾸어 [아이디와 비밀번호]로 로그인을 시키고, 암호화가 되어있지 않은 이 아이디를 통해 유저를 찾는 것이 더 효과적이라고 생각하여 닉네임을 아이디의 역할로서 기능하도록 바꾸기로 결정했다.


💡 앱 하나를 개발하면서 개인정보 보호가 무척이나!! 매우!! 중요하다는 것을 깨닫게 되었다. 이를 위해 다양한 자료조사를 진행하면서 나에게 무척이나 큰 도움을 받을 수 있었다. SHA, AES 암호화 알고리즘을 이해하고 적용해봄으로써, 앱 개발 과정에서는 보안 측면을 항상 고려해야 하며 사용자들에게 안전하고 신뢰성 있는 환경을 제공할 수 있도록 하는 노력이 필요함을 깨달았다. 개인정보 보호는 모든 개발자에게 중요한 과제이며, 이를 위해 계속해서 학습해야겠다.

참고자료

https://velog.io/@zz1996zz/%EC%95%94%ED%98%B8%ED%99%94%EC%99%80-%EB%B3%B5%ED%98%B8%ED%99%94-AesBytesEncryptor

https://kkkdh.tistory.com/entry/Spring-Security%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EB%B9%84%EB%B0%80%EB%B2%88%ED%98%B8-%EC%95%94%ED%98%B8%ED%99%94-%EB%A1%9C%EC%A7%81-%EA%B5%AC%ED%98%84-%EA%B3%BC%EC%A0%95-%EA%B8%B0%EB%A1%9D%ED%95%98%EA%B8%B0

profile
컴퓨터공학전공 학부생 Back-end Developer

0개의 댓글