해당 글은 Spring으로 구현한 프로젝트를 SpringBoot로 리팩토링 하는 과정을 담은 글입니다.
- Index -
[SpringSecurity] SpringSecurity + JWT 로그인 구현_1
[SpringSecurity] SpringSecurity + JWT 로그인 구현_2
[SpringSecurity] SpringSecurity + JWT 로그인 구현_3
[SpringSecurity] SpringSecurity + JWT 회원가입, 테스트 코드 작성
[번외]JwtAuthorizationFilter에서 상속받을 필터에 대한 고민
이번 글에서는 회원가입 로직을 구현하고, test code를 작성하여 회원 가입 및 인증, 인가를 구현할 것이다.
회원가입 로직
1. SignUpDto를 통해 들어온 데이터를 toEntity 메서드를 통해 Member entity로 변환한다.
( Entity를 직접 사용하는 것은 좋지 않은 방법 -> Dto를 통해서 데이터 교환)
2. 변환한 entity를 DB에 저장. 이때 반환값으로 저장된 Member entity를 받는다.
3. 반환받은 entity를 MemberDto의 static method인 toDto를 호출하여 MemberDto로 변환시켜서 반환한다.
SignUpDto -> entity(DB저장, 반환) -> MemberDto(반환)
toEntity toDto
1. 회원 가입 로직 구현
SignUpDto.java
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class SignUpDto {
private String username;
private String password;
private String country;
private String eName;
private String birth;
private String phone;
private String email;
private String insDate;
private String modDate;
public Member toEntity(String encodedPassword, List<String> roles){
return Member.builder()
.username(username)
.password(encodedPassword)
.country(country)
.eName(eName)
.birth(birth)
.phone(phone)
.email(email)
.insDate(insDate)
.modDate(modDate)
.build();
}
}
MemberDto.java
@Getter
@ToString
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class MemberDto {
private Long id;
private String username;
private String country;
private String eName;
private String birth;
private String phone;
private String email;
private String insDate;
private String modDate;
static public MemberDto toDto(Member member){
return MemberDto.builder()
.id(member.getId())
.username(member.getUsername())
.country(member.getCountry())
.eName(member.getEName())
.birth(member.getBirth())
.phone(member.getPhone())
.email(member.getEmail())
.insDate(member.getInsDate())
.modDate(member.getModDate())
.build();
}
public Member toEntity(){
return Member.builder()
.id(id)
.username(username)
.country(country)
.eName(eName)
.birth(birth)
.phone(phone)
.email(email)
.insDate(insDate)
.modDate(modDate)
.build();
}
}
MemberService.java
@Transactional
public MemberDto signUp(SignUpDto signUpDto){
if(memberRepository.existsByUsername(signUpDto.getUsername())){
throw new IllegalArgumentException("이미 사용중인 사용자 이름입니다.");
}
//password 암호화
String encodedPassword = passwordEncoder.encode(signUpDto.getPassword());
List<String> roles = new ArrayList<>();
roles.add("USER");
return MemberDto.toDto(memberRepository.save(signUpDto.toEntity(encodedPassword, roles)));
}
비밀번호를 암호화시켜 .toEntity() 메서드의 인자값으로 전달시켜 DB에 암호화된 값으로 저장한다.
2. SecurityConfig 설정
.requestMatchers("/members/sign-up").permitAll()을 추가하여 회원가입 API에 대한 모든 요청 허가한다.
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtTokenProvider jwtTokenProvider;
@Bean
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
return httpSecurity
.httpBasic(HttpBasicConfigurer::disable)
.csrf(CsrfConfigurer::disable)
.sessionManagement(configurer -> configurer.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(authorize ->
authorize
//해당 API에 대해서는 모든 요청을 허가
.requestMatchers("/members/sign-up").permitAll()
.requestMatchers("/members/sign-in").permitAll()
//USER 권한이 있어야 요청 가능
.requestMatchers("/members/test").hasRole("USER")
)
// JWT 인증을 위하여 직접 구현한 필터를 UsernamePasswordAuthenticationFilter 전에 실행
.addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class).build();
}
@Bean
public PasswordEncoder passwordEncoder() {
// BCrypt Encoder 사용
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
}
3. 회원가입 Controller
memeberService.signUp()을 호출시켜 반환된 MemberDto 객체를 ResponseEntity에 넣어서 반환한다.
MemberController.java
@PostMapping("/sign-up")
public ResponseEntity<MemberDto> signup(@RequestBody SignUpDto signUpDto){
MemberDto savedMemberDto = memberService.signUp(signUpDto);
return ResponseEntity.ok(savedMemberDto);
}
4. 회원가입 Controller 테스트
@SpringBootTest와 TestRestTemplate을 사용하여 Controller 테스트를 할 것이다.
1) @SpringBootTest
- @SpringBootTest는 통합 테스트를 제공하는 기본적인 스프링부트 테스트 어노테이션이다.
- webEmvironment 프로퍼티를 RANDOM_PORT로 지정한다.
2) TestRestTemplate
- HTTP 요청 후 JSON, xml, String과 같은 응답을 받을 수 있는 HTTP 통신 템플릿이다.
- ResponseEntity와 Server to Server 통신하는데 자주 사용한다.
SsjwtApplicationTests.java
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class SsjwtApplicationTests {
@Autowired
DatabaseCleanUp databaseCleanUp;
@Autowired
MemberService memberService;
@Autowired
TestRestTemplate testRestTemplate;
@LocalServerPort
int randomServerPort;
private SignUpDto signUpDto;
@BeforeEach
void beforeEach(){
signUpDto = SignUpDto.builder()
.username("member")
.password("123456")
.eName("member")
.country("USA")
.phone("010-1234-1234")
.birth("2000.01.01")
.email("test")
.insDate("2022-10-10 00:00:00")
.modDate("2022-10-10 00:00:00")
.build();
}
@AfterEach
void afterEach(){
databaseCleanUp.truncateAllEntity();
}
@Test
public void signUpTest() {
//API 요청 설정
String url = "http://localhost:"+randomServerPort + "/members/sign-up";
ResponseEntity<MemberDto> responseEntity = testRestTemplate.postForEntity(url, signUpDto, MemberDto.class);
//응답 검증
assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
MemberDto savedMemberDto = responseEntity.getBody();
assertThat(savedMemberDto.getUsername()).isEqualTo(signUpDto.getUsername());
assertThat(savedMemberDto.getEmail()).isEqualTo(signUpDto.getEmail());
}
}
beforeEach()
- 각 테스트 케이스 전에 실행되어 회원 등록을 위한 SignUpDto 객체 생성한다.
afterEach()
- 각 테스트 케이스 후에 실행되어 DB를 정리하기 위해 모든 엔티티 삭제한다.
signUpTest()
- 회원을 등록하고 저장된 회원 객체의 username, email 값이 예상값과 일치하는지 확인한다.
DatabaseCleanUp.java
@Service
@RequiredArgsConstructor
@Slf4j
public class DatabaseCleanUp implements InitializingBean {
@PersistenceContext
private final EntityManager entityManager;
private List<String> tableNames = new ArrayList<>();
@Override
public void afterPropertiesSet() throws Exception {
tableNames = entityManager.getMetamodel().getEntities().stream()
.filter(entityType -> entityType.getJavaType().getAnnotation(Entity.class) != null)
.map(entityType -> CaseFormat.UPPER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, entityType.getName()))
.collect(Collectors.toList());
}
@Transactional
public void truncateAllEntity() {
entityManager.flush();
// entityManager.createNativeQuery("SET OREIGN_KEY_CHECKS = 0").executeUpdate();
for (String tableName : tableNames) {
System.out.print("tableName:::"+tableName);
entityManager.createNativeQuery("TRUNCATE TABLE " + tableName).executeUpdate();
}
// entityManager.createNativeQuery("SET OREIGN_KEY_CHECKS = 1").executeUpdate();
}
}
entityManager를 통해 entity이름의 DB 테이블 명을 가져와서 TRUNCATE문을 실행한다.
** 실행 결과 **
실행한 결과 회원가입에 성공하였고, MEMBER 테이블에도 회원정보가 저장된 것을 확인할 수 있다.
5. 로그인 Controller 테스트
SsjwtApplicationTests.java
@Test
public void signInTest() {
//memberService.signUp(signUpDto);
SignInDto signInDto = SignInDto.builder()
.username("member")
.password("{bcrypt}$2a$10$C65./t.GhKFt9GOtajviSuXKrZ.ZEEnQu7zdJXKBIqFbanrHITeKm").build();
// 로그인 요청
JwtToken jwtToken = memberService.signIn(signInDto.getUsername(), signInDto.getPassword());
// HttpHeaders 객체 생성 및 토큰 추가
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.setBearerAuth(jwtToken.getAccessToken());
httpHeaders.setContentType(MediaType.APPLICATION_JSON);
// API 요청 설정
String url = "http://localhost:" + randomServerPort + "/members/test";
ResponseEntity<String> responseEntity = testRestTemplate.postForEntity(url, new HttpEntity<>(httpHeaders), String.class);
assertThat(responseEntity.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(responseEntity.getBody()).isEqualTo(signInDto.getUsername());
// assertThat(SecurityUtil.getCurrentUsername()).isEqualTo(signInDto.getUsername()); // -> 테스트 코드에서는 인증을 위한 절차를 거치지 X. SecurityContextHolder 에 인증 정보가 존재하지 않는다.
}
여기서 password의 값은 DB에 암호화된 상태로 저장 되어있는 값으로 로그인 해주었다.(그래야만 테스트 케이스 성공)
signInTest()
1. 회원가입 성공한 username, password를 입력하여 로그인
2. 로그인 성공 시 JwtToken 정상 발급
3. httpHeader에 발급받은 JwtToken 객체의 accessToken 넣고 MediaType JSON으로 지정
4. HttpEntity에 httpHeader를 등록하고, testRestTemplate의 postForEntity 메서드 실행
5. responseEntity의 상태값과 담겨온 객체 내용이 예상한 값과 일치하는지 비교
참고 블로그
[Spring] Spring Security + JWT 로그인 구현하기 - 4
📝 지난 포스팅 ➡︎ Spring Security + JWT 로그인 구현하기 - 3 [Spring] Spring Security + JWT 로그인 구현하기 - 3 📝 지난 포스팅 ➡︎ Spring Security + JWT 로그인 구현하기 - 2 [Spring] Spring Security + JWT 로그인
suddiyo.tistory.com
'Spring > SpringSecurity' 카테고리의 다른 글
JwtAuthorizationFilter에서 상속받을 필터에 대한 고민 (3) | 2024.12.01 |
---|---|
[SpringSecurity] SpringSecurity + JWT 로그인 구현_3 (0) | 2024.03.16 |
[SpringSecurity] SpringSecurity + JWT 로그인 구현_2 (0) | 2024.03.16 |
[SpringSecurity] SpringSecurity + JWT 로그인 구현_1 (0) | 2024.03.11 |