diff --git a/Joonseok/build.gradle b/Joonseok/build.gradle
index 55506486..ce009fae 100644
--- a/Joonseok/build.gradle
+++ b/Joonseok/build.gradle
@@ -20,6 +20,8 @@ repositories {
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-webmvc'
+ implementation 'org.springframework.boot:spring-boot-starter-security'
+ testImplementation 'org.springframework.security:spring-security-test'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.mysql:mysql-connector-j'
annotationProcessor 'org.projectlombok:lombok'
diff --git a/Joonseok/keyword_summary/ch08.md b/Joonseok/keyword_summary/ch08.md
new file mode 100644
index 00000000..86b0ac0b
--- /dev/null
+++ b/Joonseok/keyword_summary/ch08.md
@@ -0,0 +1,58 @@
+- Spring Security가 무엇인가?
+
+
+
+ MVC 구조를 사용하여 동기적으로 통신하는 서버와 Webflux를 사용하여 비동기적 반응형 서버 모두 적용 가능하고, 서버를 안전하게 보호하는데에 편리한 기능을 제공합니다.
+
+ Java 17 이상의 JRE에서만 사용 가능하며, 별도의 Spring Security 관련 설정 파일을 두거나, 서버 클래스 로더에 Spring Security를 포함할 필요가 없습니다.
+
+- 인증(Authentication)vs 인가(Authorization)
+
+
+
+ ### Authentication
+
+ 인증이란 의미로, 해당 사용자가 서버에 등록되어 서버의 리소스에 접근할 수 있게 기록해두는 행위입니다.
+
+ ### Authorization
+
+ 인가라는 의미로, 해당 사용자가 서버에서 발급한 리소스 접근 권한이 유효한지 검증하는 행위입니다.
+
+ ### 가장 일반적인 경우…
+
+ 현대 서비스의 경우, 철저히 관리되는 대규모 SNS 서비스의 보안을 신뢰하며, 소셜 로그인 기능을 주력으로 밀고 나가는 경우도 많지만, 보편적이며 레거시에서는 ID와 Password를 기반으로 인증을 구현합니다.
+
+ 이 경우에는, UsernamePasswordAuthenticationFilter라는 필터 체인 단계에서 체크하게 됩니다.
+
+- Stateful vs Stateless
+
+
+
+ ### stateful
+
+ 서버가 `stateful` 하다는 의미는, 서버에서 사용자를 기억한다는 의미입니다. Session 기반으로 재인증하는 서버가 이와 같이 작동하며, 온라인 뱅킹과 같이 이전 트랜잭션에 영향을 받을 수 있는 서비스의 경우 사용합니다.
+
+ 상태 저장 트랜잭션이 중단되더라도 컨텍스트와 기록이 중단된 부분부터 복구할 수 있습니다.
+
+ ### stateless
+
+ `stateless`는 서버가 사용자를 기억하지 못한다는 것입니다. stateful은 전체 서비스 흐름에서 매 API 요청을 일종의 트랜잭션처럼 사용하여 사용자가 현재 어디까지 요청하였는지 확인할 수 있지만, stateless는 모든 API 요청에 대해 이전에 진행된 요청을 저장하지 않습니다.
+
+ - RESTful
+ - Micro Service Archtecture
\ No newline at end of file
diff --git a/Joonseok/src/main/java/com/umc/study/domain/user/entity/User.java b/Joonseok/src/main/java/com/umc/study/domain/user/entity/User.java
index 16add48e..c0caf445 100644
--- a/Joonseok/src/main/java/com/umc/study/domain/user/entity/User.java
+++ b/Joonseok/src/main/java/com/umc/study/domain/user/entity/User.java
@@ -36,6 +36,9 @@ public class User extends BaseEntity {
@Column(nullable = false)
private String email;
+ @Column(nullable = false)
+ private String password;
+
@Column(nullable = false)
private String phoneNum;
diff --git a/Joonseok/src/main/java/com/umc/study/domain/user/exception/code/UserSuccessCode.java b/Joonseok/src/main/java/com/umc/study/domain/user/exception/code/UserSuccessCode.java
index d4da4366..85042040 100644
--- a/Joonseok/src/main/java/com/umc/study/domain/user/exception/code/UserSuccessCode.java
+++ b/Joonseok/src/main/java/com/umc/study/domain/user/exception/code/UserSuccessCode.java
@@ -9,7 +9,7 @@
@AllArgsConstructor
public enum UserSuccessCode implements BaseResponseCode {
- USER_SIGN_IN_CREATED(
+ USER_SIGN_UP_CREATED(
HttpStatus.CREATED,
"USER201_1",
"회원가입에 성공했습니다."
diff --git a/Joonseok/src/main/java/com/umc/study/domain/user/repository/UserRepository.java b/Joonseok/src/main/java/com/umc/study/domain/user/repository/UserRepository.java
index 530877b7..d4248475 100644
--- a/Joonseok/src/main/java/com/umc/study/domain/user/repository/UserRepository.java
+++ b/Joonseok/src/main/java/com/umc/study/domain/user/repository/UserRepository.java
@@ -20,4 +20,5 @@ select count(mh)
""")
long countCompletedMissions(@Param("userId") Long userId);
+ Optional findByEmail(String username);
}
diff --git a/Joonseok/src/main/java/com/umc/study/domain/user/service/UserService.java b/Joonseok/src/main/java/com/umc/study/domain/user/service/UserService.java
index de4576c7..8ca92a0a 100644
--- a/Joonseok/src/main/java/com/umc/study/domain/user/service/UserService.java
+++ b/Joonseok/src/main/java/com/umc/study/domain/user/service/UserService.java
@@ -1,20 +1,26 @@
package com.umc.study.domain.user.service;
+import com.umc.study.domain.mission.entity.Location;
import com.umc.study.domain.mission.entity.Mission;
import com.umc.study.domain.mission.entity.Restaurant;
import com.umc.study.domain.mission.exception.PageOverflowException;
import com.umc.study.domain.mission.repository.MissionRepository;
import com.umc.study.domain.mission.repository.RestaurantRepository;
import com.umc.study.domain.user.entity.User;
+import com.umc.study.domain.user.enums.Role;
import com.umc.study.domain.user.exception.UserNotFoundException;
import com.umc.study.domain.user.repository.UserRepository;
import com.umc.study.domain.user.web.dto.GetHomeRes;
import com.umc.study.domain.user.web.dto.GetMyPageRes;
+import com.umc.study.domain.user.web.dto.SignInReq;
+import com.umc.study.domain.user.web.dto.SignUpReq;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
+import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@@ -25,6 +31,27 @@ public class UserService {
private final UserRepository userRepository;
private final MissionRepository missionRepository;
private final RestaurantRepository restaurantRepository;
+ private final PasswordEncoder encoder;
+
+ @Transactional
+ public void saveUser(SignUpReq request) {
+ User user = User.builder()
+ .name(request.name())
+ .isMale(request.isMale())
+ .birth(request.birth())
+ .role(Role.GUEST)
+ .email(request.email())
+ .password(encoder.encode(request.password()))
+ .phoneNum(request.phoneNum())
+ .pointDeposit(0)
+ .location(Location.builder()
+ .streetAddress(request.address())
+ .detailedAddress(request.detailAddress())
+ .build())
+ .build();
+
+ userRepository.save(user);
+ }
public GetMyPageRes getMyPage(Long userId) {
diff --git a/Joonseok/src/main/java/com/umc/study/domain/user/web/controller/UserController.java b/Joonseok/src/main/java/com/umc/study/domain/user/web/controller/UserController.java
index 93a2991e..f3d78f78 100644
--- a/Joonseok/src/main/java/com/umc/study/domain/user/web/controller/UserController.java
+++ b/Joonseok/src/main/java/com/umc/study/domain/user/web/controller/UserController.java
@@ -4,6 +4,7 @@
import com.umc.study.domain.user.service.UserService;
import com.umc.study.domain.user.web.dto.GetHomeRes;
import com.umc.study.domain.user.web.dto.GetMyPageRes;
+import com.umc.study.domain.user.web.dto.SignUpReq;
import com.umc.study.global.apiPayload.ApiResponse;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Max;
@@ -21,15 +22,17 @@ public class UserController {
private final UserService userService;
- @PostMapping("/sign-in")
- public ResponseEntity> signIn(
- @Valid @RequestBody Object request
+ @PostMapping("/auth/sign-up")
+ public ResponseEntity> signUp(
+ @Valid @RequestBody SignUpReq request
) {
// call Service method
+ userService.saveUser(request);
+
return ResponseEntity
- .status(UserSuccessCode.USER_SIGN_IN_CREATED.getStatus())
- .body(ApiResponse.onComplete(UserSuccessCode.USER_SIGN_IN_CREATED, null));
+ .status(UserSuccessCode.USER_SIGN_UP_CREATED.getStatus())
+ .body(ApiResponse.onComplete(UserSuccessCode.USER_SIGN_UP_CREATED, null));
}
@PostMapping("/my/pref")
diff --git a/Joonseok/src/main/java/com/umc/study/domain/user/web/dto/SignUpReq.java b/Joonseok/src/main/java/com/umc/study/domain/user/web/dto/SignUpReq.java
new file mode 100644
index 00000000..77594e49
--- /dev/null
+++ b/Joonseok/src/main/java/com/umc/study/domain/user/web/dto/SignUpReq.java
@@ -0,0 +1,51 @@
+package com.umc.study.domain.user.web.dto;
+
+import com.umc.study.domain.user.entity.Food;
+import jakarta.validation.constraints.Email;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+
+import java.time.LocalDate;
+import java.util.List;
+
+public record SignUpReq(
+ @NotNull(message = "서비스 이용 동의 필드는 비어있을 수 없습니다.")
+ Agree agree,
+
+ @NotBlank(message = "이름 필드는 비어있을 수 없습니다.")
+ String name,
+
+ @NotNull(message = "성별 필드는 비어있을 수 없습니다.")
+ Boolean isMale,
+
+ @NotNull(message = "생년월일 필드는 비어있을 수 없습니다.")
+ LocalDate birth,
+
+ @NotBlank(message = "주소 필드는 비어있을 수 없습니다.")
+ String address,
+
+ @NotBlank(message = "상세 주소 필드는 비어있을 수 없습니다.")
+ String detailAddress,
+
+ @NotEmpty(message = "선호 음식 배열은 비어있을 수 없습니다.")
+ List foodList,
+
+ @NotBlank(message = "이메일 필드는 비어있을 수 없습니다.") @Email(message = "이메일 형식이 맞지 않습니다.")
+ String email,
+
+ @NotBlank(message = "비밀번호 필드는 비어있을 수 없습니다.")
+ String password,
+
+ @NotBlank(message = "전화번호 필드는 비어있을 수 없습니다.")
+ String phoneNum
+) {
+
+ public record Agree(
+ Boolean age,
+ Boolean service,
+ Boolean privacy,
+ Boolean location,
+ Boolean marketing
+ ) {}
+}
diff --git a/Joonseok/src/main/java/com/umc/study/global/config/SecurityConfig.java b/Joonseok/src/main/java/com/umc/study/global/config/SecurityConfig.java
new file mode 100644
index 00000000..83dfa31d
--- /dev/null
+++ b/Joonseok/src/main/java/com/umc/study/global/config/SecurityConfig.java
@@ -0,0 +1,58 @@
+package com.umc.study.global.config;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.umc.study.global.security.CustomEntryPoint;
+import com.umc.study.global.security.exception.CustomAccessDenied;
+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 {
+
+ @Bean
+ public ObjectMapper objectMapper() {
+ return new ObjectMapper();
+ }
+
+ private final String[] allowUris = {
+ "/api/swagger-ui/**",
+ "/api/swagger-resources/**",
+ "/api/v3/api-docs/**",
+ "/api/auth/**" // sign-up, login request allow
+ };
+
+ @Bean
+ public SecurityFilterChain securityFilterChain(HttpSecurity http, CustomAccessDenied customAccessDenied, CustomEntryPoint customEntryPoint) throws Exception {
+
+ http
+ .csrf(AbstractHttpConfigurer::disable)
+ .authorizeHttpRequests(request -> request
+ .requestMatchers(allowUris).permitAll()
+ .anyRequest().authenticated()
+ )
+ .formLogin(form -> form
+ .defaultSuccessUrl("/api/swagger-ui/index.html", true)
+ .permitAll())
+ .logout(logout -> logout
+ .logoutUrl("/logout")
+ .logoutSuccessUrl("/login?logout")
+ .permitAll())
+ .exceptionHandling(e -> e
+ .accessDeniedHandler(customAccessDenied)
+ .authenticationEntryPoint(customEntryPoint));
+
+ return http.build();
+ }
+
+ @Bean
+ public PasswordEncoder passwordEncoder() {
+ return new BCryptPasswordEncoder();
+ }
+}
diff --git a/Joonseok/src/main/java/com/umc/study/global/security/CustomEntryPoint.java b/Joonseok/src/main/java/com/umc/study/global/security/CustomEntryPoint.java
new file mode 100644
index 00000000..8922a968
--- /dev/null
+++ b/Joonseok/src/main/java/com/umc/study/global/security/CustomEntryPoint.java
@@ -0,0 +1,39 @@
+package com.umc.study.global.security;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.umc.study.global.apiPayload.ApiResponse;
+import com.umc.study.global.apiPayload.code.BaseResponseCode;
+import com.umc.study.global.apiPayload.code.GeneralErrorCode;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.web.AuthenticationEntryPoint;
+import org.springframework.stereotype.Component;
+
+import java.io.IOException;
+
+@Component
+@RequiredArgsConstructor
+public class CustomEntryPoint implements AuthenticationEntryPoint {
+
+ private final ObjectMapper objectMapper;
+
+ @Override
+ public void commence(
+ HttpServletRequest request,
+ HttpServletResponse response,
+ AuthenticationException authException
+ ) throws IOException, ServletException {
+
+ BaseResponseCode errorCode = GeneralErrorCode.UNAUTHORIZED;
+
+ response.setContentType("application/json;charset=UTF-8");
+ response.setStatus(errorCode.getStatus().value());
+
+ ApiResponse errorResponse = ApiResponse.onFailure(errorCode, null);
+
+ response.getWriter().write(objectMapper.writeValueAsString(errorResponse));
+ }
+}
diff --git a/Joonseok/src/main/java/com/umc/study/global/security/entity/AuthUser.java b/Joonseok/src/main/java/com/umc/study/global/security/entity/AuthUser.java
new file mode 100644
index 00000000..b1afe0ca
--- /dev/null
+++ b/Joonseok/src/main/java/com/umc/study/global/security/entity/AuthUser.java
@@ -0,0 +1,33 @@
+package com.umc.study.global.security.entity;
+
+import com.umc.study.domain.user.entity.User;
+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 AuthUser implements UserDetails {
+
+ private final User user;
+
+ @Override
+ public Collection extends GrantedAuthority> getAuthorities() {
+ return List.of();
+ }
+
+ @Override
+ public @Nullable String getPassword() {
+ return user.getPassword();
+ }
+
+ @Override
+ public String getUsername() {
+ return user.getEmail();
+ }
+}
diff --git a/Joonseok/src/main/java/com/umc/study/global/security/exception/CustomAccessDenied.java b/Joonseok/src/main/java/com/umc/study/global/security/exception/CustomAccessDenied.java
new file mode 100644
index 00000000..e8fb2807
--- /dev/null
+++ b/Joonseok/src/main/java/com/umc/study/global/security/exception/CustomAccessDenied.java
@@ -0,0 +1,39 @@
+package com.umc.study.global.security.exception;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.umc.study.global.apiPayload.ApiResponse;
+import com.umc.study.global.apiPayload.code.BaseResponseCode;
+import com.umc.study.global.apiPayload.code.GeneralErrorCode;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import lombok.RequiredArgsConstructor;
+import org.springframework.security.access.AccessDeniedException;
+import org.springframework.security.web.access.AccessDeniedHandler;
+import org.springframework.stereotype.Component;
+
+import java.io.IOException;
+
+@Component
+@RequiredArgsConstructor
+public class CustomAccessDenied implements AccessDeniedHandler {
+
+ private final ObjectMapper objectMapper;
+
+ @Override
+ public void handle(
+ HttpServletRequest request,
+ HttpServletResponse response,
+ AccessDeniedException accessDeniedException
+ ) throws IOException, ServletException {
+
+ BaseResponseCode errorCode = GeneralErrorCode.FORBIDDEN;
+
+ response.setContentType("application/json;charset=UTF-8");
+ response.setStatus(errorCode.getStatus().value());
+
+ ApiResponse errorResponse = ApiResponse.onFailure(errorCode, null);
+
+ response.getWriter().write(objectMapper.writeValueAsString(errorResponse));
+ }
+}
diff --git a/Joonseok/src/main/java/com/umc/study/global/security/service/CustomUserDetailsService.java b/Joonseok/src/main/java/com/umc/study/global/security/service/CustomUserDetailsService.java
new file mode 100644
index 00000000..57c47cff
--- /dev/null
+++ b/Joonseok/src/main/java/com/umc/study/global/security/service/CustomUserDetailsService.java
@@ -0,0 +1,29 @@
+package com.umc.study.global.security.service;
+
+import com.umc.study.domain.user.entity.User;
+import com.umc.study.domain.user.exception.UserNotFoundException;
+import com.umc.study.domain.user.repository.UserRepository;
+import com.umc.study.global.security.entity.AuthUser;
+import lombok.Getter;
+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.Component;
+
+@Getter
+@Component
+@RequiredArgsConstructor
+public class CustomUserDetailsService implements UserDetailsService {
+
+ private final UserRepository userRepository;
+
+ @Override
+ public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
+
+ User user = userRepository.findByEmail(username)
+ .orElseThrow(UserNotFoundException::new);
+
+ return new AuthUser(user);
+ }
+}