스프링부트 독학-8장 스프링 시큐리티로 로그인/로그아웃, 회원가입 구현하기

jaegeunsong97·2023년 9월 1일
0

출처

신서영개발자님의 스프링부트 책

새롭게 알게된 내용 정리

스프링 시큐리티

  • 스프링 시큐리티 : 스프링 기반의 애플리케이션 보안(인증, 인가, 권한)을 담당하는 스프링 하위 프레임워크
    • 인증과 인가
      • 인증(Authentication) : 사용자의 신원 확인
      • 인가(Authorization) : 사이트의 특정 부분에 접근할 수 있는지 권한을 확인
    • 스프링 시큐리티 : 스프링 기반 애플리케이션의 보안을 담당하는 스프링 하위 프레임워크
      • 보안 관련 옵션을 제공
      • CSRF 공격, 세션고정 공격 방어
        • CSRF : 사용자의 권한을 가지고 특정 동작을 수행하도록 유도하는 공격
        • 세션고정 공격 : `사용자의 인증 정보를 탈취하거나 변조하는 공격
      • 요청 헤더도 보안처리`
    • 필터기반으로 동작하는 스프링 시큐리티
      • SecurityContextPersistanceFilter ~~ FilterSecurityInterceptor로 동작
      • 중요한 필터
        • UsernamePasswordAuthenticationFilter : 아이디와 패스워드가 넘어오면 인증 요청을 위임하는 인증 관리자 역할
        • FilterSecurityInterceptor : 권한 부여 처리위임접근 제어 결정을 쉽게 하는 접근 결정 관리자 역할

필터명설명
SecurityContextPersistanceFilterSecurityContextRepository에서 SecurityContext(접근 주체와 인증에 대한 정보를 담고 있는 객체)를 가져오거나 저장하는 역할을 합니다.
LogoutFilter설정된 로그아웃 URL로 오는 요청을 확인해 해당 사용자를 로그아웃 처리합니다.
UsernamePasswordAuthenticationFilter인증관리자입니다. 폼 기반 로그인을 할 때 사용되는 필터로 ID, PW 데이터를 파싱해 인증 요청을 위임합니다. 인증이 성공하면 AuthencationSuccessHandler를, 인증이 실패하면 AuthenticationFailureHandler를 실행합니다.
DefaultLoginPageGeneratingFilter사용자가 로그인 페이지를 따로 지정하지 않았을 때 기본으로 설정하는 로그인 페이지 관련 필터입니다.
BasicAuthenticationFilter요청 헤더에 있는 ID와 PW를 파싱해서 인증 요청을 위임합니다. 인증이 성공하면 AuthencationSuccessHandler를, 인증이 실패하면 AuthenticationFailureHandler를 실행합니다.
RequestCacheAwareFilter로그인 성공 후, 관련 있는 캐시 요청이 있는지 확인하고 캐시 요청을 처리해줍니다. 예를 들어 로그인하지 않은 상태로 방문했던 페이지를 기억하고 로그인 이후에 해당 페이지로 이동시킵니다.
SecurityContextHolderAwareRequestFilterHttpServletRequest 정보를 감쌉니다. 필터 체인 상의 다음 필터들에게 부가 정보를 제공되기 위해 사용합니다.
AnonymousAuthenticationFilter필터가 호출되는 시점까지 인증되지 않았다면 익명 사용자 전용 갹체인 AnonymousAuthentication을 만들어 SecurityContext에 넣어줍니다.
SessionManagementFilter인증된 사용자와 관련된 세션 관련 작업을 진행합니다. 세션 변조 방지 전략을 설정하고, 유효하지 않은 세션에 대한 처리를 하고, 세션 생성 전략을 세우는 등의 작업을 처리합니다.
ExceptionTranslationFilter요청을 처리하는 중에 발생할 수 있는 예외를 위임하거나 전달합니다.
FilterSecurityInterceptor접근 결정 관리자입니다. AccessDecisionManager로 권한 부여 처리를 위임함으로써 접근 제거 결정을 쉽게 해줍니다. 이 과정에서는 이미 사용자가 인증되어 있으므로 유효한 사용자인지도 알 수 있습니다. 즉, 인가 관련 설정을 할 수 있습니다.

