[Spring] [면적면적[4]] 초기화면 API 만들기 근데 이제 JWT를 곁들인
북적북적의 백엔드를 클론하고 있다.
build.gradle
plugins {
id 'org.springframework.boot' version '2.5.2'
id 'io.spring.dependency-management' version '1.0.11.RELEASE'
id 'java'
}
group = 'jpa'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'
configurations {
compileOnly {
extendsFrom annotationProcessor
}
all {
exclude group: 'org.springframework.boot', module: 'spring-boot-starter-logging'
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-log4j2'
implementation 'io.jsonwebtoken:jjwt:0.9.1'
implementation 'org.projectlombok:lombok:1.18.18'
runtimeOnly 'mysql:mysql-connector-java'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
}
test {
useJUnitPlatform()
}
src/main/resources/application.yml
secret:
key: secretkeyformyunjukmyunjuk
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/db_name?serverTimezone=UTC&characterEncoding=UTF-8
username: root
password: pwd
jpa:
database-platform: org.hibernate.dialect.MySQL5InnoDBDialect
open-in-view: false
show-sql: true
hibernate:
ddl-auto: none
properties:
hibernate.format_sql: true
요구사항 정리
회원가입
[Request] {email, nickname, password, POST}
1. 입력값 유효성 확인
2. 이메일 중복가입 확인
[Response] {HttpStatus.NO_CONTENT}
로그인
[Request] {email, password, POST}
1. 입력값 유효성 확인
2. 입력한 정보가 DB의 데이터와 일치하는지 확인
[Response] {accessToken, refreshToken, HttpStatus.OK}
비밀번호 찾기
[Request] {email, POST}
1. 입력값 유효성 확인
2. 입력한 정보가 DB의 데이터와 일치하는지 확인
[Response] {password, HttpStatus.OK}
비밀번호 찾기는 당장 하지 않을 것이다. 안할 수도 있고...왜냐면
기존에 유저가 저장했던 비밀번호가 아니라 새로운 비밀번호를 줘야하는데, 그럼 또 그 비밀번호를 수정할 수 있게도 해야 하니까 나중에 마이페이지 구현할 때 하려고 한다.
Spring Security + JWT
일단 더이상 보안에 소홀할 수 없으니 Srping Security와 JWT를 이용해 인증된 사용자를 만들어보자.
Spring Security + JWT의 전반적인 내용
이 블로그들을 참고했다. 내글보다 저기 있는 글들 보는게 더 나을 수도 있겠다.
src/main/java/jpa/myunjuk/infra/jwt/JwtTokenProvider.java
package jpa.myunjuk.infra.jwt;
import io.jsonwebtoken.*;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletRequest;
import java.util.Base64;
import java.util.Date;
import java.util.List;
@RequiredArgsConstructor
@Component
public class JwtTokenProvider {
@Value("${secret.key}")
private String secretKey;
private final long ACCESS_TOKEN_VALID_TIME = 30 * 60 * 1000L; //유효시간 30분
private final long REFRESH_TOKEN_VALID_TIME = 60 * 60 * 24 * 12 * 1000L; //유효시간 2주
private final UserDetailsService userDetailsService;
//객체 초기화. secretKey를 Base64로 인코딩
@PostConstruct
protected void init() {
secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes());
}
//JWT access token 생성
public String createAccessToken(String userPk, List<String> roles) {
Claims claims = Jwts.claims().setSubject(userPk); //JWT payload에 저장되는 정보단위
claims.put("roles", roles); //key - value 쌍으로 저장
Date now = new Date();
return Jwts.builder()
.setClaims(claims) //정보 저장
.setIssuedAt(now) //토큰 발행 시간
.setExpiration(new Date(now.getTime() + ACCESS_TOKEN_VALID_TIME)) //토큰 만료 시간
.signWith(SignatureAlgorithm.HS256, secretKey) //사용할 암호화 알고리즘
.compact();
}
//JWT refresh token 생성
public String createRefreshToken(String value){
Claims claims = Jwts.claims();
claims.put("value", value);
Date now = new Date();
return Jwts.builder()
.setClaims(claims) //정보 저장
.setIssuedAt(now) //토큰 발행 시간
.setExpiration(new Date(now.getTime() + REFRESH_TOKEN_VALID_TIME)) //토큰 만료 시간
.signWith(SignatureAlgorithm.HS256, secretKey) //사용할 암호화 알고리즘
.compact();
}
//JWT 토큰에서 인증 정보 조회
public Authentication getAuthentication(String token) {
UserDetails userDetails = userDetailsService.loadUserByUsername(this.getUserPk(token));
return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities());
}
//토큰에서 회원 정보 얻어내기
private String getUserPk(String token) {
return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject();
}
//Request의 Header에서 token 값 가져오기. "X-AUTH-TOKEN" : "TOKEN값"
public String resolveToken(HttpServletRequest request) {
return request.getHeader("X-AUTH-TOKEN");
}
//토큰의 유효성과 만료일자 확인
public boolean validateToken(String jwtToken) {
try {
Jws<Claims> claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwtToken);
return !claims.getBody().getExpiration().before(new Date()); //만료일자
} catch (Exception e) { //유효성
return false;
}
}
//refresh token 정보 얻어내기
public Claims getClaimsFromJwtToken(String jwtToken) throws JwtException {
return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwtToken).getBody();
}
}
이건 JWT를 생성하고 검증하는 컴포넌트다.
@Value("${secret.key}")
private String secretKey;
private final long ACCESS_TOKEN_VALID_TIME = 30 * 60 * 1000L; //유효시간 30분
private final long REFRESH_TOKEN_VALID_TIME = 60 * 60 * 24 * 12 * 1000L; //유효시간 2주
private final UserDetailsService userDetailsService;
secretKey는 말그대로 비밀이니까 application.yml에 잘 숨겨서 깃이그노어 처리했다. 저 위에 있는 yml파일에 써있는 key도 현재 지금 내 파일에 써있는 키와 다른 키다.
암튼 access token과 refresh 토큰의 유효시간도 설정한다.
//JWT access token 생성
public String createAccessToken(String userPk, List<String> roles) {
Claims claims = Jwts.claims().setSubject(userPk); //JWT payload에 저장되는 정보단위
claims.put("roles", roles); //key - value 쌍으로 저장
Date now = new Date();
return Jwts.builder()
.setClaims(claims) //정보 저장
.setIssuedAt(now) //토큰 발행 시간
.setExpiration(new Date(now.getTime() + ACCESS_TOKEN_VALID_TIME)) //토큰 만료 시간
.signWith(SignatureAlgorithm.HS256, secretKey) //사용할 암호화 알고리즘
.compact();
}
access token을 생성하는 부분이다. 인자로 들어가는 userPk가 내 경우엔 email인데...엄밀히 말하면 이건 pk가 아니다. 근데 String 타입만 가능하길래 unique 옵션 걸어놓은 email을 쓰는 것이다.
//JWT refresh token 생성
public String createRefreshToken(String value){
Claims claims = Jwts.claims();
claims.put("value", value);
Date now = new Date();
return Jwts.builder()
.setClaims(claims) //정보 저장
.setIssuedAt(now) //토큰 발행 시간
.setExpiration(new Date(now.getTime() + REFRESH_TOKEN_VALID_TIME)) //토큰 만료 시간
.signWith(SignatureAlgorithm.HS256, secretKey) //사용할 암호화 알고리즘
.compact();
}
refresh token을 생성하는 부분이다. 인자로 들어가는 value는 UUID 랜덤스트링을 변형한 값이 들어간다.
//JWT 토큰에서 인증 정보 조회
public Authentication getAuthentication(String token) {
UserDetails userDetails = userDetailsService.loadUserByUsername(this.getUserPk(token));
return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities());
}
//토큰에서 회원 정보 얻어내기
private String getUserPk(String token) {
return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(token).getBody().getSubject();
}
getUserPk로 access token에서 pk(email)을 얻어내고 그 값을 기반으로 유저를 찾아내 인증 정보를 조회한다.
(사실 잘 이해 못함)
//Request의 Header에서 token 값 가져오기. "X-AUTH-TOKEN" : "TOKEN값"
public String resolveToken(HttpServletRequest request) {
return request.getHeader("X-AUTH-TOKEN");
}
나중에 포스트맨에서 토큰을 통해 유저를 찾으려면 X-AUTH-TOKEN이라고 이름붙여서 가져오면 된다.
//토큰의 유효성과 만료일자 확인
public boolean validateToken(String jwtToken) {
try {
Jws<Claims> claims = Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwtToken);
return !claims.getBody().getExpiration().before(new Date()); //만료일자
} catch (Exception e) { //유효성
return false;
}
}
토큰엔 유효기간이 있으니 토큰의 유효성도 확인해야 하고
//refresh token 정보 얻어내기
public Claims getClaimsFromJwtToken(String jwtToken) throws JwtException {
return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwtToken).getBody();
}
이건 getUserPk랑 비슷한 것이다. 그건 access token에서 유저를 찾아냈고 이건 refresh에서 찾아내고
(아닐 수도 있음)
src/main/java/jpa/myunjuk/infra/jwt/JwtAuthenticationFilter.java
package jpa.myunjuk.infra.jwt;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.GenericFilterBean;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends GenericFilterBean {
private final JwtTokenProvider jwtTokenProvider;
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
//헤더에서 JWT 받아오기
String token = jwtTokenProvider.resolveToken((HttpServletRequest) request);
//유효한 토큰인지 확인
if (token != null && jwtTokenProvider.validateToken(token)) {
//토큰이 유효하면 토큰으로부터 유저 정보 반환
Authentication authentication = jwtTokenProvider.getAuthentication(token);
//SecurityContext에 Authentication 객체를 저장
SecurityContextHolder.getContext().setAuthentication(authentication);
}
chain.doFilter(request, response);
}
}
JwtProvider를 실제로 사용하면서 인증도 하고 유저정보도 받아오는 필터라고 하네요
src/main/java/jpa/myunjuk/infra/config/WebSecurityConfig.java
package jpa.myunjuk.infra.config;
import jpa.myunjuk.infra.jwt.JwtAuthenticationFilter;
import jpa.myunjuk.infra.jwt.JwtTokenProvider;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@RequiredArgsConstructor
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private final JwtTokenProvider jwtTokenProvider;
//암호화에 필요한 PasswordEncoder를 Bean 등록
@Bean
public PasswordEncoder passwordEncoder(){
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
//authenticationManager를 Bean 등록
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception{
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity.httpBasic().disable() //rest api만 고려하면 해제해도 되는듯?
.csrf().disable() //csrf 보안 토큰 diable 처리
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) //토큰 기반 인증이라 세션은 사용하지 않음
.and()
.authorizeRequests() //요청에 대한 권한 체크
.antMatchers("/admin/**").hasRole("ADMIN")
.antMatchers("/user/**").hasRole("USER")
.antMatchers("/**").permitAll() //그외 나머지 요청은 누구나 접근 가능
.and()
.addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider),
UsernamePasswordAuthenticationFilter.class); //JwtAuthenticationFilter를 UsernamePasswordAuthenticationFilter 전에 넣음
}
}
최종적으로 만든 필터를 여기에 등록해야 쓸 수 있나보다.
//JWT 토큰에서 인증 정보 조회
public Authentication getAuthentication(String token) {
UserDetails userDetails = userDetailsService.loadUserByUsername(this.getUserPk(token));
return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities());
}
한편 앞서 JwtTokenProvider 파일에서 이런 메소드를 짰는데
src/main/java/jpa/myunjuk/module/service/CustomUserDetailService.java
package jpa.myunjuk.module.service;
import jpa.myunjuk.infra.exception.NoSuchDataException;
import jpa.myunjuk.module.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
@RequiredArgsConstructor
@Service
public class CustomUserDetailService implements UserDetailsService {
private final UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return userRepository.findByEmail(username)
.orElseThrow(() -> new NoSuchDataException("userId = " + username));
}
}
이렇게 만들어줘야 한다.
근데 또 보면 findByEmail이 아직 없는데...
src/main/java/jpa/myunjuk/module/repository/UserRepository.java
package jpa.myunjuk.module.repository;
import jpa.myunjuk.module.model.domain.User;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
}
스프링데이터 jpa를 활용해서 만든다.
근데 다시 또 저 위에 보면 NoSuchDataException이란게 있다. 저건 뭐냐면
src/main/java/jpa/myunjuk/infra/exception/NoSuchDataException.java
package jpa.myunjuk.infra.exception;
public class NoSuchDataException extends CustomRuntimeException{
public NoSuchDataException(String msg){
super(msg);
name = "NoSuchDataException";
}
}
내가 만든 익셉션이다.
src/main/java/jpa/myunjuk/infra/exception/CustomRuntimeException.java
package jpa.myunjuk.infra.exception;
import lombok.Getter;
public class CustomRuntimeException extends RuntimeException{
@Getter
String name;
public CustomRuntimeException(String msg){
super(msg);
}
}
이 친구를 상속받은거고
src/main/java/jpa/myunjuk/infra/exception/GlobalExceptionHandler.java
package jpa.myunjuk.infra.exception;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import java.util.HashMap;
import java.util.Map;
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler({
NoSuchDataException.class})
public ResponseEntity<?> handleRuntimeExceptions(final CustomRuntimeException e) {
return ResponseEntity.badRequest().body(errorMsg(e.getName(), e.getMessage()));
}
private Map<String, String> errorMsg(String name, String msg) {
Map<String, String> error = new HashMap<>();
error.put(name, msg);
return error;
}
}
이렇게 핸들링한다.
참으로 멀리왔는데...
src/main/java/jpa/myunjuk/module/model/domain/User.java
package jpa.myunjuk.module.model.domain;
import com.sun.istack.NotNull;
import lombok.*;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Builder
public class User implements UserDetails {
@Id
@Column(name = "user_sn")
@GeneratedValue
private Long id;
@Column(unique = true)
@NotNull
private String email;
@Column(length = 300)
@NotNull
private String password;
@NotNull
private String nickname;
@Column(name = "user_img")
private String img;
@Builder.Default
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL)
private List<Book> books = new ArrayList<>();
@Builder.Default
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL)
private List<UserCharacter> userCharacters = new ArrayList<>();
@Setter
private String refreshTokenValue;
@ElementCollection(fetch = FetchType.EAGER)
@Builder.Default
private List<String> roles = new ArrayList<>();
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return this.roles.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
}
@Override
public String getUsername() {
return email;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
그래서 이것들을 사용하기 위해 User domian이 UserDetails를 implement한 것이다.
getUsername()으로 넘어가는게 email인게 맘에 들지 않지만...String이어야 하니 어쩔 수 없다.
그럼 이제 준비를 다 했으니 요구사항을 구현하도록 하자.
근데 이대로 프론트와 연결을 시도하면 cors에러가 뜰 것이다.
링크<- 해결한 글
회원가입
1. 회원가입시엔 이메일, 비밀번호, 닉네임이 필요하다.
2. 이미 가입된 이메일로 가입할 수 없다.
3. 회원가입을 하면 귀여운 기본 캐릭터를 하나 준다.(뭔지 궁금하면 어플 깔아보세요)
src/main/java/jpa/myunjuk/module/model/dto/UserDtos.java
package jpa.myunjuk.module.model.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotNull;
public class UserDtos {
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@NotNull
public static class UserSignUpReqDto {
@Email
private String email;
private String password;
private String nickname;
}
}
회원가입할 사용자의 정보를 저장할 dto를 만들었다. 모두 @NotNull을 달아주고 email에는 추가로 @Email도 달아줬다.
그럼 얘네들에 대한 validation이 잘 작동하는지 확인해야 한다.
inner class로 만든 이유는 양이 점점...많아지기 때문인데 그래도 중복이나 그런게 많아서...나중에 리팩터링을 해야겠다.
src/main/java/jpa/myunjuk/infra/exception/GlobalExceptionHandler.java
package jpa.myunjuk.infra.exception;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import java.util.HashMap;
import java.util.Map;
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler({
NoSuchDataException.class})
public ResponseEntity<?> handleRuntimeExceptions(final CustomRuntimeException e) {
return ResponseEntity.badRequest().body(errorMsg(e.getName(), e.getMessage()));
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<?> handleValidationExceptions(MethodArgumentNotValidException e) {
Map<String, String> errors = new HashMap<>();
e.getBindingResult().getAllErrors()
.forEach(c -> errors.put(((FieldError) c).getField(), c.getDefaultMessage()));
return ResponseEntity.badRequest().body(errors);
}
private Map<String, String> errorMsg(String name, String msg) {
Map<String, String> error = new HashMap<>();
error.put(name, msg);
return error;
}
}
그전에 validation에 대한 익셉션을 처리해주자.
참고 블로그 여길 참고했다.
src/test/java/jpa/myunjuk/module/model/dto/UserSignUpReqDtoTest.java
package jpa.myunjuk.module.model.dto;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
import java.util.Set;
import static jpa.myunjuk.module.model.dto.UserDtos.*;
import static org.assertj.core.api.Assertions.assertThat;
@DisplayName("User Sign-Up Validation Test")
class UserSignUpReqDtoTest {
public static ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
public static Validator validator = factory.getValidator();
@BeforeAll
public static void init() {
factory = Validation.buildDefaultValidatorFactory();
validator = factory.getValidator();
}
@Test
@DisplayName("User Sign-Up Request | Fail : Email Null")
void emptyEmail(){
UserSignUpReqDto user = UserSignUpReqDto.builder()
.password("1234")
.nickname("hello")
.build();
Set<ConstraintViolation<UserSignUpReqDto>> violations = validator.validate(user);
assertThat(violations).isEmpty();
}
@Test
@DisplayName("User Sign-Up Request | Fail : Invalid Email Format")
void invalidEmail(){
UserSignUpReqDto user = UserSignUpReqDto.builder()
.email("wrong_format")
.password("1234")
.nickname("hello")
.build();
Set<ConstraintViolation<UserSignUpReqDto>> violations = validator.validate(user);
violations.forEach(error -> {
assertThat(error.getMessage()).isEqualTo("올바른 형식의 이메일 주소여야 합니다");
});
}
@Test
@DisplayName("User Sign-Up Request | Fail : Password Null")
void emptyPassword(){
UserSignUpReqDto user = UserSignUpReqDto.builder()
.email("h@gmail.com")
.nickname("hello")
.build();
Set<ConstraintViolation<UserSignUpReqDto>> violations = validator.validate(user);
assertThat(violations).isEmpty();
}
@Test
@DisplayName("User Sign-Up Request | Fail : Nickname Null")
void emptyNickname(){
UserSignUpReqDto user = UserSignUpReqDto.builder()
.email("h@gmail.com")
.password("1234")
.build();
Set<ConstraintViolation<UserSignUpReqDto>> violations = validator.validate(user);
assertThat(violations).isEmpty();
}
}
3개의 값에 대한 널체크와 email에 대한 체크 총 4개의 테스트이다.
대충 읽어보면 어떻게 작동하는 코드인지 알듯하니 설명은 생략한다.
그럼 이제 회원가입 코드를 작성하자
src/main/java/jpa/myunjuk/module/service/UserService.java
package jpa.myunjuk.module.service;
import jpa.myunjuk.infra.exception.DuplicateUserException;
import jpa.myunjuk.infra.exception.InvalidReqParamException;
import jpa.myunjuk.infra.exception.NoSuchDataException;
import jpa.myunjuk.infra.jwt.JwtTokenProvider;
import jpa.myunjuk.module.model.domain.Characters;
import jpa.myunjuk.module.model.domain.User;
import jpa.myunjuk.module.model.domain.UserCharacter;
import jpa.myunjuk.module.model.dto.UserDtos;
import jpa.myunjuk.module.repository.CharactersRepository;
import jpa.myunjuk.module.repository.UserCharacterRepository;
import jpa.myunjuk.module.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import static jpa.myunjuk.module.model.dto.JwtDtos.*;
@Service
@RequiredArgsConstructor
public class UserService {
private final PasswordEncoder passwordEncoder;
private final JwtTokenProvider jwtTokenProvider;
private final UserRepository userRepository;
private final UserCharacterRepository userCharacterRepository;
private final CharactersRepository charactersRepository;
/**
* signUp
*
* @param userSignUpReqDto
* @return User
*/
public User signUp(UserDtos.UserSignUpReqDto userSignUpReqDto) {
checkDuplicateUser(userSignUpReqDto.getEmail()); //중복 가입 체크
Characters characters = charactersRepository.findById(1L).orElseThrow(() -> new NoSuchDataException("Missing default character"));
User user = userRepository.save(buildUserFromUserJoinDto(userSignUpReqDto));
userCharacterRepository.save(UserCharacter.builder() //회원가입시 주어지는 기본캐릭터
.user(user)
.characters(characters)
.achieve(LocalDate.now())
.representation(true)
.build());
return user;
}
private void checkDuplicateUser(String email) {
userRepository.findByEmail(email)
.ifPresent(param -> {
throw new DuplicateUserException("email = " + email);
});
}
private User buildUserFromUserJoinDto(UserDtos.UserSignUpReqDto userSignUpReqDto) {
return User.builder()
.email(userSignUpReqDto.getEmail())
.password(passwordEncoder.encode(userSignUpReqDto.getPassword()))
.nickname(userSignUpReqDto.getNickname())
.roles(Collections.singletonList("ROLE_USER"))
.build();
}
}
하나하나 뜯어보자
/**
* signUp
*
* @param userSignUpReqDto
* @return User
*/
public User signUp(UserDtos.UserSignUpReqDto userSignUpReqDto) {
checkDuplicateUser(userSignUpReqDto.getEmail()); //중복 가입 체크
Characters characters = charactersRepository.findById(1L).orElseThrow(() -> new NoSuchDataException("Missing default character"));
User user = userRepository.save(buildUserFromUserJoinDto(userSignUpReqDto));
userCharacterRepository.save(UserCharacter.builder() //회원가입시 주어지는 기본캐릭터
.user(user)
.characters(characters)
.achieve(LocalDate.now())
.representation(true)
.build());
return user;
}
일단 입력받은 사용자의 이메일에 대해 중복 가입 여부를 체크한다.
private void checkDuplicateUser(String email) {
userRepository.findByEmail(email)
.ifPresent(param -> {
throw new DuplicateUserException("email = " + email);
});
}
만약 존재하면 DuplicateUserException을 던지는데
src/main/java/jpa/myunjuk/infra/exception/DuplicateUserException.java
package jpa.myunjuk.infra.exception;
public class DuplicateUserException extends CustomRuntimeException{
public DuplicateUserException(String msg){
super(msg);
name = "DuplicateUserException";
}
}
그냥 아까처럼 이렇게 만들고
src/main/java/jpa/myunjuk/infra/exception/GlobalExceptionHandler.java
package jpa.myunjuk.infra.exception;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import java.util.HashMap;
import java.util.Map;
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler({
DuplicateUserException.class,
NoSuchDataException.class})
public ResponseEntity<?> handleRuntimeExceptions(final CustomRuntimeException e) {
return ResponseEntity.badRequest().body(errorMsg(e.getName(), e.getMessage()));
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<?> handleValidationExceptions(MethodArgumentNotValidException e) {
Map<String, String> errors = new HashMap<>();
e.getBindingResult().getAllErrors()
.forEach(c -> errors.put(((FieldError) c).getField(), c.getDefaultMessage()));
return ResponseEntity.badRequest().body(errors);
}
private Map<String, String> errorMsg(String name, String msg) {
Map<String, String> error = new HashMap<>();
error.put(name, msg);
return error;
}
}
등록하면 된다.
다시 서비스로 돌아와서
/**
* signUp
*
* @param userSignUpReqDto
* @return User
*/
public User signUp(UserDtos.UserSignUpReqDto userSignUpReqDto) {
checkDuplicateUser(userSignUpReqDto.getEmail()); //중복 가입 체크
Characters characters = charactersRepository.findById(1L).orElseThrow(() -> new NoSuchDataException("Missing default character"));
User user = userRepository.save(buildUserFromUserJoinDto(userSignUpReqDto));
userCharacterRepository.save(UserCharacter.builder() //회원가입시 주어지는 기본캐릭터
.user(user)
.characters(characters)
.achieve(LocalDate.now())
.representation(true)
.build());
return user;
}
중복 가입이 아님을 확인하면 유저를 저장하기 전에 기본캐릭터부터 찾아본다.
만약 없어서 NoSuchDataException이 터지면 내가 데베에 캐릭터들을 넣지 않은 것이다.
그러고 나서 드디어 userRepository.save로 저장하는데
private User buildUserFromUserJoinDto(UserDtos.UserSignUpReqDto userSignUpReqDto) {
return User.builder()
.email(userSignUpReqDto.getEmail())
.password(passwordEncoder.encode(userSignUpReqDto.getPassword()))
.nickname(userSignUpReqDto.getNickname())
.roles(Collections.singletonList("ROLE_USER"))
.build();
}
비밀번호는 암호화하고, roles를 추가해서 저장한다.
userCharacterRepository.save(UserCharacter.builder() //회원가입시 주어지는 기본캐릭터
.user(user)
.characters(characters)
.achieve(LocalDate.now())
.representation(true)
.build());
return user;
마지막으로 저장된 유저에 대해 기본캐릭터도 넣어준 다음 리턴하면 되는데 주의할 것이 있다.
난 User와 UserCharacter를 양방향 매핑했다. 그래서 UserCharacter에 user를 넣을 때 user에도 넣어줘야 하는데...
src/main/java/jpa/myunjuk/module/model/domain/UserCharacter.java
package jpa.myunjuk.module.model.domain;
import com.sun.istack.NotNull;
import jpa.myunjuk.infra.converter.BooleanToYNConverter;
import lombok.*;
import org.springframework.util.Assert;
import javax.persistence.*;
import java.time.LocalDate;
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class UserCharacter {
@Id
@Column(name = "uc_sn")
@GeneratedValue
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_sn")
@NotNull
private User user;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "character_sn")
@NotNull
private Characters characters;
@NotNull
private LocalDate achieve;
@Convert(converter = BooleanToYNConverter.class)
@NotNull
private boolean representation;
@Builder
public UserCharacter(Long id, User user, Characters characters, LocalDate achieve, boolean representation) {
Assert.notNull(user, "User must not be null");
Assert.notNull(characters, "Characters must not be null");
Assert.notNull(achieve, "Achieve must not be null");
this.id = id;
this.user = user;
this.characters = characters;
this.achieve = achieve;
this.representation = representation;
if (!user.getUserCharacters().contains(this))
user.getUserCharacters().add(this);
}
}
그래서 UserCharacter 엔티티의 빌더 안에 이렇게 user->character 정보도 입력되도록 만들어 준다.
아무튼 이러면 끝인데, 마지막에 dto없이 user를 그대로 리턴하는게 찜찜하게 느껴질 수도 있다.
src/main/java/jpa/myunjuk/module/controller/UserController.java
package jpa.myunjuk.module.controller;
import jpa.myunjuk.module.model.domain.User;
import jpa.myunjuk.module.service.UserService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import static jpa.myunjuk.module.model.dto.JwtDtos.*;
import static jpa.myunjuk.module.model.dto.UserDtos.*;
@Slf4j
@RequiredArgsConstructor
@RestController
public class UserController {
private final UserService userService;
/**
* 회원가입
* localhost:8080/sign-up
*
* @param userSignUpReqDto
* @return ResponseEntity
*/
@PostMapping("/sign-up")
public ResponseEntity<?> join(@Valid @RequestBody UserSignUpReqDto userSignUpReqDto) {
log.info("[Request] sign-up");
userService.signUp(userSignUpReqDto);
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}
}
어차피 response에 안들어가서 괜찮다.
그럼 테스트를 하자
src/test/java/jpa/myunjuk/module/service/UserServiceTest.java
package jpa.myunjuk.module.service;
import jpa.myunjuk.module.model.domain.User;
import jpa.myunjuk.module.repository.UserRepository;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;
import static jpa.myunjuk.module.model.dto.UserDtos.*;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
@DisplayName("User Service Test")
@Transactional
class UserServiceTest {
@Autowired
private UserRepository userRepository;
@Autowired
private UserService userService;
@Test
@DisplayName("User Sign-Up | Success")
void singUpSuccess() throws Exception {
UserSignUpReqDto userSignUpReqDto = UserSignUpReqDto.builder()
.email("new@new.com")
.password("1234")
.nickname("hello")
.build();
User user = userService.signUp(userSignUpReqDto);
//양방향 제대로 됐는지 확인
assertEquals(user.getUserCharacters().size(), 1);
assertTrue(user.getUserCharacters().get(0).isRepresentation());
}
}
굳이 설명할 필요는 없을 것 같다.
src/test/java/jpa/myunjuk/module/controller/UserControllerTest.java
package jpa.myunjuk.module.controller;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.*;
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.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.transaction.annotation.Transactional;
import java.util.HashMap;
import java.util.Map;
import static jpa.myunjuk.module.model.dto.UserDtos.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@Transactional
@SpringBootTest
@DisplayName("User Controller Test")
@AutoConfigureMockMvc(addFilters = false)
class UserControllerTest {
@Autowired private MockMvc mockMvc;
@Autowired private ObjectMapper objectMapper;
@Test
@DisplayName("User Sign-Up | Success")
void signUpSuccess() throws Exception {
UserSignUpReqDto userSignUpReqDto = UserSignUpReqDto.builder()
.email("new@new.com")
.password("1234")
.nickname("hello")
.build();
String jsonString = objectMapper.writeValueAsString(userSignUpReqDto);
mockMvc.perform(post("/sign-up")
.contentType(MediaType.APPLICATION_JSON)
.content(jsonString))
.andExpect(status().isNoContent());
}
}
컨트롤러에서도 테스트해보자
중복 유저를 테스트하기 전에...지난 며칠 시행착오를 겪으면서 깨달은 것인데 DB에 테스트 유저를 넣어야겠다 싶었다.
그래서 회원가입이 잘되는 것도 확인했으니
테스트 유저를 넣을 것이다.
예상한대로 잘 나왔다.
그럼 아까 그 컨트롤러 테스트에
@Test
@DisplayName("User Sign-Up | Fail : Duplicate User")
void signUpFailDuplicate() throws Exception {
UserSignUpReqDto userSignUpReqDto = UserSignUpReqDto.builder()
.email("test@test.com")
.password("1234")
.nickname("hello")
.build();
String jsonString = objectMapper.writeValueAsString(userSignUpReqDto);
Map<String, String> error = new HashMap<>();
error.put("DuplicateUserException", "email = test@test.com");
String response = objectMapper.writeValueAsString(error);
mockMvc.perform(post("/sign-up")
.contentType(MediaType.APPLICATION_JSON)
.content(jsonString))
.andExpect(status().isBadRequest())
.andExpect(content().string(response));
}
이걸 추가한다.
BadRequest가 뜨는지, 그리고 에러 메세지가 예상한바와 같은지 확인할 것이다.
이렇게 회원가입이 끝났다.
로그인
로그인을 할 때 중요한 것은 다음과 같다.
1. 이메일/비밀번호 일치
2. Access token과 refresh token 발행
먼저 dto를 만들면
src/main/java/jpa/myunjuk/module/model/dto/UserDtos.java
package jpa.myunjuk.module.model.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotNull;
public class UserDtos {
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@NotNull
public static class UserSignUpReqDto {
@Email
private String email;
private String password;
private String nickname;
}
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@NotNull
public static class UserSignInReqDto {
@Email
private String email;
private String password;
}
}
거의 중복이라 테스트는 생략한다.
src/main/java/jpa/myunjuk/module/service/UserService.java
package jpa.myunjuk.module.service;
import jpa.myunjuk.infra.exception.DuplicateUserException;
import jpa.myunjuk.infra.exception.InvalidReqParamException;
import jpa.myunjuk.infra.exception.NoSuchDataException;
import jpa.myunjuk.infra.jwt.JwtTokenProvider;
import jpa.myunjuk.module.model.domain.Characters;
import jpa.myunjuk.module.model.domain.User;
import jpa.myunjuk.module.model.domain.UserCharacter;
import jpa.myunjuk.module.model.dto.UserDtos;
import jpa.myunjuk.module.repository.CharactersRepository;
import jpa.myunjuk.module.repository.UserCharacterRepository;
import jpa.myunjuk.module.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.util.Collections;
import java.util.List;
import java.util.UUID;
import static jpa.myunjuk.module.model.dto.JwtDtos.*;
@Service
@RequiredArgsConstructor
public class UserService {
private final PasswordEncoder passwordEncoder;
private final JwtTokenProvider jwtTokenProvider;
private final UserRepository userRepository;
private final UserCharacterRepository userCharacterRepository;
private final CharactersRepository charactersRepository;
/**
* signUp
*
* @param userSignUpReqDto
* @return User
*/
public User signUp(UserDtos.UserSignUpReqDto userSignUpReqDto) {
checkDuplicateUser(userSignUpReqDto.getEmail()); //중복 가입 체크
Characters characters = charactersRepository.findById(1L).orElseThrow(() -> new NoSuchDataException("Missing default character"));
User user = userRepository.save(buildUserFromUserJoinDto(userSignUpReqDto));
userCharacterRepository.save(UserCharacter.builder() //회원가입시 주어지는 기본캐릭터
.user(user)
.characters(characters)
.achieve(LocalDate.now())
.representation(true)
.build());
return user;
}
private void checkDuplicateUser(String email) {
userRepository.findByEmail(email)
.ifPresent(param -> {
throw new DuplicateUserException("email = " + email);
});
}
/**
* signIn
*
* @param userSignInReqDto
* @return JwtDto
*/
public JwtDto signIn(UserDtos.UserSignInReqDto userSignInReqDto) {
User user = userRepository.findByEmail(userSignInReqDto.getEmail())
.orElseThrow(() -> new NoSuchDataException("email = " + userSignInReqDto.getEmail()));
if (!passwordEncoder.matches(userSignInReqDto.getPassword(), user.getPassword()))
throw new NoSuchDataException("password = " + userSignInReqDto.getPassword());
String[] jwtTokens = createJwtTokens(user, user.getRoles());
return buildJwtDto(user, jwtTokens);
}
private String[] createJwtTokens(User user, List<String> roles) {
String accessToken = jwtTokenProvider.createAccessToken(user.getUsername(), roles);
String refreshTokenValue = UUID.randomUUID().toString().replace("-", "");
saveRefreshTokenValue(user, refreshTokenValue);
String refreshToken = jwtTokenProvider.createRefreshToken(refreshTokenValue);
return new String[]{accessToken, refreshToken};
}
private void saveRefreshTokenValue(User user, String refreshToken) { //사용자의 refreshToken 데베에 저장
user.setRefreshTokenValue(refreshToken);
userRepository.save(user);
}
/**
* refreshToken
*
* @param jwtRefreshReqDto
* @return JwtDto
*/
public JwtDto refreshUserTokens(JwtRefreshReqDto jwtRefreshReqDto) {
User user = userRepository.findByEmail(jwtRefreshReqDto.getEmail())
.orElseThrow(() -> new NoSuchDataException("email = " + jwtRefreshReqDto.getEmail()));
checkIfRefreshTokenValid(user.getRefreshTokenValue(), jwtRefreshReqDto.getRefreshToken()); //유저의 실제 토큰과 클라이언트에서 넘어온 토큰
String[] jwtTokens = createJwtTokens(user, user.getRoles());
return buildJwtDto(user, jwtTokens);
}
private void checkIfRefreshTokenValid(String requiredValue, String givenRefreshToken) { //유효한지 확인
String givenValue = String.valueOf(jwtTokenProvider.getClaimsFromJwtToken(givenRefreshToken).get("value"));
if (!givenValue.equals(requiredValue)) {
throw new InvalidReqParamException("Invalid refreshToken");
}
}
private User buildUserFromUserJoinDto(UserDtos.UserSignUpReqDto userSignUpReqDto) {
return User.builder()
.email(userSignUpReqDto.getEmail())
.password(passwordEncoder.encode(userSignUpReqDto.getPassword()))
.nickname(userSignUpReqDto.getNickname())
.roles(Collections.singletonList("ROLE_USER"))
.build();
}
private JwtDto buildJwtDto(User user, String[] jwtTokens) {
return JwtDto.builder()
.userId(user.getId())
.accessToken(jwtTokens[0])
.refreshToken(jwtTokens[1])
.build();
}
}
그리고 이번엔 로그인이랑 같이 토큰을 재발행 해주는 api도 같이 만든다.
/**
* signIn
*
* @param userSignInReqDto
* @return JwtDto
*/
public JwtDto signIn(UserDtos.UserSignInReqDto userSignInReqDto) {
User user = userRepository.findByEmail(userSignInReqDto.getEmail())
.orElseThrow(() -> new NoSuchDataException("email = " + userSignInReqDto.getEmail()));
if (!passwordEncoder.matches(userSignInReqDto.getPassword(), user.getPassword()))
throw new NoSuchDataException("password = " + userSignInReqDto.getPassword());
String[] jwtTokens = createJwtTokens(user, user.getRoles());
return buildJwtDto(user, jwtTokens);
}
먼저 이메일에 대한 유효성 체크를 하고, 비밀번호에 대한 유효성 체크를 한다.
그리고 나서 createJwtTokens 메소드로 토큰을 발행한다.
private String[] createJwtTokens(User user, List<String> roles) {
String accessToken = jwtTokenProvider.createAccessToken(user.getUsername(), roles);
String refreshTokenValue = UUID.randomUUID().toString().replace("-", "");
saveRefreshTokenValue(user, refreshTokenValue);
String refreshToken = jwtTokenProvider.createRefreshToken(refreshTokenValue);
return new String[]{accessToken, refreshToken};
}
private void saveRefreshTokenValue(User user, String refreshToken) { //사용자의 refreshToken 데베에 저장
user.setRefreshTokenValue(refreshToken);
userRepository.save(user);
}
access token은 jwtTokenProvider가 주는걸 받아오고, refresh token은 UUID 랜덤 스트링을 만들어서 역시나 jwtTokenProvider가 주는걸 받아온다.
private JwtDto buildJwtDto(User user, String[] jwtTokens) {
return JwtDto.builder()
.userId(user.getId())
.accessToken(jwtTokens[0])
.refreshToken(jwtTokens[1])
.build();
}
토큰들은 사용자 정보와 함께 dto로 담는데
src/main/java/jpa/myunjuk/module/model/dto/JwtDtos.java
package jpa.myunjuk.module.model.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import javax.validation.constraints.NotNull;
public class JwtDtos {
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@NotNull
public static class JwtRefreshReqDto {
private String email;
private String refreshToken;
}
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public static class JwtDto {
private Long userId;
private String accessToken;
private String refreshToken;
}
}
이렇게 생겼다. JwtRefreshReqDto는 refreshTokens가 쓸 것인데 이건 로그인이랑 유사하니 설명하지 않는다.
src/main/java/jpa/myunjuk/module/controller/UserController.java
package jpa.myunjuk.module.controller;
import jpa.myunjuk.module.model.domain.User;
import jpa.myunjuk.module.service.UserService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import static jpa.myunjuk.module.model.dto.JwtDtos.*;
import static jpa.myunjuk.module.model.dto.UserDtos.*;
@Slf4j
@RequiredArgsConstructor
@RestController
public class UserController {
private final UserService userService;
/**
* 로그인
* localhost:8080/sign-in
*
* @param userSignInReqDto
* @return ResponseEntity
*/
@PostMapping("/sign-in")
public ResponseEntity<?> signIn(@Valid @RequestBody UserSignInReqDto userSignInReqDto) {
log.info("[Request] sign-in " + userSignInReqDto.toString());
return new ResponseEntity<>(userService.signIn(userSignInReqDto), HttpStatus.OK);
}
/**
* 토큰 재발행
* localhost:8080/refresh-tokens
*
* @param jwtRefreshReqDto
* @return ResponseEntity
*/
@PostMapping("/refresh-tokens")
public ResponseEntity<?> refreshUserToken(@Valid @RequestBody JwtRefreshReqDto jwtRefreshReqDto) {
log.info("[Request] refresh-tokens");
return new ResponseEntity<>(userService.refreshUserTokens(jwtRefreshReqDto), HttpStatus.OK);
}
}
컨트롤러에 올려주고
src/test/java/jpa/myunjuk/module/controller/UserControllerTest.java
package jpa.myunjuk.module.controller;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.*;
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.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.transaction.annotation.Transactional;
import java.util.HashMap;
import java.util.Map;
import static jpa.myunjuk.module.model.dto.UserDtos.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@Transactional
@SpringBootTest
@DisplayName("User Controller Test")
@AutoConfigureMockMvc(addFilters = false)
class UserControllerTest {
@Autowired private MockMvc mockMvc;
@Autowired private ObjectMapper objectMapper;
@Test
@DisplayName("User Sign-Up | Success")
void signUpSuccess() throws Exception {
UserSignUpReqDto userSignUpReqDto = UserSignUpReqDto.builder()
.email("new@new.com")
.password("1234")
.nickname("hello")
.build();
String jsonString = objectMapper.writeValueAsString(userSignUpReqDto);
mockMvc.perform(post("/sign-up")
.contentType(MediaType.APPLICATION_JSON)
.content(jsonString))
.andExpect(status().isNoContent());
}
@Test
@DisplayName("User Sign-Up | Fail : Duplicate User")
void signUpFailDuplicate() throws Exception {
UserSignUpReqDto userSignUpReqDto = UserSignUpReqDto.builder()
.email("test@test.com")
.password("1234")
.nickname("hello")
.build();
String jsonString = objectMapper.writeValueAsString(userSignUpReqDto);
Map<String, String> error = new HashMap<>();
error.put("DuplicateUserException", "email = test@test.com");
String response = objectMapper.writeValueAsString(error);
mockMvc.perform(post("/sign-up")
.contentType(MediaType.APPLICATION_JSON)
.content(jsonString))
.andExpect(status().isBadRequest())
.andExpect(content().string(response));
}
@Test
@DisplayName("User Sign-In | Success")
void signInSuccess() throws Exception {
UserSignInReqDto userSignInReqDto = UserSignInReqDto.builder()
.email("test@test.com")
.password("1234")
.build();
String jsonString = objectMapper.writeValueAsString(userSignInReqDto);
mockMvc.perform(post("/sign-in")
.contentType(MediaType.APPLICATION_JSON)
.content(jsonString))
.andExpect(status().isOk());
}
@Test
@DisplayName("User Sign-In | Fail : Wrong Email")
void singInFailEmail() throws Exception {
UserSignInReqDto userSignInReqDto = UserSignInReqDto.builder()
.email("wrong@test.com")
.password("1234")
.build();
String jsonString = objectMapper.writeValueAsString(userSignInReqDto);
Map<String, String> error = new HashMap<>();
error.put("NoSuchDataException", "email = wrong@test.com");
String response = objectMapper.writeValueAsString(error);
mockMvc.perform(post("/sign-in")
.contentType(MediaType.APPLICATION_JSON)
.content(jsonString))
.andExpect(status().isBadRequest())
.andExpect(content().string(response));
}
@Test
@DisplayName("User Sign-In | Fail : Wrong Password")
void singInFailPassword() throws Exception {
UserSignInReqDto userSignInReqDto = UserSignInReqDto.builder()
.email("test@test.com")
.password("wrong")
.build();
String jsonString = objectMapper.writeValueAsString(userSignInReqDto);
Map<String, String> error = new HashMap<>();
error.put("NoSuchDataException", "password = wrong");
String response = objectMapper.writeValueAsString(error);
mockMvc.perform(post("/sign-in")
.contentType(MediaType.APPLICATION_JSON)
.content(jsonString))
.andExpect(status().isBadRequest())
.andExpect(content().string(response));
}
}
로그인 테스트를 추가한다.
끝