Skip to content
Merged
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 @@ -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') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.example.Spring_Boot.domain.member.exception.code.MemberSuccessCode;
import com.example.Spring_Boot.domain.member.service.MemberService;
import com.example.Spring_Boot.global.apiPayload.ApiResponse;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
Expand Down Expand Up @@ -34,8 +35,8 @@ public ApiResponse<MemberResDTO.MyPageResponse> getMyPage(

@PostMapping
public ApiResponse<MemberResDTO.CreateMemberResponse> createMember(
@RequestBody MemberReqDTO.CreateMemberRequest request
) {
@Valid @RequestBody MemberReqDTO.CreateMemberRequest request
) {
MemberResDTO.CreateMemberResponse response = memberService.createMember(request);

return ApiResponse.onSuccess(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package com.example.Spring_Boot.domain.member.converter;

import com.example.Spring_Boot.domain.member.dto.MemberReqDTO;
import com.example.Spring_Boot.domain.member.dto.MemberResDTO;
import com.example.Spring_Boot.domain.member.entity.Member;

import java.util.List;

public class MemberConverter {

private MemberConverter() {
Expand All @@ -13,10 +16,40 @@ public static MemberResDTO.MyPageResponse toMyPageResponse(Member member) {
.userId(member.getUserId())
.name(member.getName())
.nickname(member.getNickname())
.email(null)
.email(member.getEmail())
.phoneNumber(member.getPhoneNumber())
.point(member.getPoint())
.socialProvider(member.getSocialProvider().name())
.build();
}

public static Member toMember(MemberReqDTO.CreateMemberRequest request, String encodedPassword) {
return Member.builder()
.email(request.email())
.password(encodedPassword)
.name(request.name())
.nickname(request.nickname())
.phoneNumber(request.phoneNumber())
.gender(request.gender())
.birth(request.birth())
.address(request.address())
.build();
}

public static MemberResDTO.CreateMemberResponse toCreateMemberResponse(
Member member,
List<Long> categoryIds
) {
return MemberResDTO.CreateMemberResponse.builder()
.userId(member.getUserId())
.email(member.getEmail())
.name(member.getName())
.nickname(member.getNickname())
.phoneNumber(member.getPhoneNumber())
.gender(member.getGender())
.birth(member.getBirth())
.address(member.getAddress())
.categoryIds(categoryIds)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package com.example.Spring_Boot.domain.member.dto;

import com.example.Spring_Boot.domain.member.enums.Gender;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Builder;

import java.time.LocalDate;
Expand All @@ -10,9 +14,33 @@ public class MemberReqDTO {

@Builder
public record CreateMemberRequest(
@NotBlank(message = "이메일은 필수입니다.")
@Email(message = "이메일 형식이 올바르지 않습니다.")
String email,

@NotBlank(message = "비밀번호는 필수입니다.")
@Size(min = 8, max = 30, message = "비밀번호는 8자 이상 30자 이하로 입력해야 합니다.")
String password,

@NotBlank(message = "이름은 필수입니다.")
@Size(max = 5, message = "이름은 5자 이하로 입력해야 합니다.")
String name,

@NotBlank(message = "닉네임은 필수입니다.")
@Size(max = 15, message = "닉네임은 15자 이하로 입력해야 합니다.")
String nickname,

@NotBlank(message = "전화번호는 필수입니다.")
@Size(max = 15, message = "전화번호는 15자 이하로 입력해야 합니다.")
String phoneNumber,

@NotNull(message = "성별은 필수입니다.")
Gender gender,

@NotNull(message = "생년월일은 필수입니다.")
LocalDate birth,

@NotBlank(message = "주소는 필수입니다.")
String address,
List<Long> categoryIds
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,10 @@ public class MemberResDTO {
@Builder
public record CreateMemberResponse(
Long userId,
String email,
String name,
String nickname,
String phoneNumber,
Gender gender,
LocalDate birth,
String address,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ public class Member {
@Column(nullable = false, length = 15)
private String nickname;

@Column(unique = true)
private String email;

private String password;

@Builder.Default
@Enumerated(EnumType.STRING)
@Column(nullable = false)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ public enum MemberErrorCode implements BaseErrorCode {
HttpStatus.NOT_FOUND,
"MEMBER404_1",
"유저를 찾을 수 없습니다."
),
DUPLICATE_EMAIL(
HttpStatus.CONFLICT,
"MEMBER409_1",
"이미 사용 중인 이메일입니다."
);

private final HttpStatus status;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,11 @@
import com.example.Spring_Boot.domain.member.entity.Member;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface MemberRepository extends JpaRepository<Member, Long> {

Optional<Member> findByEmail(String email);

boolean existsByEmail(String email);
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import com.example.Spring_Boot.domain.member.exception.code.MemberErrorCode;
import com.example.Spring_Boot.domain.member.repository.MemberRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

Expand All @@ -16,6 +17,7 @@
public class MemberService {

private final MemberRepository memberRepository;
private final PasswordEncoder passwordEncoder;

@Transactional(readOnly = true)
public MemberResDTO.MyPageResponse getMyPage(String authorization) {
Expand All @@ -25,8 +27,17 @@ public MemberResDTO.MyPageResponse getMyPage(String authorization) {
return MemberConverter.toMyPageResponse(member);
}

@Transactional
public MemberResDTO.CreateMemberResponse createMember(MemberReqDTO.CreateMemberRequest request) {
throw new UnsupportedOperationException("Member creation is not implemented yet.");
if (memberRepository.existsByEmail(request.email())) {
throw new MemberException(MemberErrorCode.DUPLICATE_EMAIL);
}

String encodedPassword = passwordEncoder.encode(request.password());
Member member = MemberConverter.toMember(request, encodedPassword);
Member savedMember = memberRepository.save(member);

return MemberConverter.toCreateMemberResponse(savedMember, request.categoryIds());
}

private Long extractMemberId(String authorization) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
import com.example.Spring_Boot.global.apiPayload.code.GeneralErrorCode;
import com.example.Spring_Boot.global.apiPayload.exception.ProjectException;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
Expand All @@ -15,6 +17,26 @@
@RestControllerAdvice
public class GeneralExceptionAdvice {

// Spring Security 인증 실패 예외 처리
@ExceptionHandler(AuthenticationException.class)
public ResponseEntity<ApiResponse<Void>> handleAuthenticationException(
AuthenticationException e
) {
BaseErrorCode code = GeneralErrorCode.UNAUTHORIZED;
return ResponseEntity.status(code.getStatus())
.body(ApiResponse.onFailure(code, null));
}

// Spring Security 인가 실패 예외 처리
@ExceptionHandler(AccessDeniedException.class)
public ResponseEntity<ApiResponse<Void>> handleAccessDeniedException(
AccessDeniedException e
) {
BaseErrorCode code = GeneralErrorCode.FORBIDDEN;
return ResponseEntity.status(code.getStatus())
.body(ApiResponse.onFailure(code, null));
}

// @Valid 어노테이션 검증 실패 예외 처리
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ApiResponse<Map<String, String>>> handleMethodArgumentNotValidException(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package com.example.Spring_Boot.global.config;

import com.example.Spring_Boot.global.security.handler.CustomAccessDeniedHandler;
import com.example.Spring_Boot.global.security.handler.CustomAuthenticationEntryPoint;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

@EnableWebSecurity
@Configuration
@RequiredArgsConstructor
public class SecurityConfig {

private final CustomAuthenticationEntryPoint customAuthenticationEntryPoint;
private final CustomAccessDeniedHandler customAccessDeniedHandler;

private final String[] allowUris = {
// Swagger 허용
"/swagger-ui/**",
"/swagger-resources/**",
"/v3/api-docs/**",
"/auth/**"
};

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(requests -> requests
.requestMatchers(allowUris).permitAll()
.requestMatchers(HttpMethod.POST, "/api/members").permitAll()
.anyRequest().authenticated()
)
.formLogin(form -> form
.usernameParameter("email")
.passwordParameter("password")
.defaultSuccessUrl("/swagger-ui/index.html", true)
.permitAll()
)
.logout(logout -> logout
.logoutUrl("/logout")
.logoutSuccessUrl("/login?logout")
.permitAll()
)
.exceptionHandling(exception -> exception
.authenticationEntryPoint(customAuthenticationEntryPoint)
.accessDeniedHandler(customAccessDeniedHandler)
);

return http.build();
}

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.example.Spring_Boot.global.security.auth;

import com.example.Spring_Boot.domain.member.entity.Member;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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<? extends GrantedAuthority> getAuthorities() {
return List.of(new SimpleGrantedAuthority("ROLE_USER"));
}

@Override
public String getPassword() {
return member.getPassword();
}

@Override
public String getUsername() {
return member.getEmail();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.example.Spring_Boot.global.security.auth;

import com.example.Spring_Boot.domain.member.repository.MemberRepository;
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 email) throws UsernameNotFoundException {
return memberRepository.findByEmail(email)
.map(AuthMember::new)
.orElseThrow(() -> new UsernameNotFoundException("회원을 찾을 수 없습니다."));
}
}
Loading