From d467e00c8523e449677177b51cf5d2a6746bfd47 Mon Sep 17 00:00:00 2001 From: Eunseo Kim Date: Sun, 24 May 2026 21:42:00 +0900 Subject: [PATCH 1/3] mission/#8 --- build.gradle | 4 ++ .../umc10th/domain/member/entity/Member.java | 3 + .../member/repository/MemberRepository.java | 5 +- .../domain/member/service/MemberService.java | 3 + .../umc10th/global/config/SecurityConfig.java | 67 +++++++++++++++++++ .../global/security/entity/AuthMember.java | 33 +++++++++ .../handler/CustomAccessDeniedHandler.java | 35 ++++++++++ .../CustomAuthenticationEntryPoint.java | 35 ++++++++++ .../service/CustomUserDetailsService.java | 30 +++++++++ 9 files changed, 214 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/example/umc10th/global/config/SecurityConfig.java create mode 100644 src/main/java/com/example/umc10th/global/security/entity/AuthMember.java create mode 100644 src/main/java/com/example/umc10th/global/security/handler/CustomAccessDeniedHandler.java create mode 100644 src/main/java/com/example/umc10th/global/security/handler/CustomAuthenticationEntryPoint.java create mode 100644 src/main/java/com/example/umc10th/global/security/service/CustomUserDetailsService.java diff --git a/build.gradle b/build.gradle index 0cccfcd..d803a8d 100644 --- a/build.gradle +++ b/build.gradle @@ -32,6 +32,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/domain/member/entity/Member.java b/src/main/java/com/example/umc10th/domain/member/entity/Member.java index fc9cac3..f77becc 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 @@ -64,6 +64,9 @@ public class Member extends BaseEntity { @Column(name = "email", nullable = false, length = 50) private String email; + @Column(name = "password", nullable = false) + private String password; + @Column(name = "phone_number", length = 11) private String phoneNumber; diff --git a/src/main/java/com/example/umc10th/domain/member/repository/MemberRepository.java b/src/main/java/com/example/umc10th/domain/member/repository/MemberRepository.java index 6680311..55aeda8 100644 --- a/src/main/java/com/example/umc10th/domain/member/repository/MemberRepository.java +++ b/src/main/java/com/example/umc10th/domain/member/repository/MemberRepository.java @@ -2,7 +2,10 @@ import com.example.umc10th.domain.member.entity.Member; import org.springframework.data.jpa.repository.JpaRepository; -// JpaRepository: save(), findById(), findAll(), delete() 등 기본 CRUD 기능 자동 제공 +import java.util.Optional; + +// JpaRepository: save(), findById(), findAll(), delete() 등 기본 CRUD 기능 자동 제공 public interface MemberRepository extends JpaRepository { + 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 bbf2dac..fd5d1f8 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 @@ -25,6 +25,7 @@ import com.example.umc10th.domain.mission.repository.MissionRepository; import com.example.umc10th.global.enums.Address; import lombok.RequiredArgsConstructor; +import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service; @@ -46,6 +47,7 @@ public class MemberService { private final MemberTermRepository memberTermRepository; private final MemberMissionRepository memberMissionRepository; private final MissionRepository missionRepository; + private final PasswordEncoder passwordEncoder; public MemberResDTO.MyPage getMyPage() { Member member = memberRepository.findById(CURRENT_MEMBER_ID) @@ -59,6 +61,7 @@ public MemberResDTO.SignUp signUp(MemberReqDTO.SignUp dto) { .name(dto.name()) .nickname(dto.nickname()) .email(dto.email()) + .password(passwordEncoder.encode(dto.password())) .socialUid(dto.email()) .socialType(SocialType.KAKAO) .gender(parseGender(dto.gender())) 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..2f480e1 --- /dev/null +++ b/src/main/java/com/example/umc10th/global/config/SecurityConfig.java @@ -0,0 +1,67 @@ +package com.example.umc10th.global.config; + +import com.example.umc10th.global.security.handler.CustomAccessDeniedHandler; +import com.example.umc10th.global.security.handler.CustomAuthenticationEntryPoint; +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/**" + }; + + @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(customAccessDeniedHandler()) + .authenticationEntryPoint(customAuthenticationEntryPoint()) + ); + + return http.build(); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Bean + public CustomAccessDeniedHandler customAccessDeniedHandler() { + return new CustomAccessDeniedHandler(); + } + + @Bean + public CustomAuthenticationEntryPoint customAuthenticationEntryPoint() { + return new CustomAuthenticationEntryPoint(); + } +} 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..3621047 --- /dev/null +++ b/src/main/java/com/example/umc10th/global/security/entity/AuthMember.java @@ -0,0 +1,33 @@ +package com.example.umc10th.global.security.entity; + +import com.example.umc10th.domain.member.entity.Member; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.lang.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(); + } +} \ No newline at end of file diff --git a/src/main/java/com/example/umc10th/global/security/handler/CustomAccessDeniedHandler.java b/src/main/java/com/example/umc10th/global/security/handler/CustomAccessDeniedHandler.java new file mode 100644 index 0000000..86701a0 --- /dev/null +++ b/src/main/java/com/example/umc10th/global/security/handler/CustomAccessDeniedHandler.java @@ -0,0 +1,35 @@ +package com.example.umc10th.global.security.handler; + +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 CustomAccessDeniedHandler 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.onFailure(code, null); + + // 실제 Response로 덮어쓰기 + objectMapper.writeValue(response.getOutputStream(), errorResponse); + } +} diff --git a/src/main/java/com/example/umc10th/global/security/handler/CustomAuthenticationEntryPoint.java b/src/main/java/com/example/umc10th/global/security/handler/CustomAuthenticationEntryPoint.java new file mode 100644 index 0000000..7ca1cdb --- /dev/null +++ b/src/main/java/com/example/umc10th/global/security/handler/CustomAuthenticationEntryPoint.java @@ -0,0 +1,35 @@ +package com.example.umc10th.global.security.handler; + +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 CustomAuthenticationEntryPoint 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.onFailure(code, null); + + // 실제 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..fa9de84 --- /dev/null +++ b/src/main/java/com/example/umc10th/global/security/service/CustomUserDetailsService.java @@ -0,0 +1,30 @@ +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); + } +} From b15588290d58df2c0672fd2bdd56313e6f02951d Mon Sep 17 00:00:00 2001 From: Eunseo Kim Date: Sun, 24 May 2026 22:14:44 +0900 Subject: [PATCH 2/3] =?UTF-8?q?feat:=20Spring=20Security=20=ED=8F=BC=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EB=B0=8F=20=EC=9D=B8=EC=A6=9D/?= =?UTF-8?q?=EC=9D=B8=EA=B0=80=20=EC=98=88=EC=99=B8=20=EC=B2=98=EB=A6=AC=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/security/service/CustomUserDetailsService.java | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) 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 index fa9de84..e2cf267 100644 --- a/src/main/java/com/example/umc10th/global/security/service/CustomUserDetailsService.java +++ b/src/main/java/com/example/umc10th/global/security/service/CustomUserDetailsService.java @@ -1,8 +1,6 @@ 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; @@ -23,7 +21,7 @@ public UserDetails loadUserByUsername( ) throws UsernameNotFoundException { Member member = memberRepository.findByEmail(username) - .orElseThrow(() -> new MemberException(MemberErrorCode.MEMBER_NOT_FOUND)); + .orElseThrow(() -> new UsernameNotFoundException("존재하지 않는 회원입니다.")); return new AuthMember(member); } From 90a9b47a06e491bc5f2395cb68c9b789624af5cc Mon Sep 17 00:00:00 2001 From: Eunseo Kim Date: Sun, 24 May 2026 22:15:28 +0900 Subject: [PATCH 3/3] =?UTF-8?q?fix:=20=EC=9D=B4=EB=A9=94=EC=9D=BC=20?= =?UTF-8?q?=EC=A4=91=EB=B3=B5=20=EA=B2=80=EC=A6=9D=20=EB=B0=8F=20unique=20?= =?UTF-8?q?=EC=A0=9C=EC=95=BD=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/example/umc10th/domain/member/entity/Member.java | 2 +- .../umc10th/domain/member/repository/MemberRepository.java | 1 + .../example/umc10th/domain/member/service/MemberService.java | 4 ++++ 3 files changed, 6 insertions(+), 1 deletion(-) 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 f77becc..be38519 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 @@ -61,7 +61,7 @@ public class Member extends BaseEntity { @Column(name = "point", nullable = false) private Integer point = 0; - @Column(name = "email", nullable = false, length = 50) + @Column(name = "email", nullable = false, unique = true, length = 50) private String email; @Column(name = "password", nullable = false) diff --git a/src/main/java/com/example/umc10th/domain/member/repository/MemberRepository.java b/src/main/java/com/example/umc10th/domain/member/repository/MemberRepository.java index 55aeda8..ab0b082 100644 --- a/src/main/java/com/example/umc10th/domain/member/repository/MemberRepository.java +++ b/src/main/java/com/example/umc10th/domain/member/repository/MemberRepository.java @@ -8,4 +8,5 @@ // JpaRepository: save(), findById(), findAll(), delete() 등 기본 CRUD 기능 자동 제공 public interface MemberRepository extends JpaRepository { Optional findByEmail(String email); + boolean existsByEmail(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 fd5d1f8..e86c67e 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 @@ -57,6 +57,10 @@ public MemberResDTO.MyPage getMyPage() { @Transactional public MemberResDTO.SignUp signUp(MemberReqDTO.SignUp dto) { + if (memberRepository.existsByEmail(dto.email())) { + throw new MemberException(MemberErrorCode.MEMBER_EMAIL_DUPLICATED); + } + Member member = Member.builder() .name(dto.name()) .nickname(dto.nickname())