빙응의 공부 블로그

[Spring REST API]로그인 구현하기 - 기본 본문

Spring/개인공부_실습

[Spring REST API]로그인 구현하기 - 기본

빙응이 2024. 6. 3. 21:21

📝실습 전 세팅

JWT + Scurity를 사용하여 REST API의 로그인을 구현해보려 한다.

또한 예외처리까지 실습할 예정이다.

 

구현 목록
기능 URL
회원가입 /users/join
로그인  /users/login

 

의존성
dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-web'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'

	//JPA
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

	//Security
	implementation 'org.springframework.boot:spring-boot-starter-security'
	testImplementation 'org.springframework.security:spring-security-test'

	//JWT
	implementation 'io.jsonwebtoken:jjwt-api:0.12.3'
	implementation 'io.jsonwebtoken:jjwt-impl:0.12.3'
	implementation 'io.jsonwebtoken:jjwt-jackson:0.12.3'
	//Lombok
	compileOnly 'org.projectlombok:lombok'
	annotationProcessor 'org.projectlombok:lombok'

	//MySQL_Connector
	runtimeOnly 'com.mysql:mysql-connector-j'
}

 

📝 Entity 설계하기

단순 회원가입이기 때문에 단순하게 설계했다.

또한 역할을 추가하여 나중에 있을 개인프로젝트용 템플릿으로 만들 것이다!

@Entity
@AllArgsConstructor
@NoArgsConstructor
@Getter
public class User {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;
  private String userName;
  private String password;
  
  @ElementCollection(fetch = FetchType.LAZY)
  @Enumerated(EnumType.STRING)
  private List<Role> roles = new ArrayList<>();
  
  @Builder
  public User(String userName, String password, List<Role> roles) {
    this.userName = userName;
    this.password = password;
    this.roles = Collections.singletonList(Role.MEMBER);
  }
  
  public void addRole(Role role) {
    this.roles.add(role);
  }
  
}
  • 회원은 권한들을 가지며 아이디와 비밀번호를 가진다. 
  • Mysql이기에 IDENTITY를 사용 

📝기초적인 로그인 서비스 

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class UserService {
  private final BCryptPasswordEncoder bCryptPasswordEncoder;
  private final UserRepository userRepository;
  
  @Transactional
  public void join(String userName, String password){
    // USERNAME 중복체크
    userRepository.findByUserName(userName)
        .ifPresent(user -> {
          throw new AppException(UserErrorCode.USERNAME_DUPLICATED);
    });
    // 저장
    userRepository.save(User.builder()
        .userName(userName)
        .password(bCryptPasswordEncoder.encode(password))
        .build());
  }
  
  public String login(String userName, String password){
    //userName 없음
    User user = userRepository.findByUserName(userName)
        .orElseThrow(() ->
          new AppException(UserErrorCode.FAILD_LOGIN));
    //password 틀림
    if(!bCryptPasswordEncoder.matches(password, user.getPassword())){
      throw new AppException(UserErrorCode.FAILD_LOGIN);
    }
    return "token";
  }
}

간단한 로그인, 회원가입 로직이다. 또한 사용자 오류 처리를 사용하여 더 가독성 있게 구현하였다.

 

 

커스텀 Exception 정의
@AllArgsConstructor
@Getter
public class AppException extends RuntimeException {
  private ErrorCode errorCode;
}
오류 처리 객체
public interface ErrorCode {
  HttpStatus getHttpStatus();
  String getMessage();
}


@AllArgsConstructor
@Getter
public enum UserErrorCode implements ErrorCode {
  USERNAME_DUPLICATED(HttpStatus.CONFLICT, "유저 중복"),
  FAILD_LOGIN(HttpStatus.NOT_FOUND, "유저가 없거나 비밀번호가 틀립니다.");
  private HttpStatus httpStatus;
  private String message;
}
RestControllerAdivce

RestController에서 들어오는 오류를 여기서 처리할 수 있게 해준다. 

@RestControllerAdvice
public class ExceptionManager {

  @ExceptionHandler(AppException.class)
  public ResponseEntity<ResponseDto<?>> appExceptionHandler(AppException e) {
    ErrorCode errorCode = e.getErrorCode();
    ResponseDto<?> response = ResponseDto.fail(errorCode.getHttpStatus().value(), errorCode.getMessage() + " " + e.getMessage());
    return ResponseEntity.status(errorCode.getHttpStatus()).body(response);
  }
}
커스텀 responseDto

커스텀 responseDto는 성공, 실패때 body로 보낼 데이터를 정의한 것이다. 

@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class ResponseDto<T> {
  private int code;
  private String message;
  private T data;
  private LocalDateTime timestamp;

  public static <T> ResponseDto<T> of(T data){
    return new ResponseDto<>(HttpStatus.OK.value(), null, data, LocalDateTime.now());
  }
  public static <T> ResponseDto<T> fail(Integer status, String message) {
    return new ResponseDto<>(status, message, null, LocalDateTime.now());
  }
}