본문 바로가기

Server Programming/Spring Boot Full-Stack Programming

[스프링 풀스택 클론 코딩 - 회원가입] (1-8) 회원가입 패스워드 인코딩

반응형

암호를 평문으로 저장하지 않기 위해

해시 함수를 이용해 인코딩해서 저장

-> 해싱 알고리즘

 

스프링 시큐리트가 제공하는 bcrypt를 이용하는데, 솔트를 사용

-> 해커가 규칙성을 파악하기위해 여러번 인덱싱 해서, 복호화가 가능하도록 하는 것을 쓰레기값을 추가해 방지하는 방법

-> 즉, 같은 암호를 가지고도 새로운 값을 추가하므로, 안전한 저장이 가능하다.

 


AccountControllerTest

package com.demo.account;

import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.BDDMockito.then;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view;

import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.test.web.servlet.MockMvc;

import com.demo.domain.Account;

//테스트를 위한 어노테이션 -> 스프링부트테스트, 자동설정목업 MVC
@SpringBootTest @AutoConfigureMockMvc
//사용하면 서블릿이 뜨고, @AutoConfigureWebClient  @AutoConfigureTestClient를 이용하면 테스트 가능 
//@SpringBootTest (webEnvironment =WebEnvironment.DEFINED_PORT)
//@SpringBootTest (webEnvironment =WebEnvironment.NONE)
//@SpringBootTest (webEnvironment =WebEnvironment.RANDOM_PORT)

//테스트 코드가 필요한 이유
//코드를 변경한 이후에 작성한 코드가 오류를 발생시키지 않았다는 것을 확인하기 위해
//모놀리틱 아키텍처의 경우, 작은 변화가 코드 전체의 오류에 영향을 끼치기 쉽기 때문에 테스트 코드가 필요하다.
class AccountControllerTest {
	
	
	//의존성 주입
	@Autowired private MockMvc mockMvc;
	
	@Autowired private AccountRepository accountRepository;

	//이메일 보내는지 확인할 목업빈 -> 메일샌더가 메일을 보냈는지 여부 확인
	@MockBean
	JavaMailSender javaMailSender;

	@DisplayName("회원 가입 화면이 보이는지 테스트")
	@Test
	void signUpForm() throws Exception { //아래의 응답이 아닐경우 예외처리
		mockMvc.perform(get("/sign-up")) //회원가입 요청에서
		.andDo(print()) //실제 웹사이트 출력 -> 타임리프이기 때문에
		.andExpect(status().isOk()) //보이는지 -> 즉, 상태가 정상일때 (200일때)
		.andExpect(view().name("account/sign-up")) //뷰의 이름이 account/sign-up이 맞는지
		.andExpect(model().attributeExists("signUpForm"));
		
	}
	@DisplayName("회원 가입 처리 - 입력값 오류")
	@Test
	void signUpSubmit_with_wrong_input() throws Exception {
		mockMvc.perform(post ("/sign-up")
				.param("nickname", "jihun")
				 //이메일이나 패스워드 오류 검증
				.param("email", "email..")
				.param("password", "12345")
				.with(csrf()))		//csrf 토큰에 의한 403 오류 방지
		.andExpect(status().isOk())
		//status OK 확인 후 -> 다시 회원가입페이지 출력
		.andExpect(view().name("account/sign-up"));
	}
	//403출력하는데 -> 스프링 시큐리티에서 authorize만 확인하는데
	//-> CSRF 토큰에 의해 타사이트에서 폼데이터를 보내는 공격을 방지하기 위해
	//타임리프 탬플릿에서 form을 보낼 경우 토큰값을 확인하는데, 토큰값이 다르다는 것을 확인
	//-> csrf 토큰을 삽입한다.

