diff --git a/src/main/java/dmu/dasom/api/domain/member/controller/MemberController.java b/src/main/java/dmu/dasom/api/domain/member/controller/MemberController.java index f0632da..c8fbc73 100644 --- a/src/main/java/dmu/dasom/api/domain/member/controller/MemberController.java +++ b/src/main/java/dmu/dasom/api/domain/member/controller/MemberController.java @@ -1,6 +1,7 @@ package dmu.dasom.api.domain.member.controller; import dmu.dasom.api.domain.common.exception.ErrorResponse; +import dmu.dasom.api.domain.member.dto.LoginRequestDto; import dmu.dasom.api.domain.member.dto.SignupRequestDto; import dmu.dasom.api.domain.member.service.MemberService; import dmu.dasom.api.global.auth.dto.TokenBox; @@ -49,6 +50,68 @@ public ResponseEntity signUp(@Valid @RequestBody final SignupRequestDto re return ResponseEntity.ok().build(); } + // 일반 사용자 로그인 + @Operation(summary = "일반 사용자 로그인") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "로그인 성공 (Header로 토큰 반환)"), + @ApiResponse(responseCode = "400", description = "실패 케이스", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ErrorResponse.class), + examples = { + @ExampleObject( + name = "회원 없음", + value = "{ \"code\": \"C003\", \"message\": \"해당 회원을 찾을 수 없습니다.\" }" + ), + @ExampleObject( + name = "로그인 실패", + value = "{ \"code\": \"C005\", \"message\": \"로그인에 실패하였습니다.\" }" + ), + @ExampleObject( + name = "권한 없음", + value = "{ \"code\": \"C001\", \"message\": \"인증되지 않은 사용자입니다.\" }")}))}) + @PostMapping("/auth/user-login") + public ResponseEntity userLogin(@Valid @RequestBody final LoginRequestDto request) { + final TokenBox tokenBox = memberService.userLogin(request); + final HttpHeaders headers = new HttpHeaders(); + headers.add("Access-Token", tokenBox.getAccessToken()); + headers.add("Refresh-Token", tokenBox.getRefreshToken()); + headers.add("Authority", tokenBox.getAuthority()); + + return ResponseEntity.ok().headers(headers).build(); + } + + // 관리자 로그인 + @Operation(summary = "관리자 로그인") + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "로그인 성공 (Header로 토큰 반환)"), + @ApiResponse(responseCode = "400", description = "실패 케이스", + content = @Content( + mediaType = "application/json", + schema = @Schema(implementation = ErrorResponse.class), + examples = { + @ExampleObject( + name = "회원 없음", + value = "{ \"code\": \"C003\", \"message\": \"해당 회원을 찾을 수 없습니다.\" }" + ), + @ExampleObject( + name = "로그인 실패", + value = "{ \"code\": \"C005\", \"message\": \"로그인에 실패하였습니다.\" }" + ), + @ExampleObject( + name = "권한 없음", + value = "{ \"code\": \"C001\", \"message\": \"인증되지 않은 사용자입니다.\" }")}))}) + @PostMapping("/auth/admin-login") + public ResponseEntity adminLogin(@Valid @RequestBody final LoginRequestDto request) { + final TokenBox tokenBox = memberService.adminLogin(request); + final HttpHeaders headers = new HttpHeaders(); + headers.add("Access-Token", tokenBox.getAccessToken()); + headers.add("Refresh-Token", tokenBox.getRefreshToken()); + headers.add("Authority", tokenBox.getAuthority()); + + return ResponseEntity.ok().headers(headers).build(); + } + @Operation(summary = "토큰 갱신") @ApiResponses(value = { @ApiResponse(responseCode = "200", description = "토큰 갱신 성공 (Header로 토큰 반환)"), diff --git a/src/main/java/dmu/dasom/api/domain/member/dto/LoginRequestDto.java b/src/main/java/dmu/dasom/api/domain/member/dto/LoginRequestDto.java new file mode 100644 index 0000000..790a0cc --- /dev/null +++ b/src/main/java/dmu/dasom/api/domain/member/dto/LoginRequestDto.java @@ -0,0 +1,23 @@ +package dmu.dasom.api.domain.member.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.Getter; + +@Getter +@Schema(name = "LoginRequestDto", description = "로그인 요청 DTO") +public class LoginRequestDto { + + @NotNull(message = "이메일은 필수 값입니다.") + @Email(message = "유효한 이메일 주소를 입력해주세요.") + @Size(max = 64) + @Schema(description = "이메일 주소", example = "test@example.com") + private String email; + + @NotNull(message = "비밀번호는 필수 값입니다.") + @Size(min = 8, max = 20, message = "비밀번호는 8자 이상 20자 이하로 입력해주세요.") + @Schema(description = "비밀번호", example = "password123!") + private String password; +} diff --git a/src/main/java/dmu/dasom/api/domain/member/service/MemberService.java b/src/main/java/dmu/dasom/api/domain/member/service/MemberService.java index 719816d..db867f3 100644 --- a/src/main/java/dmu/dasom/api/domain/member/service/MemberService.java +++ b/src/main/java/dmu/dasom/api/domain/member/service/MemberService.java @@ -1,5 +1,6 @@ package dmu.dasom.api.domain.member.service; +import dmu.dasom.api.domain.member.dto.LoginRequestDto; import dmu.dasom.api.domain.member.dto.SignupRequestDto; import dmu.dasom.api.domain.member.entity.Member; import dmu.dasom.api.global.auth.dto.TokenBox; @@ -13,6 +14,12 @@ public interface MemberService { void signUp(final SignupRequestDto request); + TokenBox login(final LoginRequestDto request); + + TokenBox userLogin(final LoginRequestDto request); + + TokenBox adminLogin(final LoginRequestDto request); + TokenBox tokenRotation(final UserDetailsImpl userDetails); } diff --git a/src/main/java/dmu/dasom/api/domain/member/service/MemberServiceImpl.java b/src/main/java/dmu/dasom/api/domain/member/service/MemberServiceImpl.java index 2094c5a..32acb28 100644 --- a/src/main/java/dmu/dasom/api/domain/member/service/MemberServiceImpl.java +++ b/src/main/java/dmu/dasom/api/domain/member/service/MemberServiceImpl.java @@ -1,14 +1,18 @@ package dmu.dasom.api.domain.member.service; +import dmu.dasom.api.domain.member.dto.LoginRequestDto; import dmu.dasom.api.domain.common.exception.CustomException; import dmu.dasom.api.domain.common.exception.ErrorCode; import dmu.dasom.api.domain.recruit.service.RecruitService; import dmu.dasom.api.domain.member.dto.SignupRequestDto; import dmu.dasom.api.domain.member.entity.Member; +import dmu.dasom.api.domain.member.enums.Role; import dmu.dasom.api.domain.member.repository.MemberRepository; import dmu.dasom.api.global.auth.dto.TokenBox; import dmu.dasom.api.global.auth.jwt.JwtUtil; import dmu.dasom.api.global.auth.userdetails.UserDetailsImpl; +import dmu.dasom.api.domain.common.exception.CustomException; +import dmu.dasom.api.domain.common.exception.ErrorCode; import lombok.RequiredArgsConstructor; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; @@ -56,6 +60,43 @@ public void signUp(final SignupRequestDto request) { memberRepository.save(request.toEntity(encoder.encode(request.getPassword()), generation)); } + // 로그인 (기존 로직을 private 헬퍼 메소드로 변경) + private TokenBox authenticateAndGenerateToken(final String email, final String password, final Role expectedRole) { + // 1. 이메일로 사용자 조회 + final Member member = memberRepository.findByEmail(email) + .orElseThrow(() -> new CustomException(ErrorCode.MEMBER_NOT_FOUND)); + + // 2. 비밀번호 일치 여부 확인 + if (!encoder.matches(password, member.getPassword())) { + throw new CustomException(ErrorCode.LOGIN_FAILED); + } + + // 3. 역할 확인 + if (expectedRole != null && member.getRole() != expectedRole) { + throw new CustomException(ErrorCode.UNAUTHORIZED); // 또는 더 구체적인 에러 코드 + } + + // 4. JWT 토큰 생성 및 반환 + return jwtUtil.generateTokenBox(member.getEmail(), member.getRole().getName()); + } + + @Override + public TokenBox login(final LoginRequestDto request) { + // 이 메소드는 더 이상 사용되지 않거나, userLogin/adminLogin으로 대체됩니다. + // 여기서는 임시로 일반 로그인으로 처리합니다. + return authenticateAndGenerateToken(request.getEmail(), request.getPassword(), null); + } + + @Override + public TokenBox userLogin(final LoginRequestDto request) { + return authenticateAndGenerateToken(request.getEmail(), request.getPassword(), Role.ROLE_MEMBER); + } + + @Override + public TokenBox adminLogin(final LoginRequestDto request) { + return authenticateAndGenerateToken(request.getEmail(), request.getPassword(), Role.ROLE_ADMIN); + } + // 토큰 갱신 @Override public TokenBox tokenRotation(final UserDetailsImpl userDetails) { diff --git a/src/main/java/dmu/dasom/api/global/auth/config/SecurityConfig.java b/src/main/java/dmu/dasom/api/global/auth/config/SecurityConfig.java index cb7f8f3..6e4b41e 100644 --- a/src/main/java/dmu/dasom/api/global/auth/config/SecurityConfig.java +++ b/src/main/java/dmu/dasom/api/global/auth/config/SecurityConfig.java @@ -73,7 +73,7 @@ public SecurityFilterChain filterChain(final HttpSecurity http, final Authentica .requestMatchers("/api/auth/logout", "/api/auth/rotation").authenticated() .anyRequest().permitAll()) .addFilterBefore(jwtFilter, CustomAuthenticationFilter.class) - .addFilterAt(customAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) + .addFilterAfter(new CustomLogoutFilter(jwtUtil), JwtFilter.class) .exceptionHandling(handler -> handler .accessDeniedHandler(accessDeniedHandler)