728x90
반응형
암호를 평문으로 저장하지 않기 위해
해시 함수를 이용해 인코딩해서 저장
-> 해싱 알고리즘
스프링 시큐리트가 제공하는 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 인코더를 이용해 패스워드를 인코딩한다. -> 의도적으로 인덱싱하는데 시간을 소요하게 되어있다.
}
//회원가입 처리할 때, 패스워드인코더를 사용하도록 변경
}
728x90
반응형
'Server Programming > Spring Boot Full-Stack Programming' 카테고리의 다른 글
[스프링 풀스택 클론 코딩] 회원가입 이메일 인증 정상/오류 시 분기 (0) | 2022.08.26 |
---|---|
[스프링 풀스택 클론 코딩 - 회원가입] (1-9) 회원가입 이메일 인증 확인 (0) | 2022.08.26 |
[스프링 풀스택 클론 코딩 - 회원가입] (1-7) 회원가입 리팩토링 및 테스트 (0) | 2022.08.26 |
[스프링 풀스택 클론 코딩]목업 테스트 mokito 이용하기 (0) | 2022.08.26 |
[스프링 풀스택 클론 코딩 - 회원가입] (1-6) 회원가입 폼 서브밋 처리 (0) | 2022.08.26 |