	@DisplayName("회원 가입 처리 - 입력값 정상")
	@Test
	void signUpSubmit_with_correct_input() throws Exception {
		mockMvc.perform(post ("/sign-up")
				.param("nickname", "jihun")
				 //이메일이나 패스워드 오류 검증
				.param("email", "email@gmail.com")
				.param("password", "12345678")
				.with(csrf()))		//csrf 토큰에 의한 403 오류 방지
		.andExpect(status().is3xxRedirection())		//redirection 응답 확인
		.andExpect(view().name("redirect:/"));
		
		//비밀번호 인코딩 확인 -> id를 가져와서 확인 
		Account account = accountRepository.findByEmail("email@gmail.com");
		
		//null이 아닌지 확인 -> assertTrue와 같은 것이 된다.
		assertNotNull(account);
		//입력한 값과 같지 않은지 확인
		assertNotEquals(account.getPassword(), "12345678");
		
		//유저 조회를 위한 의존성 주입 필요-> AccountRepository
		//junit 의존성 추가
//		assertTrue(accountRepository.existsByEmail("email@gmail.com"));
		//-> 패스워드 인코딩 확인하기 위해 직접 Email가져오기 때문에 주석처리
		
		then(javaMailSender).should().send(any(SimpleMailMessage.class));
	}
	
}

 

AccountController

package com.demo.account;

import javax.validation.Valid;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.Errors;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;

import lombok.RequiredArgsConstructor;

@Controller
@RequiredArgsConstructor
public class AccountController {

	private final SignUpFormValidator signUpFormValidator;
	//계정 리포지토리와 메일 샌더는 서비스로 이전
	private final AccountService accountService; 


	@InitBinder("signUpForm")
	public void initBinder(WebDataBinder webDataBinder) {
		webDataBinder.addValidators(signUpFormValidator);
	}

	// sign-up 페이지에 연결된다면
	@GetMapping("/sign-up")
	public String signUpForm(Model model) {
		// model.addAttribute(new SignUpForm()); 생략 가능
		model.addAttribute("signUpForm", new SignUpForm());
		return "account/sign-up";
	}
	// 스프링부트 자동설정에 의해
	// templates에 존재하는 view인 account/sign-up을 리턴한다.

	@PostMapping("/sign-up") // 복합객체는 본디 ModelAttribute로 받지만, 생략가능
	public String signUpSubmit(@Valid @ModelAttribute SignUpForm signUpForm, Errors errors) {
		if (errors.hasErrors()) {
			return "account/sign-up";
		}
		// SignUpForm 도메인에 Valid를 위한 처리 필요

//		@InitBinder를 이용해 대체 
//		signUpFormValidator.validate(signUpForm, errors);
//		if (errors.hasErrors()) {
//			return "account/sign-up";
//		}
		// -> 자동으로 SignUpForm 검증을 한다.

		//이메일 보내는 작업을 컨트롤러가 모르도록 서비스 쪽으로 이전 -> 서비스에서 private으로 수행
		accountService.processNewAccount(signUpForm);
		
		return "redirect:/";

	}

}

 

AccountRepository

package com.demo.account;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.transaction.annotation.Transactional;

import com.demo.domain.Account;

//해당 클래스를 인터페이스로 만든다. -> Account에서, ID타입으로 조회
//기본적으로 write를 안쓰고 읽기만 하게해서 메모리 사용량을 최적화
@Transactional(readOnly = true)
public interface AccountRepository extends JpaRepository<Account, Long> {

	boolean existsByEmail(String email);

	boolean existsByNickname(String nickname);

	Account findByEmail(String email);

}

 

AppConfig

package com.demo.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;

//솔트를 이용해 비밀번호를 인덱싱하기 위해
@Configuration
public class AppConfig {
	
	//빈즈에 명시적 주입
	@Bean
	public PasswordEncoder passwordEncoder() {
		return PasswordEncoderFactories.createDelegatingPasswordEncoder();
		//bcrypt 인코더를 이용해 패스워드를 인코딩한다. -> 의도적으로 인덱싱하는데 시간을 소요하게 되어있다.
	}
	//회원가입 처리할 때, 패스워드인코더를 사용하도록 변경

}
반응형