Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ 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-security'
implementation 'io.jsonwebtoken:jjwt-api:0.12.3'
implementation 'io.jsonwebtoken:jjwt-impl:0.12.3'
implementation 'io.jsonwebtoken:jjwt-jackson:0.12.3'
implementation 'org.springframework.boot:spring-boot-configuration-processor'

compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.mysql:mysql-connector-j'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.example.umc10th.domain.member.controller;

import com.example.umc10th.domain.member.dto.UserLoginRequestDTO;
import com.example.umc10th.domain.member.dto.UserLoginResponseDTO;
import com.example.umc10th.domain.member.exception.code.MemberSuccessCode;
import com.example.umc10th.domain.member.service.MemberService;
import com.example.umc10th.global.apiPayload.ApiResponse;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

// 인증 관련 REST API를 처리하는 컨트롤러입니다.
@RestController
@RequiredArgsConstructor
@RequestMapping("/auth")
public class AuthController {

private final MemberService memberService;

// 로그인 요청을 처리하고 JWT 토큰을 발급합니다.
@PostMapping("/login")
public ResponseEntity<ApiResponse<UserLoginResponseDTO>> login(
@RequestBody @Valid UserLoginRequestDTO request
) {
UserLoginResponseDTO response = memberService.login(request);

return ResponseEntity.ok(ApiResponse.onSuccess(MemberSuccessCode.LOGIN_SUCCESS, response));
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/home 이랑 /mypage API가 둘 다 컨트롤러에서 memberService의 getmyPage()를 호출하고 있어! 응답도 마찬가지로 둘 다 HomeResponseDTO에 담고 있는데 둘이 섞였나?? 확인 한번 해봐야할 것 같아!

Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,14 @@
import com.example.umc10th.domain.mission.exception.code.MissionSuccessCode;
import com.example.umc10th.domain.mission.service.MissionService;
import com.example.umc10th.global.apiPayload.ApiResponse;
import com.example.umc10th.global.security.AuthMember;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
Expand All @@ -26,53 +27,54 @@
@RequestMapping("/api/users")
public class UserController {

// 회원 관련 비즈니스 로직을 처리하는 서비스입니다.
private final MemberService memberService;

// 홈 화면 미션 조회 로직을 처리하는 서비스입니다.
private final MissionService missionService;

// 회원가입 요청을 처리합니다.
@PostMapping
public ResponseEntity<ApiResponse<UserJoinResponseDTO>> joinUser(
// 요청 본문 JSON을 회원가입 요청 DTO로 바인딩하고 검증합니다.
@RequestBody @Valid UserJoinRequestDTO request
) {
UserJoinResponseDTO response = memberService.join(request);

return ResponseEntity.ok(ApiResponse.onSuccess(MemberSuccessCode.JOIN_SUCCESS, response));
}

// 홈 화면 회원 정보를 조회합니다.
// JWT 토큰 기반 홈 화면 회원 정보를 조회합니다.
@GetMapping("/home")
public ResponseEntity<ApiResponse<HomeResponseDTO>> home(
// URL에 노출하지 않고 요청 헤더의 userId 값으로 회원을 식별합니다.
@RequestHeader("userId") Long userId
@AuthenticationPrincipal AuthMember authMember
) {
HomeResponseDTO response = memberService.getMyPage(userId);
HomeResponseDTO response = memberService.getMyPage(authMember);

return ResponseEntity.ok(ApiResponse.onSuccess(MemberSuccessCode.GET_MEMBER_SUCCESS, response));
}

// 마이페이지 회원 정보를 조회합니다.
// JWT 토큰 기반 마이페이지 정보를 조회합니다.
@GetMapping("/mypage")
public ResponseEntity<ApiResponse<HomeResponseDTO>> getMyPage(
// URL에 노출하지 않고 요청 헤더의 userId 값으로 회원을 식별합니다.
@RequestHeader("userId") Long userId
@AuthenticationPrincipal AuthMember authMember
) {
HomeResponseDTO response = memberService.getMyPage(authMember);

return ResponseEntity.ok(ApiResponse.onSuccess(MemberSuccessCode.GET_MEMBER_SUCCESS, response));
}

// 워크북 형식의 JWT 토큰 기반 마이페이지 정보를 조회합니다.
@GetMapping("/me")
public ResponseEntity<ApiResponse<HomeResponseDTO>> getMe(
@AuthenticationPrincipal AuthMember authMember
) {
HomeResponseDTO response = memberService.getMyPage(userId);
HomeResponseDTO response = memberService.getMyPage(authMember);

return ResponseEntity.ok(ApiResponse.onSuccess(MemberSuccessCode.GET_MEMBER_SUCCESS, response));
}

// 홈 화면에서 도전 가능한 미션 목록을 조회합니다.
@GetMapping("/missions/home")
public ResponseEntity<ApiResponse<MissionListResDTO>> getHomeMissions(
// 요청 쿼리의 locationId 값을 바인딩합니다.
@RequestParam Long locationId,
// 요청 쿼리의 page 값을 바인딩하고 기본값을 지정합니다.
@RequestParam(defaultValue = "0") int page,
// 요청 쿼리의 size 값을 바인딩하고 기본값을 지정합니다.
@RequestParam(defaultValue = "10") int size
) {
MissionListResDTO response =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.example.umc10th.domain.member.dto.HomeResponseDTO;
import com.example.umc10th.domain.member.dto.UserJoinRequestDTO;
import com.example.umc10th.domain.member.dto.UserJoinResponseDTO;
import com.example.umc10th.domain.member.dto.UserLoginResponseDTO;
import com.example.umc10th.domain.member.entity.Member;
import com.example.umc10th.domain.member.enums.SocialType;

Expand Down Expand Up @@ -36,6 +37,12 @@ public static UserJoinResponseDTO toUserJoinResponseDTO(Member member) {
.build();
}

public static UserLoginResponseDTO toUserLoginResponseDTO(String accessToken) {
return UserLoginResponseDTO.builder()
.accessToken(accessToken)
.build();
}

// 회원 Entity를 홈/마이페이지 응답 DTO로 변환합니다.
public static HomeResponseDTO toHomeResponseDTO(Member member) {
return HomeResponseDTO.builder()
Expand Down

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Email 어노테이션으로 검증해주는거 좋네! 하지만 @Email 어노테이션은 허점이 많아서 검증이 제대로 안될 수 있다고 하네! 첨부한 링크 가볍게 읽어보는거 추천해~(링크가 아래 링크 읽는거 추천)
https://velog.io/@mj3242/Spring%EC%97%90%EC%84%9C-%EC%9D%B4%EB%A9%94%EC%9D%BC%EC%9D%84-%EA%B2%80%EC%A6%9D%ED%95%98%EB%8A%94-%EB%B0%A9%EB%B2%95

Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.example.umc10th.domain.member.dto;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import lombok.Getter;

// 로그인 요청 정보를 담는 DTO입니다.
@Getter
public class UserLoginRequestDTO {

// 로그인에 사용할 이메일입니다.
@NotBlank(message = "이메일은 필수입니다.")
@Email(message = "이메일 형식이 올바르지 않습니다.")
private String email;

// 로그인에 사용할 비밀번호입니다.
@NotBlank(message = "비밀번호는 필수입니다.")
private String password;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.example.umc10th.domain.member.dto;

import lombok.Builder;
import lombok.Getter;

// 로그인 응답 정보를 담는 DTO입니다.
@Getter
@Builder
public class UserLoginResponseDTO {

// JWT Access Token입니다.
private String accessToken;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

로그인 응답으로 요렇게 액세스토큰 자체만 내려주는구나! 나는 만료시간까지 같이 내려주는걸 추천하긴 해! 만료시간이 왜 필요한지도 링크가 한번 찾아보면 좋겠다..!

}
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
public enum MemberErrorCode implements BaseErrorCode {

MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "MEMBER404", "사용자를 찾을 수 없습니다."),
MEMBER_PASSWORD_NOT_MATCH(HttpStatus.UNAUTHORIZED, "MEMBER401", "비밀번호가 일치하지 않습니다."),
MEMBER_EMAIL_ALREADY_EXISTS(HttpStatus.CONFLICT, "MEMBER409", "이미 가입된 이메일입니다.");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이렇게 분기하면 로그인 API에서 이메일 가입 여부를 추측할 수 있게되니까, 로그인 실패는 하나로 합치는게 더 좋아보인다!!


// HTTP 응답 상태입니다.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
public enum MemberSuccessCode implements BaseSuccessCode {

JOIN_SUCCESS(HttpStatus.OK, "MEMBER2001", "회원가입에 성공했습니다."),
GET_MEMBER_SUCCESS(HttpStatus.OK, "MEMBER2002", "회원 정보 조회에 성공했습니다.");
GET_MEMBER_SUCCESS(HttpStatus.OK, "MEMBER2002", "회원 정보 조회에 성공했습니다."),
LOGIN_SUCCESS(HttpStatus.OK, "MEMBER2003", "로그인에 성공했습니다.");

// HTTP 응답 상태입니다.
private final HttpStatus status;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,24 +4,27 @@
import com.example.umc10th.domain.member.dto.HomeResponseDTO;
import com.example.umc10th.domain.member.dto.UserJoinRequestDTO;
import com.example.umc10th.domain.member.dto.UserJoinResponseDTO;
import com.example.umc10th.domain.member.dto.UserLoginRequestDTO;
import com.example.umc10th.domain.member.dto.UserLoginResponseDTO;
import com.example.umc10th.domain.member.entity.Member;
import com.example.umc10th.domain.member.exception.MemberException;
import com.example.umc10th.domain.member.exception.code.MemberErrorCode;
import com.example.umc10th.domain.member.repository.MemberRepository;
import com.example.umc10th.global.security.AuthMember;
import com.example.umc10th.global.security.JwtUtil;
import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

// 해당 클래스를 Service 계층 Bean으로 등록합니다.
// 회원 도메인의 비즈니스 로직을 처리합니다.
@Service
// final 필드를 생성자 주입 방식으로 주입합니다.
@RequiredArgsConstructor
public class MemberService {

// 회원 Entity의 DB 접근을 담당하는 Repository입니다.
private final MemberRepository memberRepository;
private final PasswordEncoder passwordEncoder;
private final JwtUtil jwtUtil;

// 회원가입 비즈니스 로직을 처리합니다.
@Transactional
Expand All @@ -37,11 +40,30 @@ public UserJoinResponseDTO join(UserJoinRequestDTO request) {
return MemberConverter.toUserJoinResponseDTO(savedMember);
}

// 회원 홈/마이페이지 정보를 조회합니다.
// 로그인 요청을 검증하고 JWT Access Token을 발급합니다.
@Transactional
public UserLoginResponseDTO login(UserLoginRequestDTO request) {
Member member = memberRepository.findByEmail(request.getEmail())
.orElseThrow(() -> new MemberException(MemberErrorCode.MEMBER_NOT_FOUND));

if (!passwordEncoder.matches(request.getPassword(), member.getPassword())) {
throw new MemberException(MemberErrorCode.MEMBER_PASSWORD_NOT_MATCH);
}

String accessToken = jwtUtil.createAccessToken(new AuthMember(member));
return MemberConverter.toUserLoginResponseDTO(accessToken);
}

// 회원 ID로 마이페이지 정보를 조회합니다.
public HomeResponseDTO getMyPage(Long userId) {
Member member = memberRepository.findById(userId)
.orElseThrow(() -> new MemberException(MemberErrorCode.MEMBER_NOT_FOUND));

return MemberConverter.toHomeResponseDTO(member);
}

// 인증 객체의 회원 정보로 마이페이지를 조회합니다.
public HomeResponseDTO getMyPage(AuthMember authMember) {
return MemberConverter.toHomeResponseDTO(authMember.getMember());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,30 @@

import com.example.umc10th.global.security.CustomAccessDeniedHandler;
import com.example.umc10th.global.security.CustomAuthenticationEntryPoint;
import com.example.umc10th.global.security.CustomUserDetailsService;
import com.example.umc10th.global.security.JwtAuthFilter;
import com.example.umc10th.global.security.JwtUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
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.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

// Spring Security의 인증, 인가 정책을 설정합니다.
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

private final JwtUtil jwtUtil;
private final CustomUserDetailsService customUserDetailsService;
private final CustomAuthenticationEntryPoint customAuthenticationEntryPoint;
private final CustomAccessDeniedHandler customAccessDeniedHandler;

Expand All @@ -27,11 +34,16 @@ public class SecurityConfig {
"/swagger-ui.html",
"/swagger-resources/**",
"/v3/api-docs/**",
"/login",
"/logout"
"/auth/login"
};

// HTTP 요청별 접근 권한과 폼 로그인 정책을 설정합니다.
// JWT 인증 필터를 Bean으로 등록합니다.
@Bean
public JwtAuthFilter jwtAuthFilter() {
return new JwtAuthFilter(jwtUtil, customUserDetailsService);
}

// HTTP 요청별 접근 권한과 JWT 필터 정책을 설정합니다.
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
Expand All @@ -41,11 +53,11 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
.requestMatchers(HttpMethod.POST, "/api/users").permitAll()
.anyRequest().authenticated()
)
.formLogin(form -> form
.usernameParameter("email")
.defaultSuccessUrl("/swagger-ui/index.html", true)
.permitAll()
.formLogin(AbstractHttpConfigurer::disable)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.addFilterBefore(jwtAuthFilter(), UsernamePasswordAuthenticationFilter.class)
.logout(logout -> logout
.logoutUrl("/logout")
.logoutSuccessUrl("/login?logout")
Expand Down
Loading