Spring Security

Woo Yong·2023년 7월 30일
1

Spring

목록 보기
6/15
post-thumbnail

📌 Spring Security


Spring Security는 Spring MVC 기반 애플리케이션의 인증 (Authentication)rhk 인가 (Authorization) 기능을 지원하는 보안 프레임워크이다.

Spring Security 기능

  • 다양한 유형(폼 로그인 인증, 토큰 기반 인증, OAuth2 기반인증, LDA인증)의 사용자 인증 기능 적용이 가능하다.
    -> 즉, 커스터마이징이 가능한 인증 및 액세스 제어 프레임 워크이다.
  • 애플리케이션 사용자의 역할(Role)에 따른 권한 레벨 적용 ( Admin, Manage, User 등)
  • 애플리케이션에서 제공하는 리소스에 대한 접근 제어
  • 민감한 정보에 대한 데이터 암호화

Spring Security 용어 정리

  • Principal(주체)
    • 일반적으로 인증 프로세스가 성공적으로 수행된 사용자의 계정정보를 의미
  • Authentication(인증)
    • 애플리케이션을 사용하는 사용자를 식별하기 위한 증명하는 절차
    • ex) 로그인
  • Authorization(인가 또는 권한 부여)
    • 인증이 정상적으로 수행된 사용자에게 하나 이상의 권한을 부여하며 특정 애플리케아션의 특정 리소스에 접근할 수 있게 허가하는 것.
    • 인증 이후에 수행되어야 하며 권한은 일반적으로 Role형태로 부여
  • Access Control(접근제어) : 사용자가 애플리케이션의 리소스에 접근하는 행위를 제어하는 것.
    • ex ) 비회원은 접근하지 못하도록 차단하는 것

📌 Spring Security 적용하기


Step 1. 라이브러리 추가

build.gradle 파일에 라이브러리를 추가해준다.

implementation 'org.springframework.boot:spring-boot-starter-security' // Spring Security 적용하기 위해 추가

Step 2. CustomUserDetail 만들기

UserDetail은 인증 정보를 만드는데 필요한 사용자 정보를 제공해 준다.
사용자 정보를 입력할 때 어떤 변수가 Username인지, 어떤 변수가 password인지, 어떤 변수가 권한 정보를 나타내는지 알아야한다.


우선 UserDetails 인터페이스를 구현해야한다. 아래와 같이 모든 값을 null과 false로 return 하는 것을 볼 수 있다.
그러면 각 메소드들이 어떤 역할을 하는지 알아보자.

public class CustomDetails implements UserDetails {

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return null;
    }

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

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

    @Override
    public boolean isAccountNonExpired() {
        return false;
    }

    @Override
    public boolean isAccountNonLocked() {
        return false;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return false;
    }

    @Override
    public boolean isEnabled() {
        return false;
    }
}

필수 오버라이드 메소드 설명

  • getAuthorities : 계정의 권한 목록 return
  • getPassword() : 계정의 비밀번호 return
  • getUsername() : 계정의 이름 return (PK에 해당하는 정보)
  • isAccountNoNExcired(): 계정이 만료되지 않았는지 return (true : 만료안됨)
  • isAccountNonLocked(): 계정이 잠겨있지 않았는지 return (true : 잠기지 않음)
  • ìsCredentiakNonExpired(): 비밀번호가 만료되지 않았는지 return (true : 만료안됨)
  • isEnabled() : 계정이 활성화(사용가능)인지 return (true : 활성화)

필수 오버라이드 메소드 구현

🥲 처음 공부할때 제일 어려웠던 점 ...
단순히 인증정보를 위한 정보를 제공해준다고 하여, getPassword()와 getUsername() 메소드에 대해서 구현하고, 나머지 isAccountNonExpired, isCredentialsNonExpired()는 구현하지 않아 모두 false로 되어있어 적용이 안되었던 경험이 있다...


이 부분 절대 놓치지 말자 !! 그리고 검색하고 글을 꼼꼼히 자세히 읽자 !!

@Data
public class CustomDetails implements UserDetails {
	
   private SiteUser siteUser;
   public CustomDetails(SiteUser siteUser) { this.siteUser = siteUser; }
	// 해당 유저의 권한을 리턴
   @Override
   public Collection<? extends GrantedAuthority> getAuthorities() {
   	// 특별한 권한 시스템을 사용하지 않을 경우
       // retrn Collections.EMPTY_LIST;
       // 를 사용하면 된다.
       Collection<GrantedAuthority> collect = new ArrayList<>();
       collect.add(new GrantedAuthority() {
           @Override
           public String getAuthority() {
               return siteUser.getRole();
           }
       });
       return null;
   }
	// 비밀번호 정보 제공
   @Override
   public String getPassword() {
       return siteUser.getPassword();
   }
	// ID 정보제공
   @Override
   public String getUsername() {
       return siteUser.getUsername();
   }
   //    계정이 만료되지 않았는지 리턴 (true: 만료안됨)
   @Override
   public boolean isAccountNonExpired() {
       return true;
   }
   //    계정이 잠겨있는지 않았는지 리턴 (true: 잠기지 않음)
   @Override
   public boolean isAccountNonLocked() {
       return true;
   }
   //    비밀번호가 만료되지 않았는지 리턴한다. (true: 만료안됨)
   @Override
   public boolean isCredentialsNonExpired() {
       return true;
   }
   //    계정이 활성화(사용가능)인지 리턴 (true: 활성화)
   @Override
   public boolean isEnabled() {
       return true;
   }
}

