Skip to content

Commit b3e9e0c

Browse files
authored
feat: 인증 로직 구현
* chore: Security, OpenApi 의존성 추가 * chore: exception 구성 수정 - ControllerAdvice가 ResponseEntity 타입으로 응답하도록 수정 - 서비스 관련 에러 코드 추가 - ErrorResponse 생성자 수정 * feat: JpaAuditing 도입 * feat: BaseEntity 생성 - 엔티티 간 공통으로 활용 - Status Enum 추가 * feat: Member 도메인 구현 - BaseEntity 상속 받은 Member 엔티티 구현 - 회원가입 구현 - Role Enum으로 권한 정의 * feat: Spring Security 구성 - SecurityConfig, EncoderConfig 구성 * feat: 로그인 요청 DTO 구현 * feat: Security FilterChain 예외 처리 구현 - ErrorResponse 타입으로 예외 응답 * feat: UserDetails 구현체 구현 * chore: redis 연결 정보, JWT 설정 정보 추가 * chore: SwaggerConfig 구현 * feat: ControllerAdvice 예외 처리 추가 - 객체 필드 타입 검증 예외 Handler 추가 * feat: JwtUtil 구현 - 토큰 생성, 저장, Claims 추출, 만료 등의 작업 수행 * feat: 토큰 응답 DTO 구현 * feat: JwtFilter 구현 - JWT 유효성 검증 필터 * chore: 토큰 관련 예외 에러코드 추가 * feat: CustomAuthenticationFilter 구현 - 로그인 요청 시 호출되는 필터 - authenticationManager.authenticate()를 통해 내부적으로 인증 진행 - 인증 성공, 실패 처리 메소드 구현 * feat: CustomLogoutFilter 구현 - 로그아웃 요청 발생 시 catch 하여 토큰 만료 처리 진행 * chore: swagger-ui 접근 url 수정 * feat: 리소스 접근 제어 수정 - 인증이 필요하지 않은 접근은 모두 허용해 불필요한 권한 검사를 피함 * feat: error 핸들링 엔드포인트 추가 - 404, 500 등 에러 상황에 따라 ErrorResponse 타입으로 응답하도록 구현 - 에러 코드 추가 * test: 테스트 모듈 설정 * test: MemberService 단위 테스트 구현
1 parent a041d6e commit b3e9e0c

32 files changed

+1051
-7
lines changed

build.gradle

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,10 @@ repositories {
3434
}
3535

