-
Notifications
You must be signed in to change notification settings - Fork 0
[Joonseok] Week8 미션 #82
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: Joonseok
Are you sure you want to change the base?
Changes from all commits
af5813f
73f1ff8
3a7cb85
35efc5b
b9488a1
6ff9a1a
3ff9b52
0b5c5a8
d75b57b
bdb023f
0a95316
88e78d7
1147569
4421d20
d51b5d2
e464abe
c889c52
ab8b900
14d56e1
2338fab
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| - Spring Security가 무엇인가? | ||
|
|
||
| <aside> | ||
| 🌱 | ||
|
|
||
| ### Authentication - Authorization, | ||
| 일반적인 공격으로 보호하는 라이브러리! | ||
|
|
||
| </aside> | ||
|
|
||
| MVC 구조를 사용하여 동기적으로 통신하는 서버와 Webflux를 사용하여 비동기적 반응형 서버 모두 적용 가능하고, 서버를 안전하게 보호하는데에 편리한 기능을 제공합니다. | ||
|
|
||
| Java 17 이상의 JRE에서만 사용 가능하며, 별도의 Spring Security 관련 설정 파일을 두거나, 서버 클래스 로더에 Spring Security를 포함할 필요가 없습니다. | ||
|
|
||
| - 인증(Authentication)vs 인가(Authorization) | ||
|
|
||
| <aside> | ||
| ⚠️ | ||
|
|
||
| ### Auth… 다 똑같은 말 아니에요? | ||
|
|
||
| </aside> | ||
|
|
||
| ### Authentication | ||
|
|
||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 인증은 사용자가 누구인지 확인하는 절차이고, 인가는 인증된 사용자가 특정 리소스나 기능에 접근할 권한을 가지는지 판단하는 절차입니다. 현재 설명은 인증을 ‘등록 기록’ 중심으로, 인가를 ‘발급된 리소스 접근 권한 검증’ 중심으로 표현하여 두 개념의 핵심 경계가 흐려질 수 있습니다. Authentication 객체, Principal, GrantedAuthority가 각각 어떤 역할을 맡는지 함께 정리하면 Spring Security 흐름을 이해하기 좋아집니다. |
||
| 인증이란 의미로, 해당 사용자가 서버에 등록되어 서버의 리소스에 접근할 수 있게 기록해두는 행위입니다. | ||
|
|
||
| ### Authorization | ||
|
|
||
| 인가라는 의미로, 해당 사용자가 서버에서 발급한 리소스 접근 권한이 유효한지 검증하는 행위입니다. | ||
|
|
||
| ### 가장 일반적인 경우… | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. UsernamePasswordAuthenticationFilter는 주로 formLogin 기반 인증 흐름에서 동작하는 필터입니다. ID와 Password를 사용하더라도 JSON 로그인, JWT 발급, 커스텀 필터를 사용하는 구조에서는 다른 필터나 AuthenticationProvider 설계가 필요할 수 있습니다. 필터 체인에서 인증 요청이 AuthenticationManager와 AuthenticationProvider로 위임되는 흐름까지 함께 조사하는 것을 권장합니다. |
||
|
|
||
| 현대 서비스의 경우, 철저히 관리되는 대규모 SNS 서비스의 보안을 신뢰하며, 소셜 로그인 기능을 주력으로 밀고 나가는 경우도 많지만, 보편적이며 레거시에서는 ID와 Password를 기반으로 인증을 구현합니다. | ||
|
|
||
| 이 경우에는, UsernamePasswordAuthenticationFilter라는 필터 체인 단계에서 체크하게 됩니다. | ||
|
|
||
| - Stateful vs Stateless | ||
|
|
||
| <aside> | ||
| <img src="/icons/database_gray.svg" alt="/icons/database_gray.svg" width="40px" /> | ||
|
|
||
| ### Session과 Token | ||
|
|
||
| </aside> | ||
|
|
||
| ### stateful | ||
|
|
||
| 서버가 `stateful` 하다는 의미는, 서버에서 사용자를 기억한다는 의미입니다. Session 기반으로 재인증하는 서버가 이와 같이 작동하며, 온라인 뱅킹과 같이 이전 트랜잭션에 영향을 받을 수 있는 서비스의 경우 사용합니다. | ||
|
|
||
| 상태 저장 트랜잭션이 중단되더라도 컨텍스트와 기록이 중단된 부분부터 복구할 수 있습니다. | ||
|
|
||
| ### stateless | ||
|
|
||
| `stateless`는 서버가 사용자를 기억하지 못한다는 것입니다. stateful은 전체 서비스 흐름에서 매 API 요청을 일종의 트랜잭션처럼 사용하여 사용자가 현재 어디까지 요청하였는지 확인할 수 있지만, stateless는 모든 API 요청에 대해 이전에 진행된 요청을 저장하지 않습니다. | ||
|
|
||
| - RESTful | ||
| - Micro Service Archtecture | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -36,6 +36,9 @@ public class User extends BaseEntity { | |
| @Column(nullable = false) | ||
| private String email; | ||
|
|
||
| @Column(nullable = false) | ||
| private String password; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 비밀번호 필드를 엔터티에 추가한 점은 인증 구현에 필요하지만, 저장 시점에는 반드시 |
||
|
|
||
| @Column(nullable = false) | ||
| private String phoneNum; | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 회원가입 저장 전에 이메일 중복 여부를 확인하는 흐름이 필요합니다. |
||
| } | ||
|
|
||
| public GetMyPageRes getMyPage(Long userId) { | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 = "선호 음식 배열은 비어있을 수 없습니다.") | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| List<Food> foodList, | ||
|
|
||
| @NotBlank(message = "이메일 필드는 비어있을 수 없습니다.") @Email(message = "이메일 형식이 맞지 않습니다.") | ||
| String email, | ||
|
|
||
| @NotBlank(message = "비밀번호 필드는 비어있을 수 없습니다.") | ||
| String password, | ||
|
|
||
| @NotBlank(message = "전화번호 필드는 비어있을 수 없습니다.") | ||
| String phoneNum | ||
| ) { | ||
|
|
||
| public record Agree( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| Boolean age, | ||
| Boolean service, | ||
| Boolean privacy, | ||
| Boolean location, | ||
| Boolean marketing | ||
| ) {} | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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(); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<Void> errorResponse = ApiResponse.onFailure(errorCode, null); | ||
|
|
||
| response.getWriter().write(objectMapper.writeValueAsString(errorResponse)); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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(); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 현재 권한 목록을 항상 빈 리스트로 반환하면 |
||
| } | ||
|
|
||
| @Override | ||
| public @Nullable String getPassword() { | ||
| return user.getPassword(); | ||
| } | ||
|
|
||
| @Override | ||
| public String getUsername() { | ||
| return user.getEmail(); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Spring Security와 Java 버전의 관계는 사용 중인 Spring Boot/Security 버전에 따라 달라집니다. 예를 들어 Spring Security 6 계열은 Java 17 기준으로 이해할 수 있지만, 모든 Spring Security가 Java 17 이상에서만 동작한다고 일반화하면 개념이 부정확해질 수 있습니다. 또한 보안 설정 파일이 필요 없다는 표현은 현재 SecurityConfig를 작성한 구조와도 충돌하므로, 자동 설정과 명시적 SecurityFilterChain 설정의 차이를 함께 정리하는 것을 권장합니다.