-
Notifications
You must be signed in to change notification settings - Fork 0
[Minsu] Week8 미션 #96
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: Minsu
Are you sure you want to change the base?
[Minsu] Week8 미션 #96
Changes from all commits
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,21 @@ | ||
| ## Spring Security가 무엇인가? | ||
|
|
||
| Spring Security는 Spring 기반 애플리케이션에서 인증과 인가를 처리하는 보안 프레임워크다. | ||
| 요청이 Controller에 도달하기 전에 Security Filter Chain을 먼저 통과하면서 로그인 여부와 권한을 검사한다. | ||
| 개발자는 `SecurityFilterChain`, `UserDetailsService`, `PasswordEncoder` 같은 구성 요소를 조합해 보안 흐름을 설정한다. | ||
| 폼 로그인에서는 사용자가 입력한 이메일과 비밀번호를 기반으로 인증하고, 성공한 인증 정보는 `SecurityContext`에 저장된다. | ||
|
|
||
| ## 인증(Authentication)vs 인가(Authorization) | ||
|
|
||
| 인증(Authentication)은 “이 사용자가 누구인가?”를 확인하는 과정이다. | ||
| 예를 들어 이메일과 비밀번호로 로그인해서 DB에 저장된 회원인지 확인하는 흐름이 인증이다. | ||
| 인가(Authorization)는 “인증된 사용자가 이 기능에 접근할 권한이 있는가?”를 확인하는 과정이다. | ||
| 로그인하지 않은 사용자가 Private API에 접근하면 인증 실패로 401 응답이 나가고, 권한이 부족하면 인가 실패로 403 응답이 나간다. | ||
| Spring Security에서는 인증 후 생성된 `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. 인증/인가 차이를 401/403과 연결한 점이 좋습니다. 여기에 |
||
|
|
||
| ## Stateful vs Stateless | ||
|
|
||
| Stateful은 서버가 사용자의 로그인 상태를 세션 등에 저장하는 방식이다. | ||
| 폼 로그인은 기본적으로 Stateful 방식이며, 로그인 성공 후 서버 세션을 통해 사용자를 계속 식별한다. | ||
| Stateless는 서버가 로그인 상태를 저장하지 않고, 클라이언트가 매 요청마다 토큰 같은 인증 정보를 보내는 방식이다. | ||
| JWT 인증은 대표적인 Stateless 방식으로, 서버 확장에는 유리하지만 토큰 관리와 만료 처리가 중요하다. | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,62 @@ | ||
| package com.example.umc10th.global.config; | ||
|
|
||
| import com.example.umc10th.global.security.CustomAccessDeniedHandler; | ||
| import com.example.umc10th.global.security.CustomAuthenticationEntryPoint; | ||
| import lombok.RequiredArgsConstructor; | ||
| 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; | ||
|
|
||
| @Configuration | ||
| @EnableWebSecurity | ||
| @RequiredArgsConstructor | ||
| public class SecurityConfig { | ||
|
|
||
| private static final String[] PUBLIC_URIS = { | ||
| "/auth/**", | ||
| "/login", | ||
| "/logout", | ||
| "/swagger-ui/**", | ||
| "/swagger-resources/**", | ||
| "/v3/api-docs/**" | ||
| }; | ||
|
|
||
| private final CustomAuthenticationEntryPoint customAuthenticationEntryPoint; | ||
| private final CustomAccessDeniedHandler customAccessDeniedHandler; | ||
|
|
||
| @Bean | ||
| public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { | ||
| return http | ||
| .csrf(AbstractHttpConfigurer::disable) | ||
| .authorizeHttpRequests(requests -> requests | ||
| .requestMatchers(PUBLIC_URIS).permitAll() | ||
| .anyRequest().authenticated() | ||
|
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.
Useful? React with 👍 / 👎. |
||
| ) | ||
| .formLogin(form -> form | ||
| .usernameParameter("email") | ||
| .passwordParameter("password") | ||
| .defaultSuccessUrl("/swagger-ui/index.html", true) | ||
|
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. REST API 응답을 공통 JSON으로 맞추는 흐름이라면 로그인 성공 시 Swagger로 리다이렉트하는 전략과 API 클라이언트 계약이 섞일 수 있습니다. 학습 단계에서는 formLogin 세션 방식인지 JSON 로그인 API 방식인지 먼저 구분하고, 성공/실패 응답 정책을 한 흐름으로 정리하는 것을 권장합니다. |
||
| .permitAll() | ||
| ) | ||
| .logout(logout -> logout | ||
| .logoutUrl("/logout") | ||
| .logoutSuccessUrl("/login?logout") | ||
| .permitAll() | ||
| ) | ||
| .exceptionHandling(exception -> exception | ||
| .authenticationEntryPoint(customAuthenticationEntryPoint) | ||
| .accessDeniedHandler(customAccessDeniedHandler) | ||
| ) | ||
| .build(); | ||
| } | ||
|
|
||
| @Bean | ||
| public PasswordEncoder passwordEncoder() { | ||
| return new BCryptPasswordEncoder(); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| package com.example.umc10th.global.security; | ||
|
|
||
| import com.example.umc10th.domain.member.entity.Member; | ||
| import lombok.Getter; | ||
| 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 | ||
| public class AuthMember implements UserDetails { | ||
|
|
||
| private final Long memberId; | ||
| private final String email; | ||
| private final String password; | ||
|
|
||
| public AuthMember(Member member) { | ||
| this.memberId = member.getId(); | ||
| this.email = member.getEmail(); | ||
| this.password = member.getPassword(); | ||
| } | ||
|
|
||
| @Override | ||
| public Collection<? extends GrantedAuthority> getAuthorities() { | ||
| return List.of(new SimpleGrantedAuthority("ROLE_USER")); | ||
| } | ||
|
|
||
| @Override | ||
| public String getPassword() { | ||
| return password; | ||
| } | ||
|
|
||
| @Override | ||
| public String getUsername() { | ||
| return email; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| package com.example.umc10th.global.security; | ||
|
|
||
| import com.example.umc10th.global.apiPayload.code.GeneralErrorCode; | ||
| 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 CustomAccessDeniedHandler implements AccessDeniedHandler { | ||
|
|
||
| private final SecurityResponseWriter securityResponseWriter; | ||
|
|
||
| @Override | ||
| public void handle( | ||
| HttpServletRequest request, | ||
| HttpServletResponse response, | ||
| AccessDeniedException accessDeniedException | ||
| ) throws IOException { | ||
| securityResponseWriter.write(response, GeneralErrorCode.FORBIDDEN); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| package com.example.umc10th.global.security; | ||
|
|
||
| import com.example.umc10th.global.apiPayload.code.GeneralErrorCode; | ||
| 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 CustomAuthenticationEntryPoint implements AuthenticationEntryPoint { | ||
|
|
||
| private final SecurityResponseWriter securityResponseWriter; | ||
|
|
||
| @Override | ||
| public void commence( | ||
| HttpServletRequest request, | ||
| HttpServletResponse response, | ||
| AuthenticationException authException | ||
| ) throws IOException { | ||
| securityResponseWriter.write(response, GeneralErrorCode.UNAUTHORIZED); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| package com.example.umc10th.global.security; | ||
|
|
||
| import com.example.umc10th.domain.member.entity.Member; | ||
| import com.example.umc10th.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 username) throws UsernameNotFoundException { | ||
| Member member = memberRepository.findActiveByEmail(username) | ||
| .orElseThrow(() -> new UsernameNotFoundException("회원을 찾을 수 없습니다.")); | ||
|
|
||
| return new AuthMember(member); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| package com.example.umc10th.global.security; | ||
|
|
||
| import com.example.umc10th.global.apiPayload.ApiResponse; | ||
| import com.example.umc10th.global.apiPayload.code.BaseErrorCode; | ||
| import jakarta.servlet.http.HttpServletResponse; | ||
| import lombok.RequiredArgsConstructor; | ||
| import org.springframework.http.MediaType; | ||
| import org.springframework.stereotype.Component; | ||
| import tools.jackson.databind.ObjectMapper; | ||
|
|
||
| import java.io.IOException; | ||
| import java.nio.charset.StandardCharsets; | ||
|
|
||
| @Component | ||
| @RequiredArgsConstructor | ||
| public class SecurityResponseWriter { | ||
|
|
||
| private final ObjectMapper objectMapper; | ||
|
|
||
| public void write(HttpServletResponse response, BaseErrorCode errorCode) throws IOException { | ||
| response.setStatus(errorCode.getStatus().value()); | ||
| response.setContentType(MediaType.APPLICATION_JSON_VALUE); | ||
| response.setCharacterEncoding(StandardCharsets.UTF_8.name()); | ||
| objectMapper.writeValue(response.getWriter(), ApiResponse.onFailure(errorCode, null)); | ||
| } | ||
| } |
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-boot-starter-validation이 위에서 이미 선언되어 있어 중복 의존성으로 보입니다. 동작에는 큰 문제가 없을 수 있지만, 빌드 파일은 의존성 의도를 보여주는 문서 역할도 하므로 하나만 남기는 것을 권장합니다.