3636
dependencies {
37-
// implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0'
37+
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.7.0'
3838
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
3939
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
40-
// implementation 'org.springframework.boot:spring-boot-starter-security'
40+
implementation 'org.springframework.boot:spring-boot-starter-security'
4141
implementation 'org.springframework.boot:spring-boot-starter-validation'
4242
implementation 'org.springframework.boot:spring-boot-starter-web'
4343
compileOnly 'org.projectlombok:lombok'

src/main/java/dmu/dasom/api/ApiApplication.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
import org.springframework.boot.SpringApplication;
44
import org.springframework.boot.autoconfigure.SpringBootApplication;
5+
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
56

7+
@EnableJpaAuditing
68
@SpringBootApplication
79
public class ApiApplication {
810

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package dmu.dasom.api.domain.common;
2+
3+
import jakarta.persistence.*;
4+
import lombok.Getter;
5+
import org.springframework.data.annotation.CreatedDate;
6+
import org.springframework.data.annotation.LastModifiedDate;
7+
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
8+
9+
import java.time.LocalDateTime;
10+
11+
@EntityListeners(AuditingEntityListener.class)
12+
@Getter
13+
@MappedSuperclass
14+
public class BaseEntity {
15+
16+
// 생성일
17+
@CreatedDate
18+
@Column(name = "created_at", updatable = false, nullable = false)
19+
private LocalDateTime createdAt;
20+
21+
// 수정일
22+
@LastModifiedDate
23+
@Column(name = "updated_at", nullable = false)
24+
private LocalDateTime updatedAt;
25+
26+
// 상태
27+
@Column(name = "status", length = 16, nullable = false)
28+
@Enumerated(EnumType.STRING)
29+
private Status status = Status.ACTIVE;
30+
31+
// 상태 변경
32+
public void updateStatus(final Status status) {
33+
this.status = status;
34+
}
35+
36+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package dmu.dasom.api.domain.common;
2+
3+
public enum Status {
4+
ACTIVE,
5+
INACTIVE,
6+
DELETED
7+
}
Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,22 @@
11
package dmu.dasom.api.domain.common.exception;
22

3+
import org.springframework.http.HttpStatus;
4+
import org.springframework.http.ResponseEntity;
5+
import org.springframework.web.bind.MethodArgumentNotValidException;
36
import org.springframework.web.bind.annotation.ExceptionHandler;
47
import org.springframework.web.bind.annotation.RestControllerAdvice;
58

69
@RestControllerAdvice
710
public class CustomControllerAdvice {
811

912
@ExceptionHandler(CustomException.class)
10-
public ErrorResponse customException(final CustomException e) {
11-
return new ErrorResponse(e.getErrorCode().getCode(), e.getErrorCode().getMessage());
13+
public ResponseEntity<ErrorResponse> customException(final CustomException e) {
14+
return ResponseEntity.status(e.getErrorCode().getStatus()).body(new ErrorResponse(e.getErrorCode()));
15+
}
16+
17+
@ExceptionHandler(MethodArgumentNotValidException.class)
18+
public ResponseEntity<ErrorResponse> methodArgumentNotValidException(final MethodArgumentNotValidException e) {
19+
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(new ErrorResponse(ErrorCode.ARGUMENT_NOT_VALID));
1220
}
1321

1422
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package dmu.dasom.api.domain.common.exception;
2+
3+
import jakarta.servlet.RequestDispatcher;
4+
import jakarta.servlet.http.HttpServletRequest;
5+
import org.springframework.boot.web.servlet.error.ErrorController;
6+
import org.springframework.http.HttpStatus;
7+
import org.springframework.http.ResponseEntity;
8+
import org.springframework.web.bind.annotation.RequestMapping;
9+
import org.springframework.web.bind.annotation.RestController;
10+
11+
@RestController
12+
public class CustomErrorController implements ErrorController {
13+
14+
@RequestMapping("/error")
15+
public ResponseEntity<ErrorResponse> handleError(final HttpServletRequest request) {
16+
// 에러 코드 추출
17+
final Integer statusCode = (Integer) request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
18+
final HttpStatus status = statusCode != null ? HttpStatus.valueOf(statusCode) : HttpStatus.INTERNAL_SERVER_ERROR;
19+
20+
// 404 에러
21+
if (status == HttpStatus.NOT_FOUND)
22+
return ResponseEntity.status(status).body(new ErrorResponse(ErrorCode.NOT_FOUND));
23+
24+
// 기타 에러
25+
return ResponseEntity.status(status).body(new ErrorResponse(ErrorCode.INTERNAL_SERVER_ERROR));
26+
}
27+
28+
}

src/main/java/dmu/dasom/api/domain/common/exception/ErrorCode.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,14 @@ public enum ErrorCode {
99

1010
UNAUTHORIZED(401, "C001", "인증되지 않은 사용자입니다."),
1111
FORBIDDEN(403, "C002", "권한이 없습니다."),
12+
MEMBER_NOT_FOUND(400, "C003", "해당 회원을 찾을 수 없습니다."),
13+
TOKEN_EXPIRED(400, "C004", "토큰이 만료되었습니다."),
14+
LOGIN_FAILED(400, "C005", "로그인에 실패하였습니다."),
15+
SIGNUP_FAILED(400, "C006", "회원가입에 실패하였습니다."),
16+
ARGUMENT_NOT_VALID(400, "C007", "요청한 값이 올바르지 않습니다."),
17+
TOKEN_NOT_VALID(400, "C008", "토큰이 올바르지 않습니다."),
18+
INTERNAL_SERVER_ERROR(500, "C009", "서버에 문제가 발생하였습니다."),
19+
NOT_FOUND(404, "C010", "해당 리소스를 찾을 수 없습니다.")
1220
;
1321

1422
private final int status;
Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
package dmu.dasom.api.domain.common.exception;
22

33
import lombok.AccessLevel;
4-
import lombok.AllArgsConstructor;
54
import lombok.Getter;
65
import lombok.NoArgsConstructor;
76

8-
@AllArgsConstructor
97
@Getter
108
@NoArgsConstructor(access = AccessLevel.PROTECTED)
119
public class ErrorResponse {
1210

1311
private String code;
1412
private String message;
1513

14+
public ErrorResponse(final ErrorCode errorCode) {
15+
this.code = errorCode.getCode();
16+
this.message = errorCode.getMessage();
17+
}
18+
1619
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package dmu.dasom.api.domain.member.controller;
2+
3+
import dmu.dasom.api.domain.common.exception.ErrorResponse;
4+
import dmu.dasom.api.domain.member.dto.SignupRequestDto;
5+
import dmu.dasom.api.domain.member.service.MemberService;
6+
import io.swagger.v3.oas.annotations.Operation;
7+
import io.swagger.v3.oas.annotations.media.Content;
8+
import io.swagger.v3.oas.annotations.media.ExampleObject;
9+
import io.swagger.v3.oas.annotations.media.Schema;
10+
import io.swagger.v3.oas.annotations.responses.ApiResponse;
11+
import io.swagger.v3.oas.annotations.responses.ApiResponses;
12+
import jakarta.validation.Valid;
13+
import lombok.RequiredArgsConstructor;
14+
import org.springframework.http.ResponseEntity;
15+
import org.springframework.web.bind.annotation.PostMapping;
16+
import org.springframework.web.bind.annotation.RequestBody;
17+
import org.springframework.web.bind.annotation.RequestMapping;
18+
import org.springframework.web.bind.annotation.RestController;
19+
20+
@RestController
21+
@RequestMapping("/api")
22+
@RequiredArgsConstructor
23+
public class MemberController {
24+
25+
private final MemberService memberService;
26+
27+
// 회원가입
28+
@Operation(summary = "회원가입")
29+
@ApiResponses(value = {
30+
@ApiResponse(responseCode = "200", description = "회원가입 성공"),
31+
@ApiResponse(responseCode = "400", description = "실패 케이스",
32+
content = @Content(
33+
mediaType = "application/json",
34+
schema = @Schema(implementation = ErrorResponse.class),
35+
examples = {
36+
@ExampleObject(
37+
name = "이메일 중복",
38+
value = "{ \"code\": \"C006\", \"message\": \"회원가입에 실패하였습니다.\" }"
39+
),
40+
@ExampleObject(
41+
name = "이메일 또는 비밀번호 형식 올바르지 않음",
42+
value = "{ \"code\": \"C007\", \"message\": \"요청한 값이 올바르지 않습니다.\" }"
43+
)
44+
}
45+
)
46+
)
47+
})
48+
@PostMapping("/auth/signup")
49+
public ResponseEntity<Void> signUp(@Valid @RequestBody final SignupRequestDto request) {
50+
memberService.signUp(request);
51+
return ResponseEntity.ok().build();
52+
}
53+
54+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package dmu.dasom.api.domain.member.dto;
2+
3+
import dmu.dasom.api.domain.member.entity.Member;
4+
import io.swagger.v3.oas.annotations.media.Schema;
5+
import jakarta.validation.constraints.Email;
6+
import jakarta.validation.constraints.NotNull;
7+
import lombok.Getter;
8+
import org.hibernate.validator.constraints.Length;
9+
10+
@Getter
11+
@Schema(name = "SignupRequestDto", description = "회원가입 요청 DTO")
12+
public class SignupRequestDto {
13+
14+
@Email
15+
@Length(max = 64)
16+
@NotNull
17+
@Schema(description = "이메일", example = "[email protected]", maxLength = 64)
18+
private String email;
19+
20+
@Length(min = 8, max = 128)
21+
@NotNull
22+
@Schema(description = "비밀번호", example = "password", minLength = 8, maxLength = 128)
23+
private String password;
24+
25+
public Member toEntity(final String password) {
26+
return Member.builder()
27+
.email(this.email)
28+
.password(password)
29+
.build();
30+
}
31+
}

0 commit comments

Comments
 (0)