(진짜 기네..........)

  • 가장 많이 사용하는 ID, PW 기반 formLogin 시도

  • 순서
    • 사용자가 폼에 ID, PW 입력 -> HttpServletRequest에 ID, PW 정보 전달
      • 이때 AuthenticationFilter가 넘어온 ID, PW 유효성 검사 진행
    • 유효성 검사 끝나면, 실제 구현체UsernamePasswordAuthenticationToken을 만들어 넘김
    • UsernamePasswordAuthenticationToken인증용 객체AuthenticationManager에게 보냄
    • AuthenticationManagerUsernamePasswordAuthenticationTokenAuthenticationProvider로 보냄
    • 사용자 IDUserDetailsService로 보냄
      • UserDetailsService사용자 ID로 찾은 사용자의 정보를 UserDetails 객체로 만들어 AthenticationProvider에게 전달
        • DB 조회
      • 입력정보와 UserDetails의 정보를 비교해 실제 인증처리
      • 완성되면 SecurityContextHolder의 Authentication에 저장
        • 성공 : AuthenticationSuccessHandler
        • 실패 : AuthenticationFailureHandler

도메인과 레포, 서비스 세팅

@Table(name = "users")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@Entity
public class User implements UserDetails {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "id", updatable = false)
    private Long id;

    @Column(name = "email", nullable = false, unique = true)
    private String email;

    @Column(name = "password", nullable = false)
    private String password;

    @Builder
    public User(String email, String password, String auth) {
        this.email = email;
        this.password = password;
    }


    @Override // 권한 반환
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return List.of(new SimpleGrantedAuthority("user"));
    }

    @Override
    public String getUsername() {
        return email;
    }

    @Override
    public String getPassword() {
        return password;
    }

    @Override // 계정 만료 여부
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override // 계정 잠금 여부
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override // PW 만료 여부
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override // 계정 사용 가능 여부
    public boolean isEnabled() {
        return true;
    }
}
메소드반환타입설명
getAuthorities()Collection<? extends GrantedAuthority>사용자가 가지고 있는 권한의 목록을 반환, 현재 예제 코드에서는 사용자 이외의 권한이 없기 때문에 user권한만 담아 반환
getUsername()String사용자를 식별할 수 있는 사용자 이름을 반환, 이때 사용되는 사용자 이름은 반드시 UNIQUE, 현재 예제 코드는 유니크 속성이 적용된 EMAIL
getPassword()String사용자 비밀번호 반환, 이때 저장되어 있는 비밀번호는 암호화 해서 저장
isAccountNonExpired()boolean계정이 만료되었는지 확인, 만약 만료 X -> true
isAccountNonLocked()boolean계정이 잠금되었는지 확인, 만약 잠금 만료 X -> true
isCredentialsNonExpired()boolean비밀번호가 만료되었는지 확인, 만약 만료 X -> true
isEnabled()boolean계정 사용가능 확인, 사용 가능 -> true
  • 레포지토리
    • 네임드 쿼리 : 스프링 데이터 JPA는 메소드 규칙에 맞춰 메소드를 선언하면 이름을 분석해 자동으로 쿼리를 생성
public interface UserRepository extends JpaRepository<User, Long> {

    Optional<User> findByEmail(String email); // 네임드 쿼리
}
코드설명쿼리
findByName()"name"컬럼의 값 중 파라미터로 들어오는 값과 같은 데이터 반환...WHERE name = ?1
findByNameAndAge()파라미터로 들어오는 값 중 첫 번쨰 값은 "name" 컬럼에서 조회하고, 두 번쨰 값은 "age"컬럼에서 조회한 데이터 반환...WHERE name = ?1 AND age = ?2
findByNameOrAge()파라미터로 들어오는 값 중 첫 번째 값이 "name" 컬럼에서 조회되거나 두 번쨰 값이 "age"컬럼에서 조회한 데이터 반환...WHERE name = ?1 OR age = ?@
findByAgeLessThan()"age" 컬럼의 값 중 파라미터로 들어온 값도다 작은 데이터 반환...WHERE age < ?1
findByAgeGreaterThan()"age" 컬럼의 값 중 파라미터로 들어온 값보다 큰 데이터 반환...WHERE age > ?1
findByName(Is)Null()"name" 컬럼의 값 중 null인 데이터 반환...WHERE name IS NULL
  • UserDetailService
    • loadUserByUsername() : 사용자 정보를 가져오는 로직