Step 3. CustomUserDetailService 만들기

사용자의 인증정보를 제공할 UserDetails를 만들었다면 받은 정보를 토대로 사용자를 찾고 UserDetails를 만들어서 SecurityContextHorder에 제공할 CustomUserDetailService를 만들어야한다.


✅ 즉, UserDetailService를 구현해야한다

혹시나 왜 구현체를 implements해야하는지 이해가 안된다면 참고자료를 확인하면 좋다.
해당 글은 완벽한 동작원리는 안나와있지만 inteface implements를 통해 어떻게 Custom이 가능하고, 동적으로 구현체를 선택 쉽게 설명이 되어있다고 생각된다.

필수 오버라이드 메소드 설명

public class CustomUserDetailsService implements UserDetailsService{
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return null;
    }
}
  • loadUserByUsername() : 사용자의 Id(PK, 스프링부트에서는 username으로 고정)d에 해당하는 사용자 정보를 받아서 알맞은 UserDetails 객체를 반환해준다.
  • 📌 이때, 중요한점은 return 값은 반드시 위에서 UserDetails를 구현한 CustomDetails를 return 해주어야한다. 즉, 그리고 이 과정을 위해서 UserDetails를 구현한 CustomDetails를 구현한 것이다
@Service
public class CustomUserDetailsService implements UserDetailsService {
    @Autowired
    private SiteUserRepository siteUserRepository;
    // 시큐리티 session에는 Authentication타입 필요
    // Authentication타입에서 User를 검사하기 위한 UserDetails타입 필요
    // 따라서 securityconfig의 loginProcessingUrl의 경로 접근 시 loadUserByUsername이 호출되어
    // UserDetails타입을 Authentication 내부에 return을해준다.
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        SiteUser user = siteUserRepository.findByUsername(username)
                .orElseThrow(() -> {
                    throw new UsernameNotFoundException("해당 %s를 찾을 수 없습니다. 다시 확인해주세요.".formatted(username));
                });
        return new CustomDetails(user);
    }
}

Step 4. Configuration 사용설정

기존에는 WebSecurityConfigurerAdapter를 상속받은 Config Class를 만들어 Security 설정을 했다.
하지만 Spring Security 5.7.0M-2부터 deprecated되어버렸다.

그렇기 때문에 해당 설정 방법은 Spring Boot 2.7.8이하는 이전 방법으로 설정해야한다. 이전 설정 방법은 참고자료를 확인하면된다.

WebSecurity

WebSecurity는 패턴에 해당하는 리소스에 아예 Spring Security를 적용하지 않도록 설정한다.
즉, 패턴이 적용된 리소스는 자유롭게 Access(접근)가 가능하다는 것이다.

하지만, WebSecurity는 Spring Security Filter Chain을 거치지 않기 때문에 '인증', '인가' 서비스가 모두 적용되지 않는다.
그렇기 떄문에 (XSS), content-sniffing에 대한 보호가 제공되지 않는다.

HttpSecurity

HttpSecurityWebSecurity에서 제외된 외의 부분에 Spring Security를 설정할 수 있게 해준다.
authorizeRequests를 통해 리소스의 인증 부분을 ignore할 수 있다고해도 그 외에 다른 security 기능들에 대해서는 영향을 받는다.

결국 WebSecurity는 HttpSecurity의 상위에 있다. 그렇기 때문에 WebSecurity 패턴에 적용되는 리소스들은 접근이 자유롭게 가능해지는 것이다.
그러므로 보안과 전혀 상관없는 페이지에는 WebSecurity를 적용하고, 그 외에는 HttpSecurity를 적용하는 것이 좋다.
조금 더 쉽게 말하면, 보안처리가 필요한 곳은 HttpSecurity를 적용하고, 그 외(정적리소스,HTML)에는 WebSecurity를 적용한다.

Configuration Class 작성

