[Trouble Shooting] Spring Security 403 Forbidden 해결
Spring Boot 3 버전 Spring Security를 사용하다 겪었던 문제와, 해결방안을 정리해 둔 글입니다.
본 포스팅은 Spring Boot 3.1.1
기반으로 작성된 글이며,
인증 방식으로는 Spring Security Session-Cookie
방식을 사용했습니다.
문제 상황
토이프로젝트로 도서판매시스템을 개발하던 중.
myPage로 USER 권한이 있어야 들어갈 수 있게 했고,
회원가입을 하는 순간 USER 권한을 부여해줌.
그 이후 당연히 될 것 이라고 생각하고 myPage로 들어가는 순간!
403 forbidden
등장..
403 Forbidden?
HTTP
403 Forbidden
클라이언트 오류 상태 응답 코드는 서버에 요청이 전달되었지만, 권한 때문에 거절되었다는 것을 의미합니다.이 상태는
[401](https://developer.mozilla.org/ko/docs/Web/HTTP/Status/401)
과 비슷하지만, 로그인 로직(틀린 비밀번호로 로그인 행위)처럼 반응하여 재인증(re-authenticating)을 하더라도 지속적으로 접속을 거절합니다.
출처 : https://developer.mozilla.org/ko/docs/Web/HTTP/Status/403
조치 과정
DB 데이터 확인 이후 데이터 입력은 문제가 없는 것을 확인 → 문제 X과정에서 누락된 로직이 있는 지 확인Slf4j를 통해 과정에서 권한이 누락되는 부분 확인
user is User(seq=1, username=1, password=$2a$10$5Ib1OC2lG4Ar53tigrZ1ve3Ucao8pg.l6YB4m.HKzd93pfzX8ggim, name=1, nickname=1, grade=BRONZE, mileage=0, createdDate=2023-07-29 19:05:48.999699, modifiedDate=2023-07-29 19:05:48.999699, enabled=true, authorities=[])
→ authorities가 빈 채로 넘어오는걸 발견
- 인증된 사용자의 권한 정보를 가져올 수 있게 함(UserServiceImpl → processRegistration 메서드 수정)
- 메서드 수정 이후 무한 재귀 →
StackOverFlow
예외 발생으로 인한 엔티티 수정 - authorities가 정상적으로 넘어오는 것을 확인한 후 UserDetailsImpl 메서드 수정
- 해결
변경된 소스코드 및 설정
들어가기 앞서, 시큐리티 설정(SecurityConfig)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
@RequiredArgsConstructor @Configuration public class SecurityConfig { private final CorsFilter corsFilter; @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception { return httpSecurity .formLogin(login -> login .loginPage("/login") //User가 로그인 요청할 URL 지정 .defaultSuccessUrl("/", false)) //사용자가 인증되기 전에 방문하려고 한 페이지로 리다이렉트 됨 .logout(logout -> logout .logoutUrl("/logout") //User가 로그아웃 요청할 URL 지정 .logoutSuccessUrl("/") //로그아웃 성공 후 사용자가 리다이렉트 될 URL 지정 .invalidateHttpSession(true) // 로그아웃 성공 시 사용자의 HttpSession 무효화 설정 ) .addFilterBefore(corsFilter, UsernamePasswordAuthenticationFilter.class) // CORS 필터를 UsernamePassword..필터 전에 추가해서 CORS 관련 처리가 등록된 필터보다 먼저 실행되게 함 .csrf(AbstractHttpConfigurer::disable) //개발할 때 CSRF에 대한 추가 설정 없이 테스트하기 위해 CSRF 보호 비활성화, 실제 배포할 경우에는 활성화 해두는게 좋음 .authorizeHttpRequests(registry -> { // registry.requestMatchers("/myPage").hasRole("USER") //myPage URL 요청 -> 사용자 역할이 USER일 때만 허용됨 .requestMatchers("/", "/**").permitAll(); // root 및 모든 경로에 대한 모든 요청은 모든 사용자에게 허용 }).getOrBuild(); } }
인증된 사용자의 권한 정보를 가져올 수 있게 함 (UserServiceImpl)
before
1 2 3 4 5 6 7 8
@Override @Transactional public void processRegistration(RegistrationForm form) Authority auth = new Authority(); auth.setRole(Role.ROLE_USER); auth.setUser(userRepository.save(form.toUser(passwordEncoder))); authorityRepository.save(auth); }
after
1 2 3 4 5 6 7 8 9 10 11
@Override @Transactional public void processRegistration(RegistrationForm form) { Authority auth = new Authority(); auth.setRole(Role.ROLE_USER); User newUser = form.toUser(passwordEncoder); auth.setUser(newUser); User savedUser = userRepository.save(newUser); savedUser.getAuthorities().add(auth); authorityRepository.save(auth); }
수정 후 StackOverflow 예외에 따른 변경
Authority(Entity)
Before
1 2 3 4 5 6 7 8 9
@Entity @Data public class Authority { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private long seq; ... 나머지 생략 ... }
After
1 2 3 4 5 6 7 8 9 10 11 12
@Entity @Getter @Setter @EqualsAndHashCode(onlyExplicitlyIncluded = true) <-- 추가 public class Authority { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @EqualsAndHashCode.Include <-- 추가 private long seq; ... 나머지 생략 ... }
User(Entity)
Before
1 2 3 4 5 6 7 8 9 10 11
@Entity @Data @EntityListeners(AuditingEntityListener.class) public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private long seq; ... 나머지 필드 생략 ... }
After
1 2 3 4 5 6 7 8 9 10 11 12 13 14
@Entity @Getter @Setter @EqualsAndHashCode(onlyExplicitlyIncluded = true) <-- 추가 @EntityListeners(AuditingEntityListener.class) public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @EqualsAndHashCode.Include <-- 추가 private long seq; ... 나머지 필드 생략 ... }
authorities 확인 이후 UserDetailsImpl 수정
before
1
2
3
4
5
6
7
@Override
@Transactional
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities.stream()
.map(authority -> (GrantedAuthority) () -> authority.getRole().toString())
.collect(Collectors.toSet());
}
after
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Override
@Transactional
public Collection<? extends GrantedAuthority> getAuthorities() {
log.info("UserDetailsImpl -> getAuthorities : OK");
if (authorities.isEmpty()) {
log.info("authorities is empty");
}else {
log.info("authorities size: {}", authorities.size());
}
return authorities.stream()
.map(authority -> new SimpleGrantedAuthority(authority.getRole().name()))
.collect(Collectors.toSet());
}
마무리
스프링 시큐리티를 처음 도입한 토이프로젝트였지만 스프링 부트 3 버전을 사용해버리는 바람에 자료를 찾기 힘들어 간단해보이지만 주말내내 고생을 했습니다..
수정할 사항이나 추가해야할 사항이 있다면 댓글이나 이메일로 남겨주세요.