[Spring] 스프링 시큐리티의 모든 것(feat. spring boot 최신 3.xx 버전)

cielo ru·2024년 5월 21일
0

Spring

목록 보기
1/9
post-thumbnail

Spring에서 사용자 서비스를 담당한 적이 있는데 인증과 인가 처리를 위해 spring security를 사용했고, 제대로 이해도 못한 채 코드를 짜는 내 모습을 보게 되었다. 당연하게도 구현이 잘 되지 않았다. 검색해서 나오는 모든 블로그 글을 다 읽었고 그제서야 구조와 흐름이 이해가 갔다.

이 경험을 바탕으로 spring security 에 대해 정리해두면 좋을 것 같아 구조, 흐름, 특징, 최신버전 security, 사용 이유에 대해 정리해보려 한다.

➰ 스프링 시큐리티란?

스프링 기반 웹 애플리케이션에서 인증(Authentication)과 권한(Authorization)을 담당하는 스프링의 하위 프레임워크

  1. 스프링에서 보안(인증, 인가)과 관련된 기능을 구현
  2. 애플리케이션의 취약점을 방지하여 안전한 서비스를 제공한다.

즉 , 개발자 입장에서는 일일이 보안관련 로직을 작성하지 않아도 되는 편리함이 있다.


CF.인증과 인가
인증 -> 인가 순으로 진행
- 인증(Authentication) : 시스템에 접근하기 위해 식별 가능한 정보 (이름 , 이메일)를 이용하여 회원의 정보를 확인하는 것
- 인가(Authorization) : 특정 리소스에 접근하려는 회원의 권한을 확인하는 것

EX.예시
중고거래 사이트에서 물건을 거래하려고 할 때 해당 사이트에 등록되어 있는 회원이 맞는지 확인(인증)하고, 내가 작성한 글을 삭제하려고 할 때 접근하려는 자원에 대한 권한이 있는지 확인(인가)한다.

Q. 인가가 꼭 필요한가요?
=> 인증을 한 사용자에게 모든 서비스를 사용하게 한다면 서비스 운영에 지장이 생길 수 있다. 다른 사람이 내가 작성한 글을 지울 수 있고, 개인정보를 열람하여 도용할 위험이 있다. 따라서 인증된 사용자가 접근하려는 자원에 대한 권한이 있는지 확인하는 절차가 필요하다.

AD. Credential 기반 인증 방식
Spring Security에서는 인증과 인가를 위해 Principal을 아이디로, Credential을 비밀번호로 사용하는 Credential 기반의 인증 방식을 사용한다.

  • Principal(접근 주체): 보호받는 Resource에 접근하는 대상
  • Credential(비밀번호): Resource에 접근하는 대상의 비밀번호

➰ 스프링 시큐리티 특징

  1. Filter 기반으로 동작한다.
    (MVC와 분리하여 관리 및 동작)
  2. Bean으로 설정할 수 있다.
    (어노테이션을 통한 간단한 설정이 가능)
  3. Spring Security는 기본적으로 세션 & 쿠키 방식으로 인증한다.
  4. 인증관리자(Authentication Manager)와 접근 결정 관리자(Access Decision Manager)를 통해 사용자의 리소스 접근을 관리한다.
    (인증 관리자는 UserNamePasswordAuthenticationFilter, 접근 관리자는 FilterSecurityInterceptor가 수행)

Security는 '인증'과 '권한'에 대한 부분을 Filter 흐름에 따라 처리한다.
=> Filter는 Dispatcher Servlet으로 가기 전에 적용되므로 가장 먼저 URL 요청을 받지만, Interceptor는 Dispatcher와 Controller사이에 위치한다는 점에서 적용 시기의 차이가 있다.(자세한 내용은 다음 블로그 참고)

흐름 참고

Client(request) -> Filter -> DispatcherServlet -> Intercepter -> Controller

➰ 스프링 시큐리티 구조 및 처리과정

이제 스프링 시큐리티 구조에 대해 알아보자.

