[Spring] Spring Security 프레임워크

최혜원·2023년 10월 18일
1

Spring

목록 보기
13/19
post-thumbnail

📍Spring Security 적용

'Spring Security' 프레임워크는 Spring 서버에 필요한 인증 및 인가를 위해 많은 기능을 제공해 줌으로써 개발의 수고를 덜어 줍니다. 마치 'Spring' 프레임워크가 웹 서버 구현에 편의를 제공해 주는 것과 같습니다.

  • 'Spring Security' 프레임워크 추가
// Security
implementation 'org.springframework.boot:spring-boot-starter-security'
  • 'Spring Security' 설정
    WebSecurityConfig
package com.sparta.springauth.config;

import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity // Spring Security 지원을 가능하게 함
public class WebSecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        // CSRF 설정
        http.csrf((csrf) -> csrf.disable());

        http.authorizeHttpRequests((authorizeHttpRequests) ->
                authorizeHttpRequests
                        .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll() // resources 접근 허용 설정
                        .anyRequest().authenticated() // 그 외 모든 요청 인증처리
        );

        // 로그인 사용
        http.formLogin(Customizer.withDefaults());

        return http.build();
    }
}

 

📍CSRF

🔥 CSRF란?

  • CSRF(사이트 간 요청 위조, Cross-site request forgery)
    • 공격자가 인증된 브라우저에 저장된 쿠키의 세션 정보를 활용하여 웹 서버에 사용자가 의도하지 않은 요청을 전달하는 것입니다.
    • CSRF 설정이 되어있는 경우 html 에서 CSRF 토큰 값을 넘겨주어야 요청을 수신 가능합니다.
    • 쿠키 기반의 취약점을 이용한 공격 이기 때문에 REST 방식의 API 에서는 disable 가능합니다.
    • POST 요청마다 처리해 주는 대신 CSRF protection 을 disable 하겠습니다.
      • http.csrf((csrf) -> csrf.disable());

 

📍 Spring Security의 default 로그인 기능

  • Username: user
  • Password: Spring 로그 확인 (서버 시작 시마다 변경됨)

 

📍Spring Security 이해하기

Spring Security - Filter Chain

  • Spring에서 모든 호출은 DispatcherServlet을 통과하게 되고 이후에 각 요청을 담당하는 Controller 로 분배됩니다.
  • 이 때, 각 요청에 대해서 공통적으로 처리해야할 필요가 있을 때 DispatcherServlet 이전에 단계가 필요하며 이것이 Filter 입니다.
  • Spring Security도 인증 및 인가를 처리하기 위해 Filter를 사용하는데
    • Spring Security는 FilterChainProxy를 통해서 상세로직을 구현하고 있습니다.

Form Login 기반은 인증


Form Login 기반 인증은 인증이 필요한 URL 요청이 들어왔을 때 인증이 되지 않았다면 로그인 페이지를 반환하는 형태입니다.

UsernamePasswordAuthenticationFilter

  • UsernamePasswordAuthenticationFilter는 Spring Security의 필터인 AbstractAuthenticationProcessingFilter를 상속한 Filter입니다.
  • 기본적으로 Form Login 기반을 사용할 때 username 과 password 확인하여 인증합니다.
  • 인증 과정
    1. 사용자가 username과 password를 제출하면 UsernamePasswordAuthenticationFilter는 인증된 사용자의 정보가 담기는 인증 객체인 Authentication의 종류 중 하나인 UsernamePasswordAuthenticationToken을 만들어 AuthenticationManager에게 넘겨 인증을 시도합니다.
    2. 실패하면 SecurityContextHolder를 비웁니다.
    3. 성공하면 SecurityContextHolderAuthentication를 세팅합니다.

SecurityContextHolder

  • SecurityContext는 인증이 완료된 사용자의 상세 정보(Authentication)를 저장합니다.
  • SecurityContext는 SecurityContextHolder 로 접근할 수 있습니다.
// 예시코드
SecurityContext context = SecurityContextHolder.createEmptyContext();
Authentication authentication = new UsernamePasswordAuthenticationToken(principal, credentials, authorities);
context.setAuthentication(authentication); // SecurityContext 에 인증 객체 Authentication 를 저장합니다.

SecurityContextHolder.setContext(context);

Authentication

  • 현재 인증된 사용자를 나타내며 SecurityContext에서 가져올 수 있습니다.
  • principal : 사용자를 식별합니다.
    • Username/Password 방식으로 인증할 때 일반적으로 UserDetails 인스턴스입니다.
  • credentials : 주로 비밀번호, 대부분 사용자 인증에 사용한 후 비웁니다.
  • authorities : 사용자에게 부여한 권한을 GrantedAuthority로 추상화하여 사용합니다.
<UserDetails>
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
    UserRoleEnum role = user.getRole();
    String authority = role.getAuthority();

    SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(authority);
    Collection<GrantedAuthority> authorities = new ArrayList<>();
    authorities.add(simpleGrantedAuthority);

    return authorities;
}