@RequiredArgsConstructor
@Service
public class UserDetailService implements UserDetailsService {

    private final UserRepository userRepository;

    @Override
    public User loadUserByUsername(String email) {
        return userRepository.findByEmail(email)
                .orElseThrow(() -> new IllegalArgumentException((email)));
    }
}

시큐리티 설정

@RequiredArgsConstructor
@Configuration
public class WebSecurityConfig {

    private final UserDetailService userService;

    @Bean // 스프링 시큐리티 기능 비활성화 -> 정적 리소스, H2만 설정
    public WebSecurityCustomizer configure() {
        return (web) -> web.ignoring()
                .requestMatchers(toH2Console())
                .requestMatchers("/static/**");
    }

    @Bean // 특정 HTTP 요청에 대한 웹 기반 보안 구성
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return http
                .authorizeRequests() // 인증, 인가 설정
                	.requestMatchers("/login", "/signup", "/user").permitAll()
                	.anyRequest().authenticated()
                .and()
                .formLogin() // 폼 기반 로그인 설정
                	.loginPage("/login")
                	.defaultSuccessUrl("/articles")
                .and()
                .logout()
                	.logoutSuccessUrl("/login")
                	.invalidateHttpSession(true) // 로그아웃 후 세션 삭제해?
                .and()
                .csrf().disable() // 비활성화
                .build();
    }

    @Bean // 인증 관리자 관련 설정
    public AuthenticationManager authenticationManager(HttpSecurity http, BCryptPasswordEncoder bCryptPasswordEncoder, UserDetailService userDetailService) throws Exception {
        return http.getSharedObject(AuthenticationManagerBuilder.class)
                .userDetailsService(userService) // 사용자 정보 서비스 설정 -> 반드시 설정하는 서비스 클래스는 UserDetailsService 상속받음
                .passwordEncoder(bCryptPasswordEncoder)
                .and()
                .build();
    }

    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

로그아웃

@GetMapping("/logout")
    public String logout(HttpServletRequest request, HttpServletResponse response) {
        new SecurityContextLogoutHandler().logout(request, response, SecurityContextHolder.getContext().getAuthentication());
        return "redirect:/login";
    }
  • 로그아웃 담당 핸들러 : SecurityContextLogoutHandler

    • logout() 호출
  • 핵심요약

    • 인증 : 보호된 리소스에 접근하는 것을 허용하기 이전에 등록된 사용자의 신원을 입증하는 과정(로그인과 회원가입 같은 거)
    • 인가 : 특정 부분에 접근할 수 있는지에 확인하는 작업
    • 스프링 시큐리티 : 스프링 기반의 애플리케이션 보안(인증, 인가, 권한)을 담당하는 스프링 하위 프레임워크
      • 필터기반으로 작동
      • 각 필터에서 인증, 인가 관련된 작업 진행
      • 기본적으로 세션, 쿠키 방식으로 인증 처리
      • 스프링 시큐리티에서 사용자의 인증, 인가 정보 -> UserDetails 객체에 담음
        • 이 클래스를 상속받은 뒤 메소드를 Override
      • 스프링 시큐리티에서 사용자의 정보를 가져오는 데 UserDetailService 사용
        • 이 클래스를 상속받은 뒤 loadByUsername()을 Override하면 스프링 시큐리티에서 사용자의 정보를 가져올 때 오버라이드된 메소드를 사용
profile
블로그 이전 : https://medium.com/@jaegeunsong97

0개의 댓글