diff --git a/build.gradle b/build.gradle index b0d243f..4e0fc1c 100644 --- a/build.gradle +++ b/build.gradle @@ -33,6 +33,10 @@ dependencies { // Swagger implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:3.0.1' implementation 'org.springdoc:springdoc-openapi-starter-webmvc-api:3.0.1' + + // Security + implementation 'org.springframework.boot:spring-boot-starter-security' + testImplementation 'org.springframework.security:spring-security-test' } tasks.named('test') { diff --git a/src/main/java/com/example/umc10th/Umc10thApplication.java b/src/main/java/com/example/umc10th/Umc10thApplication.java index 9983f43..b0ccfa5 100644 --- a/src/main/java/com/example/umc10th/Umc10thApplication.java +++ b/src/main/java/com/example/umc10th/Umc10thApplication.java @@ -2,7 +2,9 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.data.jpa.repository.config.EnableJpaAuditing; +@EnableJpaAuditing @SpringBootApplication public class Umc10thApplication { diff --git a/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java b/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java index dd770f4..a654dbc 100644 --- a/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java +++ b/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java @@ -1,5 +1,6 @@ package com.example.umc10th.domain.member.converter; +import com.example.umc10th.domain.member.dto.MemberReqDTO; import com.example.umc10th.domain.member.dto.MemberResDTO; import com.example.umc10th.domain.member.entity.Member; @@ -19,4 +20,26 @@ public static MemberResDTO.GetInfo toGetInfo(Member member) { .build(); } + // 회원가입 + public static Member toMember(MemberReqDTO.SignUp dto, String encodedPassword) { + return Member.builder() + .name(dto.name()) + .gender(dto.gender()) + .birth(dto.birth()) + .address(dto.address()) + .detailAddress(dto.detailAddress()) + .email(dto.email()) + .password(encodedPassword) + .phoneNumber(dto.phoneNumber()) + .build(); + } + + public static MemberResDTO.SignUp toSignUp(Member member) { + return MemberResDTO.SignUp.builder() + .memberId(member.getId()) + .name(member.getName()) + .email(member.getEmail()) + .createdAt(member.getCratedAt()) + .build(); + } } diff --git a/src/main/java/com/example/umc10th/domain/member/dto/MemberReqDTO.java b/src/main/java/com/example/umc10th/domain/member/dto/MemberReqDTO.java index d529001..b2a4725 100644 --- a/src/main/java/com/example/umc10th/domain/member/dto/MemberReqDTO.java +++ b/src/main/java/com/example/umc10th/domain/member/dto/MemberReqDTO.java @@ -1,5 +1,7 @@ package com.example.umc10th.domain.member.dto; +import com.example.umc10th.domain.member.enums.Address; +import com.example.umc10th.domain.member.enums.Gender; import jakarta.validation.constraints.NotNull; import java.time.LocalDate; @@ -17,16 +19,17 @@ public record SignUp( @NotNull(message = "이름을 입력해주세요.") String name, @NotNull(message = "성별을 입력해주세요.") - String gender, + Gender gender, @NotNull(message = "생년월일을 입력해주세요.") LocalDate birth, @NotNull(message = "주소를 입력해주세요.") - String address, + Address address, String detailAddress, @NotNull(message = "이메일을 입력해주세요.") String email, + @NotNull(message = "비밀번호를 입력해주세요.") + String password, @NotNull(message = "전화번호를 입력해주세요.") String phoneNumber ) {} } - diff --git a/src/main/java/com/example/umc10th/domain/member/entity/Member.java b/src/main/java/com/example/umc10th/domain/member/entity/Member.java index 6776c7a..c7812cc 100644 --- a/src/main/java/com/example/umc10th/domain/member/entity/Member.java +++ b/src/main/java/com/example/umc10th/domain/member/entity/Member.java @@ -47,10 +47,13 @@ public class Member extends BaseEntity { @Column(name = "detail_address", nullable = false) private String detailAddress; - @Column(name = "social_uid", nullable = false) + @Column(name = "password", nullable = false) + private String password; + + @Column(name = "social_uid") private String socialUid; - @Column(name = "social_type", nullable = false) + @Column(name = "social_type") @Enumerated(EnumType.STRING) private SocialType socialType; diff --git a/src/main/java/com/example/umc10th/domain/member/exception/MemberException.java b/src/main/java/com/example/umc10th/domain/member/exception/MemberException.java index 2995a10..be55ea5 100644 --- a/src/main/java/com/example/umc10th/domain/member/exception/MemberException.java +++ b/src/main/java/com/example/umc10th/domain/member/exception/MemberException.java @@ -1,7 +1,10 @@ package com.example.umc10th.domain.member.exception; -public class MemberException extends RuntimeException { - public MemberException(String message) { - super(message); +import com.example.umc10th.global.apiPayload.code.BaseErrorCode; +import com.example.umc10th.global.apiPayload.exception.ProjectException; + +public class MemberException extends ProjectException { + public MemberException(BaseErrorCode code) { + super(code); } } diff --git a/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java b/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java index 135bd22..66ecdee 100644 --- a/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java +++ b/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java @@ -1,4 +1,20 @@ package com.example.umc10th.domain.member.exception.code; -public enum MemberErrorCode { +import com.example.umc10th.global.apiPayload.code.BaseErrorCode; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpStatus; + +@RequiredArgsConstructor +@Getter +public enum MemberErrorCode implements BaseErrorCode { + + MEMBER_NOT_FOUND(HttpStatus.BAD_REQUEST, + "MEMBER404_1", + "사용자가 존재하지 않습니다."), + ; + + private final HttpStatus status; + private final String code; + private final String message; } diff --git a/src/main/java/com/example/umc10th/domain/member/repository/MemberRespository.java b/src/main/java/com/example/umc10th/domain/member/repository/MemberRepository.java similarity index 68% rename from src/main/java/com/example/umc10th/domain/member/repository/MemberRespository.java rename to src/main/java/com/example/umc10th/domain/member/repository/MemberRepository.java index 39385ec..64887e5 100644 --- a/src/main/java/com/example/umc10th/domain/member/repository/MemberRespository.java +++ b/src/main/java/com/example/umc10th/domain/member/repository/MemberRepository.java @@ -5,6 +5,8 @@ import java.util.Optional; -public interface MemberRespository extends JpaRepository { +public interface MemberRepository extends JpaRepository { Optional findByNameAndDeletedAtIsNull(String name); + + Optional findByEmail(String email); } diff --git a/src/main/java/com/example/umc10th/domain/member/service/MemberService.java b/src/main/java/com/example/umc10th/domain/member/service/MemberService.java index 54d1d49..2916b39 100644 --- a/src/main/java/com/example/umc10th/domain/member/service/MemberService.java +++ b/src/main/java/com/example/umc10th/domain/member/service/MemberService.java @@ -4,9 +4,10 @@ import com.example.umc10th.domain.member.dto.MemberReqDTO; import com.example.umc10th.domain.member.dto.MemberResDTO; import com.example.umc10th.domain.member.entity.Member; -import com.example.umc10th.domain.member.repository.MemberRespository; +import com.example.umc10th.domain.member.repository.MemberRepository; import com.example.umc10th.global.apiPayload.code.GeneralErrorCode; import lombok.RequiredArgsConstructor; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -15,7 +16,8 @@ @Transactional(readOnly = true) public class MemberService { - private final MemberRespository memberRepository; + private final MemberRepository memberRepository; + private final PasswordEncoder passwordEncoder; public MemberResDTO.GetInfo getInfo(MemberReqDTO.GetInfo dto) { // TODO: 구현 예정 @@ -32,7 +34,10 @@ public MemberResDTO.GetInfo getMyPage(Long memberId) { @Transactional public MemberResDTO.SignUp signUp(MemberReqDTO.SignUp dto) { - // TODO: 구현 예정 - return null; + String encodedPassword = passwordEncoder.encode(dto.password()); + Member member = MemberConverter.toMember(dto, encodedPassword); + Member savedMember = memberRepository.save(member); + + return MemberConverter.toSignUp(savedMember); } } diff --git a/src/main/java/com/example/umc10th/domain/mission/service/MissionService.java b/src/main/java/com/example/umc10th/domain/mission/service/MissionService.java index ee8cfbb..0e49e70 100644 --- a/src/main/java/com/example/umc10th/domain/mission/service/MissionService.java +++ b/src/main/java/com/example/umc10th/domain/mission/service/MissionService.java @@ -1,7 +1,7 @@ package com.example.umc10th.domain.mission.service; import com.example.umc10th.domain.member.entity.Member; -import com.example.umc10th.domain.member.repository.MemberRespository; +import com.example.umc10th.domain.member.repository.MemberRepository; import com.example.umc10th.domain.mission.converter.MissionConverter; import com.example.umc10th.domain.mission.dto.MissionReqDTO; import com.example.umc10th.domain.mission.dto.MissionResDTO; @@ -31,7 +31,7 @@ public class MissionService { private final MissionRespository missionRepository; private final MemberMissionRepository memberMissionRepository; - private final MemberRespository memberRepository; + private final MemberRepository memberRepository; private final StoreRepository storeRepository; // 홈 화면 diff --git a/src/main/java/com/example/umc10th/domain/review/service/ReviewService.java b/src/main/java/com/example/umc10th/domain/review/service/ReviewService.java index 88c5a65..c117e70 100644 --- a/src/main/java/com/example/umc10th/domain/review/service/ReviewService.java +++ b/src/main/java/com/example/umc10th/domain/review/service/ReviewService.java @@ -1,8 +1,7 @@ package com.example.umc10th.domain.review.service; import com.example.umc10th.domain.member.entity.Member; -import com.example.umc10th.domain.member.repository.MemberRespository; -import com.example.umc10th.domain.mission.entity.Mission; +import com.example.umc10th.domain.member.repository.MemberRepository; import com.example.umc10th.domain.mission.entity.Store; import com.example.umc10th.domain.mission.repository.StoreRepository; import com.example.umc10th.domain.review.converter.ReviewConverter; @@ -28,7 +27,7 @@ public class ReviewService { private final ReviewRepository reviewRepository; - private final MemberRespository memberRepository; + private final MemberRepository memberRepository; private final StoreRepository storeRepository; // 리뷰 작성 diff --git a/src/main/java/com/example/umc10th/global/config/SecurityConfig.java b/src/main/java/com/example/umc10th/global/config/SecurityConfig.java new file mode 100644 index 0000000..4c68605 --- /dev/null +++ b/src/main/java/com/example/umc10th/global/config/SecurityConfig.java @@ -0,0 +1,66 @@ +package com.example.umc10th.global.config; + +import com.example.umc10th.global.security.exception.CustomAccessDenied; +import com.example.umc10th.global.security.exception.CustomEntryPoint; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +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.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.SecurityFilterChain; + +@EnableWebSecurity +@Configuration +public class SecurityConfig { + + private final String[] allowUris = { + // Swagger 허용 + "/swagger-ui/**", + "/swagger-resources/**", + "/v3/api-docs/**", + "/api/v1/auth/signup" + }; + + @Bean + public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + http + .csrf(AbstractHttpConfigurer::disable) + .authorizeHttpRequests(requests -> requests + .requestMatchers(allowUris).permitAll() + .anyRequest().authenticated() + ) + .formLogin(form -> form + .defaultSuccessUrl("/swagger-ui/index.html", true) + .permitAll() + ) + .logout(logout -> logout + .logoutUrl("/logout") + .logoutSuccessUrl("/login?logout") + .permitAll() + ) + .exceptionHandling(exception->exception + .accessDeniedHandler(customAccessDenied()) + .authenticationEntryPoint(customEntryPoint()) + ) + ; + + return http.build(); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Bean + public CustomAccessDenied customAccessDenied() { + return new CustomAccessDenied(); + } + + @Bean + public CustomEntryPoint customEntryPoint() { + return new CustomEntryPoint(); + } +} diff --git a/src/main/java/com/example/umc10th/global/security/entity/AuthMember.java b/src/main/java/com/example/umc10th/global/security/entity/AuthMember.java new file mode 100644 index 0000000..ec2844d --- /dev/null +++ b/src/main/java/com/example/umc10th/global/security/entity/AuthMember.java @@ -0,0 +1,34 @@ +package com.example.umc10th.global.security.entity; + +import com.example.umc10th.domain.member.entity.Member; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.jspecify.annotations.Nullable; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; +import java.util.List; + +@Getter +@RequiredArgsConstructor +public class AuthMember implements UserDetails { + + private final Member member; + + + @Override + public Collection getAuthorities() { + return List.of(); + } + + @Override + public @Nullable String getPassword() { + return member.getPassword(); + } + + @Override + public String getUsername() { + return member.getEmail(); + } +} diff --git a/src/main/java/com/example/umc10th/global/security/exception/CustomAccessDenied.java b/src/main/java/com/example/umc10th/global/security/exception/CustomAccessDenied.java new file mode 100644 index 0000000..11efb2a --- /dev/null +++ b/src/main/java/com/example/umc10th/global/security/exception/CustomAccessDenied.java @@ -0,0 +1,35 @@ +package com.example.umc10th.global.security.exception; + +import com.example.umc10th.global.apiPayload.ApiResponse; +import com.example.umc10th.global.apiPayload.code.BaseErrorCode; +import com.example.umc10th.global.apiPayload.code.GeneralErrorCode; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.access.AccessDeniedHandler; + +import java.io.IOException; + +public class CustomAccessDenied implements AccessDeniedHandler { + + @Override + public void handle( + HttpServletRequest request, + HttpServletResponse response, + AccessDeniedException accessDeniedException + ) throws IOException { + ObjectMapper objectMapper = new ObjectMapper(); + BaseErrorCode code = GeneralErrorCode.FORBIDDEN; + + // 응답 Content-Type, HTTP 상태코드 정의 + response.setContentType("application/json;charset=UTF-8"); + response.setStatus(code.getStatus().value()); + + // Response Body에 응답통일한 객체를 넣기 + ApiResponse errorResponse = ApiResponse.onFailureEntity(code, null).getBody(); + + // 실제 Response로 덮어쓰기 + objectMapper.writeValue(response.getOutputStream(), errorResponse); + } +} diff --git a/src/main/java/com/example/umc10th/global/security/exception/CustomEntryPoint.java b/src/main/java/com/example/umc10th/global/security/exception/CustomEntryPoint.java new file mode 100644 index 0000000..f802528 --- /dev/null +++ b/src/main/java/com/example/umc10th/global/security/exception/CustomEntryPoint.java @@ -0,0 +1,35 @@ +package com.example.umc10th.global.security.exception; + +import com.example.umc10th.global.apiPayload.ApiResponse; +import com.example.umc10th.global.apiPayload.code.BaseErrorCode; +import com.example.umc10th.global.apiPayload.code.GeneralErrorCode; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; + +import java.io.IOException; + +public class CustomEntryPoint implements AuthenticationEntryPoint { + + @Override + public void commence( + HttpServletRequest request, + HttpServletResponse response, + AuthenticationException authException + ) throws IOException { + ObjectMapper objectMapper = new ObjectMapper(); + BaseErrorCode code = GeneralErrorCode.UNAUTHORIZED; + + // 응답 Content-Type, HTTP 상태코드 정의 + response.setContentType("application/json;charset=UTF-8"); + response.setStatus(code.getStatus().value()); + + // Response Body에 응답통일한 객체를 넣기 + ApiResponse errorResponse = ApiResponse.onFailureEntity(code, null).getBody(); + + // 실제 Response로 덮어쓰기 + objectMapper.writeValue(response.getOutputStream(), errorResponse); + } +} diff --git a/src/main/java/com/example/umc10th/global/security/service/CustomUserDetailsService.java b/src/main/java/com/example/umc10th/global/security/service/CustomUserDetailsService.java new file mode 100644 index 0000000..4176178 --- /dev/null +++ b/src/main/java/com/example/umc10th/global/security/service/CustomUserDetailsService.java @@ -0,0 +1,26 @@ +package com.example.umc10th.global.security.service; + +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.entity.AuthMember; +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; + +@Service +@RequiredArgsConstructor +public class CustomUserDetailsService implements UserDetailsService { + + private final MemberRepository memberRepository; + + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + Member member = memberRepository.findByEmail(username) + .orElseThrow(() -> new MemberException(MemberErrorCode.MEMBER_NOT_FOUND)); + return new AuthMember(member); + } +}