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 @@ -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
2 changes: 2 additions & 0 deletions src/main/java/com/example/umc10th/Umc10thApplication.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -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();
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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
) {}
}

Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
Original file line number Diff line number Diff line change
@@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

import java.util.Optional;

public interface MemberRespository extends JpaRepository<Member, Integer> {
public interface MemberRepository extends JpaRepository<Member, Integer> {
Optional<Member> findByNameAndDeletedAtIsNull(String name);

Optional<Member> findByEmail(String email);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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: 구현 예정
Expand All @@ -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);
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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;

// 홈 화면
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -28,7 +27,7 @@
public class ReviewService {

private final ReviewRepository reviewRepository;
private final MemberRespository memberRepository;
private final MemberRepository memberRepository;
private final StoreRepository storeRepository;

// 리뷰 작성
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
@@ -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<? extends GrantedAuthority> getAuthorities() {
return List.of();
}

@Override
public @Nullable 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,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<Void> errorResponse = ApiResponse.<Void>onFailureEntity(code, null).getBody();

// 실제 Response로 덮어쓰기
objectMapper.writeValue(response.getOutputStream(), errorResponse);
}
}
Original file line number Diff line number Diff line change
@@ -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<Void> errorResponse = ApiResponse.<Void>onFailureEntity(code, null).getBody();

// 실제 Response로 덮어쓰기
objectMapper.writeValue(response.getOutputStream(), errorResponse);
}
Comment on lines +14 to +34
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

지금 저희가 CustomEntryPoint를 만든 이유부터 생각해 보면 좋을 것 같습니다.

만약 컨트롤러나 서비스에서 예외가 발생했다면 @RestControllerAdvice를 활용해서 만들어두신 GeneralExceptionAdvice가 바로 잡아 냈을 것입니다. 그러나 시큐리티 필터에서 에러가 났다면 RestControllerAdvice가 잡아낼 수 없기 때문에 CustomEntryPoint를 만들고 ObjectMapper로 JSON을 만들어 클라이언트에게 반환하게 해두신 거죠.

그런데 여기서 한 가지 아쉬운 점이 있습니다. CustomEntryPoint에서 ObjectMapper로 직접 JSON 응답을 만들고 있잖아요? 이러면 에러 응답 포맷을 바꿀 때 GeneralExceptionAdvice랑 CustomEntryPoint 두 군데를 다 수정해야 합니다.

이걸 해결하는 방법으로 HandlerExceptionResolver를 CustomEntryPoint에 주입하는 방식이 있습니다. 필터에서 터진 예외를 resolver.resolveException()으로 MVC 쪽에 토스해버리면, 우리가 이미 만들어둔 @RestControllerAdvice가 그 예외를 잡아서 처리해주니까 에러 응답 로직이 한 곳으로 모이게 됩니다.

구현은 GeneralExceptionAdvice에 AuthenticationException과 AccessDeniedException 전용 핸들러를 추가하는 방식으로 하면 될 것 같습니다! 참고로 CustomAccessDenied도 같은 구조라서, 리팩토링할 때 같이 적용하면 될 것 같습니다~

}
Loading