Authentication authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());

🔥 UsernamePasswordAuthenticationToken는 Authentication을 implements한 AbstractAuthenticationToken의 하위 클래스로, 인증객체를 만드는데 사용됩니다.

UserDetailsService

🔥 UserDetailsService는 username/password 인증방식을 사용할 때 사용자를 조회하고 검증한 후 UserDetails를 반환합니다. Custom하여 Bean으로 등록 후 사용 가능합니다.

UserDetails

🔥 검증된 UserDetails는 UsernamePasswordAuthenticationToken 타입의 Authentication를 만들 때 사용되며 해당 인증객체는 SecurityContextHolder에 세팅됩니다. Custom하여 사용가능합니다.

 

📍Spring Security : 로그인

• 스프링 시큐리티 사용 전

• 스프링 시큐리티 사용 후

  • Client 의 요청은 모두 Spring Security 를 거치게됩니다.
  • Spring Security 역할
    • 인증/인가
      1. 성공 시: Controller 로 Client 요청 전달
        1. Client 요청 + 사용자 정보 (UserDetails)
      2. 실패 시: Controller 로 Client 요청 전달되지 않음
        1. Client 에게 Error Response 보냄

• 로그인 처리 과정

  1. Client
    a. 로그인 시도
    b. 로그인 시도할 username, password 정보를 HTTP body 로 전달 (POST 요청)
    c. 로그인 시도 URL 은 WebSecurityConfig 클래스에서 변경 가능
    - 아래와 같이 설정 시 "POST /api/user/login" 로 설정됩니다.

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
     // CSRF 설정
     http.csrf((csrf) -> csrf.disable());
    
     http.authorizeHttpRequests((authorizeHttpRequests) ->
             authorizeHttpRequests
                     .requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll() // resources 접근 허용 설정
                     .anyRequest().authenticated() // 그 외 모든 요청 인증처리
     );
    
     // 로그인 사용
    	 http.formLogin((formLogin) ->
             formLogin
                  // 로그인 처리 (POST /api/user/login)
                 .loginProcessingUrl("/api/user/login").permitAll()
     );
    
     return http.build();
    }
    
  2. 인증 관리자 (Authentication Manager)
    a. UserDetailsService 에게 username 을 전달하고 회원상세 정보를 요청

  3. UserDetailsService
    a. 회원 DB 에서 회원 조회

    User user = userRepository.findByUsername(username)
        .orElseThrow(() -> new UsernameNotFoundException("Not Found " + username));
    • 회원 정보가 존재하지 않을 시 → Error 발생

    b. 조회된 회원 정보(user) 를 UserDetails 로 변환

    UserDetails userDetails = new UserDetailsImpl(user)

    c. UserDetails 를 "인증 관리자"에게 전달

    • UserDetailsService 인터페이스 → UserDetailsServiceImpl구현
    • UserDetails 인터페이스 → UserDetailsImpl구현
    • ⭐️ UserDetailsService와 UserDetails를 직접 구현해서 사용하게 되면 Security의 default 로그인 기능을 사용하지 않겠다는 설정이 되어 Security의 password를 더 이상 제공하지 않는 것을 확인할 수 있습니다.
    • POST "/api/user/login" 을 로그인 인증 URL로 설정했기 때문에 이제 해당 요청이 들어오면 우리가 직접 구현한 UserDetailsService를 통해 인증 확인 작업이 이뤄지고 인증 객체에 직접 구현한 UserDetails가 담기게 됩니다.
  4. "인증 관리자" 가 인증 처리

    • 아래 2 개의 username, password 일치 여부 확인
      • Client 가 로그인 시도한 username, password
      • UserDetailsService 가 전달해준 UserDetails 의 username, password
    • password 비교 시
      • Client 가 보낸 password 는 평문이고, UserDetails 의 password 는 암호문
      • Client 가 보낸 password 를 암호화해서 비교
    • 인증 성공 시 → 세션에 로그인 정보 저장
    • 인증 실패 시 → Error 발생

 

⭐️ 디폴트 로그인 페이지가 아니라 내가 만든 로그인 페이지를 사용할 수 있도록 설정


users 테이블과 연결해서 사용하기 위해 스프링에서 제공하는 디폴트 로그인 기능 사용하지 않고
UserDetailsServiceImpl
UserDetailsImpl
두 개의 구현체를 만들었다.
🔥 @AuthenticationPrincipal UserDetailsImpl
→ UserDetailsServiceImpl 에서 loadUserByUsername 으로 조회한 후
조회해 온 user 를 사용해서 UserDetailsImpl 을 만든다.
그리고 getUser() 사용해서 Controller 에서 user 정보를 가져온다.

@AuthenticationPrincipal 에 의해서 Authentication 인증 객체 Principal 부분에
들어있던 UserDetailsImpl 을 잘 가지고 오고 있다.

profile
어제보다 나은 오늘

0개의 댓글