05.02 학습

한강섭·2025년 5월 2일
0

학습 & 숙제

목록 보기
79/103
post-thumbnail

Spring Security


🔄 Security 복습

Secured Resource를 어떻게 보호할까?

  • Delegate: 위임하다
  • FilterChainProxy: 일련의 필터를 관리해주는 진짜 필터!
  • DaoAuthenticationProvider:
    • has-a 관계로 2가지 객체를 가짐: PasswordEncoder, UserDetailsService
    • 이를 통해 UserDetails 객체 생성 후 인증 수행

Authentication 저장 방식:
1. Session
2. ThreadLocal


🛠️ Spring Security 적용

어떤 부분을 커스텀하여 우리의 프로젝트에 적용시킬까?

로그인 처리 흐름

  1. 로그인 페이지 접근 전 private 내부적인 exception 발생
  2. 인증되지 않은 사용자는 /login 페이지로 redirect
  3. 사용자가 로그인 폼에 정보 입력 후 POST /login으로 username, password 전송
  4. Filter에서 DaoAuthenticationProviderInMemory에서 사용자 확인

커스터마이징 포인트:

  • InMemoryJDBC 방식으로 변경
  • Usernameemail로 식별자 변경
  • 인증 실패 시 처리 로직 추가
  • 로그인 성공 후 리다이렉션 로직 수정

로그아웃 처리 흐름

  1. GET /logout 접근 시:
    • 로그아웃 확인 페이지 표시
    • CSRF 토큰을 함께 POST /logout에 제공
  2. POST /logout 접근 시:
    • 실제 로그아웃 처리 수행
    • 세션 무효화
    • 쿠키 삭제
    • 홈 페이지로 리다이렉션

중요: CSRF 보호를 활성화하면 모든 POST 요청에 토큰이 전달되어야 합니다.


🔐 사용자 정의 Authentication

인증 저장소 변경: InMemory → Database

AuthenticationManager:

  • 실제 인증 방식을 제공하는 객체
  • DAO, JWT 등 상황에 맞는 다양한 구현체 제공

사용자 정의 인증 구현 단계:

  1. UserDetailsService 구현:
@Service
public class CustomUserDetailsService implements UserDetailsService {
    
    @Autowired
    private UserRepository userRepository;
    
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByEmail(username)
            .orElseThrow(() -> new UsernameNotFoundException("사용자를 찾을 수 없습니다: " + username));
            
        return new org.springframework.security.core.userdetails.User(
            user.getEmail(),
            user.getPassword(),
            user.isEnabled(),
            true, true, true,
            getAuthorities(user.getRoles())
        );
    }
    
    private Collection<? extends GrantedAuthority> getAuthorities(List<String> roles) {
        return roles.stream()
            .map(role -> new SimpleGrantedAuthority("ROLE_" + role))
            .collect(Collectors.toList());
    }
}
  1. SecurityConfig 설정:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
    
    @Autowired
    private CustomUserDetailsService userDetailsService;
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .authorizeHttpRequests(auth -> auth
                .requestMatchers("/", "/home", "/register").permitAll()
                .requestMatchers("/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
            )
            .formLogin(form -> form
                .loginPage("/login")
                .defaultSuccessUrl("/dashboard")
                .failureUrl("/login?error=true")
                .permitAll()
            )
            .logout(logout -> logout
                .logoutSuccessUrl("/")
                .invalidateHttpSession(true)
                .deleteCookies("JSESSIONID")
                .permitAll()
            );
            
        return http.build();
    }
    
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    
    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService)
            .passwordEncoder(passwordEncoder());
    }
}

🔑 Remember-Me 처리

사용자가 브라우저를 닫았다가 다시 열어도 로그인 상태를 유지하는 기능

구현 방법:
1. 쿠키 기반 방식:

  • 기본적인 구현 방식
  • 쿠키에 사용자 정보와 만료 시간, 서명을 저장
  • 보안에 취약할 수 있음
  1. 영구 토큰 방식:
    • 데이터베이스에 토큰 저장
    • 보다 안전한 방식
    • 언제든지 토큰 무효화 가능

설정 방법:

http.rememberMe(t -> t
    .tokenValiditySeconds(60 * 60 * 24 * 30) // 30일 유효
    .rememberMeParameter("remember-me")
    .key("uniqueAndSecret") // 토큰 암호화에 사용되는 키
);

적용 예시:

http.rememberMe(t -> t.tokenValiditySeconds(60)); // 60초 동안 rememberMe 활성화

💼 Controller에서의 Security

메서드 레벨 보안

@Secured 어노테이션:

  • 특정 메서드에 접근 권한 부여
  • 역할 기반 접근 제어
@Service
public class UserService {
    
    @Secured("ROLE_ADMIN")
    public List<User> getAllUsers() {
        // 관리자만 접근 가능한 메서드
        return userRepository.findAll();
    }
}

@PreAuthorize/@PostAuthorize:

  • SpEL(Spring Expression Language)을 사용한 더 복잡한 보안 규칙 정의
  • 메서드 실행 전/후에 권한 검사
@RestController
@RequestMapping("/api/users")
public class UserController {
    
    @PreAuthorize("hasRole('ADMIN') or #id == authentication.principal.id")
    @GetMapping("/{id}")
    public User getUser(@PathVariable Long id) {
        // 관리자이거나 자신의 정보만 조회 가능
        return userService.findById(id);
    }
    
    @PostAuthorize("returnObject.owner == authentication.name")
    @GetMapping("/resources/{id}")
    public Resource getResource(@PathVariable Long id) {
        // 반환되는 리소스의 소유자가 현재 로그인한 사용자와 일치해야 함
        return resourceService.findById(id);
    }
}

현재 인증된 사용자 정보 접근

Controller에서 접근:

@GetMapping("/profile")
public String viewProfile(Authentication authentication, Model model) {
    UserDetails userDetails = (UserDetails) authentication.getPrincipal();
    model.addAttribute("user", userDetails);
    return "profile";
}

SecurityContextHolder 사용:

@Service
public class UserService {
    
    public User getCurrentUser() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        String email = authentication.getName();
        return userRepository.findByEmail(email)
            .orElseThrow(() -> new UsernameNotFoundException("사용자를 찾을 수 없습니다"));
    }
}

profile
기록하고 공유하는 개발자

0개의 댓글