출처 : www.springbootdev.com

  1. 사용자가 로그인 정보와 함께 로그인 요청(인증)을 한다.
    => Http request 요청

    CF) 로그인이 이미 되어 있다면?
    : 이미 Authentication 객체가 HTTP Session 에 저장되어 있기 때문에 AuthenticationManager의 authenticate() 메소드를 호출하지 않고 SecurityContextHolder에서 Authentication 객체를 가져와 인증 후 곧바로 클라이언트 인증을 수행한다.(3번, 10번 참고)

  2. AuthenticationFilter가 요청을 가로채고, 가로챈 정보를 통해 UsernamePasswordAuthenticationToken(인증용 객체)을 생성한다.

    EX)시큐리티에서 주로 사용하는 필터(유저 관련)
    - UsernamePasswordAuthenticationFilter : 사용자 이름과 비밀번호를 사용한 기본 폼 기반 인증을 처리하는 필터, 로그인에 성공하면 이 필터가 사용자를 인증하고 세션에 사용자 정보를 저장한다.
    - JwtAuthenticationFilter : JWT 를 사용한 사용자 인증을 처리하는 필터, 클라이언트가 jwt 토큰을 주면 필터가 해당 토큰으로 사용자를 인증한다.

  3. AuthenticationManager의 구현체인 ProviderManager에게 생성한 UsernamePasswordToken 객체를 전달한다.

    CF) 생성된 UsernamePasswordToken 은 AuthenticationManager의 authenticate() 메소드를 통해 ProviderManager에게 전달된다.

  4. AuthenticationManger는 등록된 AuthenticationProvider들을 조회하여 인증을 요구한다.

  5. 실제 DB에서 사용자 인증정보를 가져오는 UserDetailsService에 사용자 정보를 넘겨준다.

    CF)UserDetailsService는 유저의 정보를 가져오는 인터페이스로 해당 인터페이스로 구현하기 위해서는 DB로부터 유저의 정보를 불러와 UserDetails 객체로 반환하는 loadUserByUsername 메소드를 구현해야한다.

  6. 넘겨받은 사용자 정보를 통해 데이터베이스에서 찾아낸 사용자 정보인 UserDetails 객체를 만든다.

  7. AuthenticaitonProvider들은 UserDetails를 넘겨받고 사용자 정보를 비교한다.

  8. 인증이 완료가 되면 권한 등의 사용자 정보를 담은 Authentication 객체를 반환한다.

    ** 필자는 Authentication 객체에 어떤 정보가 담기는지 이해하지 못한 채 코드를 짜서 많이 헷갈렸다. Authentication 객체에는 권한, 아이디, 이름 등 사용자 정보가 담겨있다.

  9. 다시 최초의 AuthenticationFilter에 Authentication 객체가 반환된다.

  10. Authentication 객체를 Security Context에 저장한다.

  • SecurityContextHolder는 세션 영역에 있는 SecurityContext 에 Authentication 객체를 저장한다.
  • Security Context
    • Authentication 객체가 직접 저장되는 저장소
    • 필요시 언제든지 authentication 객체를 가져올 수 있다.

이후의 인증과정은 SecurityContextHolder 내부의 SecurityContext에 접근하여 확인한다.

Authentictaion authentication = SecurityContextHolder.getContext().getAuthentication();

➰ 스프링 3.xx 버전 시큐리티 설정

스프링 버전 3 이상 버전이 나온 지 얼마 되지 않았을 때 스프링 시큐리티 적용을 처음 했었다. 시큐리티에 대해서 아무것도 몰랐기 때문에 검색해서 나온 코드를 보고 작성했었는데 절대 적용이 되지 않았다. 시큐리티 버전이 안맞는다고 그래서 바꿔도 보고 코드를 이리도 바꾸고 저리도 바꿔봤지만 해결이 되지 않았다. 이유는 deprecated 되었다고.. 따로 빈으로 filterchain을 등록해줘야했다. 그러니까 adapter를 extends 한게 애초에 먹히지 않았던 것..

@Bean 등록

기존 코드

@Configuration
@EnableWebSecurity // Spring Security 설정 활성화

public class WebSecurityConfig extends WebSecurityConfigurerAdapter { //adapter
 
    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
     
    @Override
    protected void configure(HttpSecurity http) throws Exception {
    	
        http.csrf().disable();
        
        http.authorizeRequests().antMatchers("/login").permitAll()
                .antMatchers("/users/**", "/settings/**").hasAuthority("admin")
                .hasAnyAuthority("admin", "user")
                .anyRequest().authenticated()
                .and().formLogin()
                .loginPage("/login")
                    .usernameParameter("email")
                    .permitAll()
                .and()
                .logout().permitAll();
 
    }
     
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/images/**", "/js/**", "/webjars/**"); 
    }
}

수정 후

@Configuration
@EnableWebSecurity
public class WebSecurityConfig {

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf.disable())
            .authorizeRequests(auth -> auth
                .antMatchers("/login").permitAll()
                .antMatchers("/images/**", "/js/**", "/webjars/**").permitAll()
                .antMatchers("/users/**", "/settings/**").hasAuthority("admin")
                .antMatchers("/**").hasAnyAuthority("admin", "user")
                .anyRequest().authenticated()
            )
            .formLogin(form -> form
                .loginPage("/login")
                .usernameParameter("email")
                .permitAll()
            )
            .logout(logout -> logout.permitAll());

        return http.build();
    }
}

기존에 WebSecurityConfigurerAdapter를 extends 해서 configure 함수를 오버라이드 했던 방식에서 스프링 3.xx 으로 업그레이드 되면서 SecurityFilterChain을 @Bean 등록해서 사용하는 방식으로 바뀌었다.

deprecated 된 메소드와 람다식 사용

아래 문서를 참고하면 deprecated 된 메소드 목록을 확인할 수 있다.
and(), csrf(), formLogin(), logout() 등 자주 사용하던 메소드들이 사용하지 못하는 걸 확인할 수 있다. 해당 메소드들을 위의 수정된 코드처럼 람다형식으로 바꿔서 사용해야 한다.

EX)

.formLogin() //기존
.formLogin(form -> form.loginPage("/login") //업데이트
http.csrf().disable(); //기존
http
	.csrf((csrf) -> csrf.disable()); //업데이트

만약 코드가 안먹고 빨간줄이 계속 떠있다면 아래 문서에서 내가 사용한 함수는 없는지 확인하는 게 좋을 것 같다.

공식 문서 참고
https://docs.spring.io/spring-security/site/docs/current/api/deprecated-list.html


➰ 참고

https://docs.spring.io/spring-security/reference/migration-7/configuration.html#_use_the_lambda_dsl
https://docs.spring.io/spring-security/site/docs/current/api/deprecated-list.html
https://mysterlee.tistory.com/98
https://hello-judy-world.tistory.com/216

profile
Cloud Engineer & BackEnd Developer

0개의 댓글