Bean 등록 객체 설명

  • configure(WebSecurity web): WebSecurity에 대한 설정
  • filterChain(HttpSecurity http) : HttpSecurity에 대한 설정
  • daoAuthenticationProvider() : UserDetailsService 및 PasswordEncoder를 사용하여 사용자 아이디와 암호를 인증하는 AuthenticationProvide 구현한 설정
  • passwordEncoder() : DB 저장 시, password Encoding 설정
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
    private final CustomUserDetailsService customUserDetailsService;
    /** WebSecurity 설정부분 **/
    @Bean
    public WebSecurityCustomizer webSecurityCustomizer() {
        return (web) -> web.ignoring().antMatchers("/images/**", "/js/**", "/webjars/**");
    }
    
    /** HttpSecurity 설정부분**/
    @Bean
    SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
	/** 시큐리티 4.x버전부터는 disable()하지 않는 한 자동으로 활성화된다.**/
        http.csrf().disable();
    /** Default는 모든 리소스에 대해 인증정보를 요구한다. **/
    /** permitAll시 해당 리소스에 대한 인증정보를 요구하지 않는다. **/
    /** hasAnyRole시 해당 리소스에는 특정 권한 정보를 요구한다. **/
    
        http.authorizeHttpRequests()
                .requestMatchers(
                        new AntPathRequestMatcher("/question/modify/**")).authenticated()
                .requestMatchers(
                        new AntPathRequestMatcher("/question/create**")).authenticated()
                .anyRequest().permitAll()
                .and()
                .formLogin()
                    .loginPage("/loginForm")
                    .loginProcessingUrl("/login")
                    .failureForwardUrl("/loginError")
                    .failureHandler(new CustomAuthFailureHandler())
                    .successHandler(new CustomSuccessHandler())
                    .defaultSuccessUrl("/")
                .and()
                    .logout()
                    .logoutUrl("/logout")
                    .logoutSuccessUrl("/");
        return http.build();
    }
    @Bean
    public DaoAuthenticationProvider daoAuthenticationProvider() {
        DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
        authenticationProvider.setUserDetailsService(customUserDetailsService);
        authenticationProvider.setPasswordEncoder(passwordEncoder());
        authenticationProvider.setHideUserNotFoundExceptions(false);
        return authenticationProvider;
    }
    
    /**  **/
    @Bean
    AuthenticationFailureHandler customAuthFailureHandler(){
        return new CustomAuthFailureHandler();
    }
    
    /** DB에 저장 시, 비밀번호를 encoding 설정 부분 **/
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
}

추가설명

DaoAuthenticationProvider는 사용자 이름과 암호를 인증하기 위해 UserDetailsService와 PasswordEncoder를 사용하는 AuthenticationProvider 구현체입니다.

해당 그림은 스프링 시큐리티 내에서 DaoAuthenticationProvider가 어떻게 작동하는지 보여줍니다.
1. 사용자 이름 및 비밀번호 읽기 섹션의 인증필터는 사용자 이름 비밀번호 인증 토큰을 ProciderManager에 의해 구현되는 AuthenticationManager로 전달합니다.
2. ProviderManager는 DaoAuthenticationProvider 유형의 AuthenticationProvider를 사용하도록 구성합니다.
3. DaoAuthenticationProvider는 UserDetailsServcie에서 UserDetails를 조회합니다.
4. DaoAuthenticationProvider는 PasswordEncoder를 사용하여 이전 단계에서 반환된 UserDetails에서 비밀번호의 유효성을 검사합니다.
5. 인증이 성공하면 반환되는 인증은 UsernamePasswordAuthenticationToken유형이며, 구성된 UserDetailsService에서 반환된 UserDetails인 주체를 갖습니다.
최종적으로 반환된 UsernamePasswordAuthenticationToken은 인증필터에 의해 SecurityContextHolder에 설정됩니다.

헷갈리는 부분을 나의 말로 바꾸어보면 3번 단계에서 UserDetailsService으로부터 반환된 UserDetails의 비밀번호를 유효성 검사한다는 말은 UserDetailsService는 받은 정보를 토대로 사용자를 찾고 UserDetails를 만들어서 SecurityContextHorder에 제공하기 때문에 사용자의 정보를 제공해주는 UserDetail객체와 PasswordEncoder를 사용하여 비밀번호 유효성 검사를 진행한다는 것이다 !!!

DaoAuthenctionProvider 참고자료1
DaoAuthenctionProvider 참고자료2
AuthenticationProvider 참고자료

🧑🏻‍💻정리


Spring Security는 정말 어렵다고 느껴지지만,, 이 어려운걸 조금씩 이해하고 있는 것 같다는 생각에 매우 뿌듯해진다...ㅋㅎㅋㅎ(이해하고있다는 것은 .. 나의 착각일 수도 ..?) 그리고 점점 기록하는 습관을 들이고 있는 것 같아서 매우 뿌듯하다 😄
다음 글은 Spring Security와 JWT를 적용해보는 글을 작성해보자 !

profile
Back-End Developer

0개의 댓글