나의 첫 스프링 프로젝트 시작!! 🌱
나의 프로젝트 주제는 '지속가능한 여행'이다. 심각한 문제로 계속해서 대두되고 있는 지구온난화 문제를 계기로 생각한 주제이며, 사용자들에게 친환경 생태 관광지와 비건 식당, 친환경 호텔 정보를 제공해주는 어플리케이션을 만들 예정이다. 그리고 이 앱을 플레이스토어에 등록하는 것까지가 우리 팀의 목표이다!
스프링에 대해 깊이 공부하지 못했지만, 이렇게 프로젝트를 무작정 시작해보게 되었다. 프로젝트를 진행하면서 나는 많은 것을 얻을 수 있을 것이라 믿는다. 사실 프로젝트를 진행한 지는 꽤 되었으나 개발 일정을 맞추는 데 급급해 포스트 작성을 이제야 한다..!🥹 부족하지만 나에게 좋은 밑거름이 되길 바란다.
앱을 플레이 스토어에 등록하기 위해서는 개인정보 보호에 관해 신경쓸 것이 꽤나 많았다.
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(양방향) 이상을 사용한다고 한다.
나는 스프링 시큐리티를 이용해 암호화를 진행하였다.
implementation 'org.springframework.boot:spring-boot-starter-security'
// @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
는 암호화해줄 문자열에 임의의 문자열을 덧붙여주어, 같은 비밀번호의 회원이 여러 명 있을지라도 안전하게 저장된다.
@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();
}
}
@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);
}
@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;
}
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;
}
}
@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;
}
이메일 정보 복호화도 정상적으로 되는 것을 함께 확인하였다.
💡 회원가입을 할 때 유저에게서 닉네임, 이메일, 비밀번호를 받고나서, 원래는 [이메일과 비밀번호]를 통해 로그인을 구현하려고 하였다. 그러나 양방향 암호화가 되어있는 이메일로 유저를 찾는 것보다는 닉네임을 아이디 역할로 바꾸어 [아이디와 비밀번호]로 로그인을 시키고, 암호화가 되어있지 않은 이 아이디를 통해 유저를 찾는 것이 더 효과적이라고 생각하여 닉네임을 아이디의 역할로서 기능하도록 바꾸기로 결정했다.
참고자료