회원가입 및 로그인 API는 인증 없이 접근할 수 있도록 허용해야 접근 가능하다.
SEcurityConfig에 추가해줬다.
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable()) // CSRF 비활성화
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)) // 세션 기반 인증 사용
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/users/signup", "/api/users/login").permitAll() // 회원가입, 로그인은 인증 없이 허용
.anyRequest().authenticated()) // 나머지 요청은 인증 필요
.formLogin(login -> login.disable()) // 기본 로그인 폼 비활성화
.httpBasic(httpBasic -> httpBasic.disable()); // HTTP Basic 인증 비활성화
return http.build();
}
이제 정상 동작하는 것을 확인할 수 있다.
로그인 API(/api/users/login)가 인증 없이 접근할 수 있도록 Security 설정을 수정해줬기 때문에, 무엇이 문제인지 엔드포인트를 먼저 확인해보면 된다.
로그인 성공 후 Cookies 탭에서 SESSIONID 쿠키가 있는지 확인해야 다음 단계로 넘어갈 수 있다.
서버가 인증된 요청으로 인식하지 못하고 있음 → 요청이 올바르게 전달되지 않거나 Spring Security 설정에 문제가 있을 가능성이 커보임.
참고로 이런 화살표가 뜨면 절대 안됌.
-> Cookie 헤더를 추가할 때 공백이나 특수문자가 들어가면 문제가 발생한다.
흠,, 뭐가 문제지 ?
SecurityConfig
package com.example.scheduledemo.security;
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.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
public class SecurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable()) // CSRF 비활성화
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)) // 세션 기반 인증 사용
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/users/signup", "/api/users/login").permitAll() // 회원가입, 로그인은 인증 없이 허용
.anyRequest().authenticated()) // 나머지 요청은 인증 필요
.formLogin(login -> login.disable()) // 기본 로그인 폼 비활성화
.httpBasic(httpBasic -> httpBasic.disable()); // HTTP Basic 인증 비활성화
return http.build();
}
}
"/api/users/signup"
과 "/api/users/login"
은 permitAll()
처리했지만, /api/schedules/**
경로는 authenticated() 처리만 되어 있음..csrf(csrf -> csrf.disable())
)는 되어 있지만,@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable()) //CSRF 보호 비활성화 (POST 요청 차단 방지)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)) //세션 기반 인증 유지
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/users/signup", "/api/users/login").permitAll() //회원가입 & 로그인 허용
.requestMatchers("/api/schedules/**").authenticated() //일정 관련 요청 인증 필요
.anyRequest().authenticated()) //모든 요청에 인증 필요
.formLogin(login -> login.disable()) // 기본 로그인 폼 비활성화
.httpBasic(httpBasic -> httpBasic.disable()); // HTTP Basic 인증 비활성화
return http.build();
}
아직 문제해결되지 않음.
createSchedule(@RequestParam Long userId, @RequestBody Schedule schedule)
@PostMapping
public ResponseEntity<Schedule> createSchedule(@RequestParam Long userId, @RequestBody Schedule schedule) {
return ResponseEntity.ok(scheduleService.createSchedule(userId, schedule));
}
// 수정 후
@PostMapping
public ResponseEntity<?> createSchedule(@RequestBody Schedule schedule, HttpSession session) {
//세션에서 로그인된 사용자 가져오기
User user = (User) session.getAttribute("user");
//사용자가 로그인되지 않은 경우 처리
if (user == null) {
return ResponseEntity.status(403).body("{\"message\": \"로그인이 필요합니다.\"}");
}
//로그인한 사용자의 ID를 기반으로 일정 생성
schedule.setUser(user);
Schedule savedSchedule = scheduleService.createSchedule(user.getId(), schedule);
return ResponseEntity.ok(savedSchedule);
}
@RequestParam userId
제거 → 세션에서 로그인한 사용자 정보를 가져오는 방식으로 변경schedule.setUser(user);
→ 일정을 로그인된 사용자에 연결scheduleService.createSchedule(user.getId(), schedule);
호출SESSIONID가 아닌 JSESSIONID가 반환됨.
Spring Security에서 기본적으로 세션을 JSESSIONID로 관리함.
하지만 인증이 처리되지 않아 403이 발생.
즉, 세션이 제대로 유지되지 않고 있음.
로그에서 확인해보니까, Using generated security password:라는 메시지가 보임.
Spring Security에서 기본적으로 inMemoryUserDetailsManager를 사용해서 인증을 수행하고 있음.
우리가 만든 세션 기반 인증(HttpSession)과 충돌할 가능성이 높음.
즉, 현재 로그인한 사용자가 SecurityContext에서 인식되지 않는 상태일 가능성이 큼.
현재 SecurityConfig는 Spring Security의 UserDetailsService를 사용하지 않고 있음.
SecurityConfig 코드 수정
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable()) //CSRF 보호 비활성화 (POST 요청 차단 방지)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)) //세션 기반 인증 유지
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/users/signup", "/api/users/login").permitAll() //회원가입 & 로그인 허용
.requestMatchers("/api/schedules/**").authenticated() //일정 관련 요청 인증 필요
.anyRequest().authenticated()) //모든 요청에 인증 필요
.formLogin(login -> login.disable()) // 기본 로그인 폼 비활성화
.httpBasic(httpBasic -> httpBasic.disable()); // HTTP Basic 인증 비활성화
return http.build();
}
에서
package com.example.scheduledemo.security;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
public class SecurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/users/signup", "/api/users/login").permitAll()
.anyRequest().authenticated())
.formLogin(login -> login.disable())
.httpBasic(httpBasic -> httpBasic.disable()) /
.logout(logout -> logout
.logoutUrl("/api/users/logout")
.deleteCookies("JSESSIONID")
.invalidateHttpSession(true));
return http.build();
}
}
sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
유지하여 세션이 필요할 때만 생성찾아보니 현재 createSchedule에서 세션을 활용하는 방식이 부적절함.
-> 기존 HttpSession에서 user를 가져오고 있지만, SecurityContext에서 인증되지 않은 사용자는 403 Forbidden이 발생할 수 있음.
@PostMapping
public ResponseEntity<?> createSchedule(@RequestBody Schedule schedule, HttpSession session) {
User user = (User) session.getAttribute("user");
if (user == null) {
return ResponseEntity.status(403).body("{\"message\": \"로그인이 필요합니다.\"}");
}
schedule.setUser(user);
Schedule savedSchedule = scheduleService.createSchedule(user.getId(), schedule);
return ResponseEntity.ok(savedSchedule);
}
수정된 코드
@PostMapping
public ResponseEntity<?> createSchedule(@RequestBody Schedule schedule, HttpServletRequest request) {
HttpSession session = request.getSession(false); //기존 세션 가져오기
if (session == null || session.getAttribute("user") == null) {
return ResponseEntity.status(403).body("{\"message\": \"로그인이 필요합니다.\"}");
}
User user = (User) session.getAttribute("user"); //로그인된 사용자 정보 가져오기
schedule.setUser(user);
Schedule savedSchedule = scheduleService.createSchedule(user.getId(), schedule);
return ResponseEntity.ok(savedSchedule);
}
E7B6719B9D052070B1C5D0F1AF7305A4
E7B6719B9D052070B1C5D0F1AF7305A4
맞는지 3번 확인해주고,
헤더의 키 벨류에 넣어주고,
역시나 403 Forbidden이 뜬다.
내가 놓친 것은 무엇일까 ?
쉬고, 전체적인 코드를 훑어보면서 다시 생각해봤다.
무엇들이 있길래 해결되지 않은 것일까 해당 코드 부분에서만이 아니라 다른 곳들에서도 내가 놓치고 있는 부분들이 있을까 ?
현재 UserService.login()에서 SecurityContextHolder
에 인증 정보 저장을 안 하고 있음.
session.setAttribute("user", user);
만으로는 Spring Security가 사용자를 인식하지 못함.
따라서, /api/schedules
엔드포인트 접근 시 403 Forbidden 발생하는중인 것 같음.
UserRepository.findByEmail(email)
에서 null을 반환할 가능성이 있음.
Optional<User> userOptional = userRepository.findByEmail(email);
로 변경하면 더 안전함.
sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)를 사용했지만, 기본 인증 필터를 설정하지 않음.
세션을 통한 인증을 명확하게 설정해야 함.
현재 ScheduleController.createSchedule()
에서 세션에서만 사용자 정보를 가져옴.
SecurityContextHolder.getContext().getAuthentication()
에서 인증 정보를 가져오는 게 더 적절함.
public boolean login(String email, String password, HttpServletRequest request, HttpServletResponse response) {
User user = userRepository.findByEmail(email);
if (user != null && passwordEncoder.matches(password, user.getPassword())) {
// 세션 생성 및 저장
HttpSession session = request.getSession();
session.setAttribute("user", user);
return true;
}
return false;
}
에서
public boolean login(String email, String password, HttpServletRequest request, HttpServletResponse response) {
Optional<User> userOptional = userRepository.findByEmail(email);
if (userOptional.isPresent()) {
User user = userOptional.get();
if (passwordEncoder.matches(password, user.getPassword())) {
HttpSession session = request.getSession(); // 세션 활용
session.setAttribute("user", user);
//SecurityContextHolder에도 저장
SecurityContext securityContext = SecurityContextHolder.getContext();
securityContext.setAuthentication(new UsernamePasswordAuthenticationToken(user, null, Collections.emptyList()));
return true;
}
}
return false;
}
SecurityContextHolder.getContext().setAuthentication(...)
추가Optional<User>
로 변경하여 안전성 강화이해하기 쉽게 다시 설명하면,
🔍 이전 코드의 문제점
이전 코드에서 UserService.login() 메서드는 사용자를 검증한 후, 세션(Session)에만 사용자 정보를 저장하고 있었습니다.
public boolean login(String email, String password, HttpServletRequest request, HttpServletResponse response) {
User user = userRepository.findByEmail(email);
if (user != null && passwordEncoder.matches(password, user.getPassword())) {
// 문제: Spring Security가 이 정보를 인식하지 못함
HttpSession session = request.getSession();
session.setAttribute("user", user); // 세션에만 사용자 저장
return true;
}
return false;
}
session.setAttribute("user", user); → Spring Security가 관리하는 인증(Authentication) 정보가 아님
SecurityContextHolder에 사용자 정보가 저장되지 않음
Spring Security의 인증 필터가 요청을 처리할 때 "로그인된 사용자가 없음"으로 판단
→ 그래서 인증이 필요한 API 요청(/api/schedules 등)이 403 Forbidden을 반환함.
해결 방법: SecurityContextHolder에 사용자 정보 저장
수정 코드 (이제 Spring Security가 사용자를 인식함)
public boolean login(String email, String password, HttpServletRequest request, HttpServletResponse response) {
Optional<User> userOptional = userRepository.findByEmail(email);
if (userOptional.isPresent()) {
User user = userOptional.get();
if (passwordEncoder.matches(password, user.getPassword())) {
// 기존 방식: 세션에 사용자 저장
HttpSession session = request.getSession();
session.setAttribute("user", user);
// 추가한 부분: SecurityContextHolder에도 사용자 저장
SecurityContext securityContext = SecurityContextHolder.getContext();
securityContext.setAuthentication(
new UsernamePasswordAuthenticationToken(user, null, Collections.emptyList())
);
return true;
}
}
return false;
}
추가된 핵심 코드
SecurityContext securityContext = SecurityContextHolder.getContext();
securityContext.setAuthentication(
new UsernamePasswordAuthenticationToken(user, null, Collections.emptyList())
);
왜 이게 해결책이 되는가?
다시 본론으로 돌아가,
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
}
@PostMapping
public ResponseEntity<?> createSchedule(@RequestBody Schedule schedule, HttpServletRequest request) {
HttpSession session = request.getSession(false); //기존 세션 가져오기
if (session == null || session.getAttribute("user") == null) {
return ResponseEntity.status(403).body("{\"message\": \"로그인이 필요합니다.\"}");
}
User user = (User) session.getAttribute("user"); //로그인된 사용자 정보 가져오기
schedule.setUser(user);
Schedule savedSchedule = scheduleService.createSchedule(user.getId(), schedule);
return ResponseEntity.ok(savedSchedule);
}
에서
@PostMapping
public ResponseEntity<?> createSchedule(@RequestBody Schedule schedule) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || authentication.getPrincipal() == null || !(authentication.getPrincipal() instanceof User)) {
return ResponseEntity.status(403).body("{\"message\": \"로그인이 필요합니다.\"}");
}
User user = (User) authentication.getPrincipal();
schedule.setUser(user);
Schedule savedSchedule = scheduleService.createSchedule(user.getId(), schedule);
return ResponseEntity.ok(savedSchedule);
}
로 수정
이전 방식은 세션만 사용해서 너무 잘못된 방식이라 생각 했으며,
session.getAttribute("user") → Spring Security가 관리하는 방식이 아님
로그인은 되었어도 SecurityContext에 정보가 없으면 403 Forbidden 발생
따라서, SecurityContextHolder 활용해봤습니다.
의존성 추가
git fetch origin
git checkout main
git pull origin main
git checkout feat
git rebase main
git push origin feat --force
네 이러면 다 날라가니까 여러분들은 꼭 주의하세요 ! ㅎㅎ..
깃에 관련해서 참고 할만한 링크
또 똑같이 에러.
그냥 rebase 등 깃헙 관련해서 확실하게 공부하는 것이 좋겠음을 느꼇다.
겨우 살렸는데,
에러 메시지
There isn’t anything to compare. main and feat are entirely different commit histories.
main과 feat 브랜치가 완전히 독립적인 커밋 히스토리를 가지고 있음
즉, feat 브랜치는 main에서 파생된 게 아니라 완전히 별개로 존재하는 상태
Git은 두 브랜치를 비교할 수 없어서 PR을 만들 수 없음
해결 방법
현재 해결 방법은 feat 브랜치를 main에서 파생된 것으로 정리하는 것이며,
이제 feat을 main 위에 올린 후 다시 GitHub에 푸시하면 PR이 가능해질 것 같은데..?
git checkout feat
git rebase main
이 과정에서 충돌(conflict)이 발생할 수도 있음.
만약 충돌이 발생하면
1. git status로 충돌 난 파일 확인
2. 해당 파일을 수정 후 git add 충돌해결된파일
3.git rebase --continue
4. 충돌이 발생하지 않을 때까지 반복
git push origin feat --force
주의: --force를 사용하면 기존 원격 feat 브랜치의 히스토리가 덮어씌워지므로, 꼭 확인하고 실행해야 함!
이제 GitHub에서 feat → main으로 PR을 만들면 정상적으로 비교가 가능할
작업을 새벽에 마무리 했으나, pr이 안 돼서,
git checkout -b main origin/main
을 하기 전에 이미 main 브랜치가 있어서 오류가 발생했습니다.🚨 git reset --hard main이 위험했던 이유
git reset --hard main
이 명령어는 feat 브랜치를 main의 상태로 강제 덮어씌우는 명령.
하지만, main 브랜치에는 아무 코드도 없었음.
결과적으로 feat 브랜치의 코드가 완전히 날아가 버리게 된 것입니다! 😱
git merge --allow-unrelated-histories
이 옵션은 서로 다른 커밋 히스토리를 가진 두 브랜치를 병합할 때 사용하는 것임에도 불구하고,
이미 fetch & rebase를 하려던 상황에서는 필요 없었음.
이걸 실행해도 변경된 게 없어서 "이미 업데이트 상태입니다."가 나왔던 것입니다.
결국 돌고 돌아 해당 명령어를 통해 '원격 브랜치를 강제로 덮어씌우는' 행위를 했습니다.
즉, reset --hard main
으로 날려버린 상태를 그대로 github에 업로드한 것이다.
그래서, github에도 코드가 전부 사라졋던 것이다.
feat이 main과 연결되지 않은 상태가 되었기 때문
PR을 만들려면 feat 브랜치가 main을 기반으로 만들어져야 함.
하지만 git reset --hard main으로 feat과 main의 연결을 끊어버려서 비교할 수 없게 된 것.
그렇다면, 앞으로 어떻게 해야 할 것인가? (개선점 및 해결책)
이제 같은 실수를 반복하지 않도록 안전한 방법을 정리해보겠습니다.
🔥 git reset --hard
는 웬만하면 사용하지 말기!
🌟 항상 fetch 후 rebase를 사용해서 브랜치 정리하기!
//GitHub 원격 브랜치 가져오기
git fetch origin
// 최신 main 브랜치를 가져오기
git checkout main
git pull origin main
//최신 main을 기반으로 feat 브랜치 정리하기
git checkout feat
git rebase main # 여기서 충돌 나면 해결 후 git rebase --continue
//안전하게 원격에 업로드
git push origin feat --force
git reset --hard는 정말 필요할 때만 사용하는 것이 좋다.
→ reset --hard main을 해버려서 feat 브랜치 작업이 다 날아갔던 것.
항상 git fetch origin & git rebase main을 먼저 하기!
→ feat이 main과 연결된 상태로 유지돼야 PR을 만들 수 있음.
GitHub에 push할 때는 --force를 주의해서 사용!
→ 실수로 날린 코드가 있다면 git reflog로 복구할 수도 있음.