[Spring Boot] (2) 로그인/회원가입 - Spring Security 설정 :: 음악학원 홈페이지 프로젝트

2020. 8. 12. 17:36Spring Boot

클릭해서 확대하면 오류없이 잘 나옵니다...

 

들어가기 전에 ...

스프링 시큐리티 Spring Security에 대해

스프링 시큐리티를 이용하면 개발시에 필요한 사용자의 인증, 권한, 보안 처리를 간단하지만 강력하게 구현 할 수 있습니다.  일반적인 웹 환경에서 브라우저가 서버에게 요청을 보내게 되면, DispatcherServlet 이 요청을 받기 이전에 많은 ServletFilter(서블릿 필터)들을 거치게 됩니다. Spring Security도 서블릿 필터로써 작동하여 인증, 권한과 관련한 처리를 진행하게 됩니다.

 

스프링 필터 체인?

Security와 관련한 서블릿 필터도 실제로는 연결된 여러 필터들로 구성 되어 있습니다. 이러한 모습때문에 Chain(체인)이라는 표현을 쓰고 있습니다. 해당 필터들의 역할과 흐름을 알고 있어야 필터의 커스터마이징을 진행 할 수 있습니다. 

 

 

 

SecurityContextPersistenceFilter : SecurityContext 객체의 생성, 저장, 조회를 담당하는 필터입니다. request 시,

 

- 익명사용자의 경우, 새로운 SecurityContext 객체를 생성하여 SecurityContextHolder에 저장합니다: AnonymousAuthenticationFilter에서 AnonymousAuthenticationToken객체를 SecurityContext에 저장합니다.

 

- 인증시, 새로운 SecurityContect 객체를 생성하여 SecurityContextHolder에 저장합니다: UsernamePasswordAuthenticationFilter에서 인증 성공 후 SecurityContext에 UsernamePasswordAuthentication 객체를 Authentication 객체와 함께 저장합니다. 인증이 완료되면 Session에 SecurityContext를 저장하고 Response 합니다.

 

- 인증 후, Session에서 SecurityContext를 꺼내 SecurityContextHolder에 저장합니다. SecurityContext내 Authentication객체가 있으면 인증을 유지합니다.

 

LogoutFilter : 유저의 로그아웃을 진행합니다. 설정된 로그아웃 URL로 오는 요청을 감시하며, 해당 유저를 로그아웃 처리합니다.

 

UsernamePasswordAuthenticationFilter : 설정된 로그인 URL로 오는 요청을 감시하며, 유저인증을 처리합니다. 인증 실패 시, AuthenticationFailureHandler를 실행합니다.

 

DefaultLoginPageGeneratingFilter : 사용자가 별도의 로그인 페이지를 구현하지 않은 경우, 스프링에서 기본적으로 설정한 로그인 페이지를 처리합니다.

 

BasicAuthenticationFilter : HTTP 요청의 (BASIC)인증 헤더를 처리하여 결과를 SecurityContextHolder에 저장합니다.

 

RememberMeAuthenticationFilter : SecurityContext에 인증(Authentication) 객체가 있는지 확인하고 RememberMeServices를 구현한 객체의 요청이 있을 경우, Remember-Me 인증 토큰으로 컨텍스트에 주입합니다.

 

AnonymousAuthenticationFilter : SecurityContextHolder에 인증(Authentication) 객체가 있는지 확인하고, 필요한 경우 Authentication 객체를 주입합니다.

 

SessionManagementFilter : 요청이 시작된 이 후 인증된 사용자 인지 확인하고, 인증된 사용자일 경우 SessionAuthenticationStrategy를 호출하여 세션 고정 보호 메커니즘을 활성화하거나 여러 동시 로그인을 확인하는 것과 같은 세션 관련 활동을 수행합니다.

 

ExceptionTranslationFilter : 필터 체인 내에서 발생(Throw)되는 모든 예외(AccessDeniedException, AuthenticationException)를 처리합니다.

 

FilterSecurityInterceptor : HTTP 리소스의 보안 처리를 수행합니다.

 

 

어떻게 적용하나요

원래 Spring XML 기반 설정에서는 web.xml에 org.springframework.web.filter.deletegatingFilterProxy라는 springSecurityFilterChain을 등록하는 것으로 시작하지만, 스프링 프레임워크 3.1 버전 이상의 경우에는 어노테이션을 통한 자바 설정을 지원하기 때문에, 자바 기반의 설정에서는 WebSecurityConfigurerAdapter를 상속받은 클래스에 @EnableWebSecurty 어노테이션을 명시하는 것 만으로도 springSecurityFilterChain이 자동으로 포함되어 집니다. 

 

본 포스팅에서도 WebSecurityConfigurerAdapter를 상속받아 디테일한 Security 설정을 하도록 하겠습니다.

 

0.  의존성 주입

dependencies {
    compile('org.springframework.boot:spring-boot-starter-security')
    implementation('org.springframework.boot:spring-boot-starter-thymeleaf')
    implementation('org.thymeleaf.extras:thymeleaf-extras-springsecurity5')
}

스프링 시큐리티를 관련 의존성을 Maven Repository에서 추가해 줍니다. 시큐리티를 적용한 데이터가 화면단에 뿌려질 예정이므로 타임리프가 이를 사용할 수 있도록 해당 의존성도 추가해 줍니다.

 

 

1.  SpringSecurity

 

import com.soriel.music.springboot.service.soriel.CustomOAuth2UserService;
import com.soriel.music.springboot.service.soriel.MemberService;
import lombok.AllArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

@Configuration
@EnableWebSecurity
@AllArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    MemberService memberService;

    @Autowired
    private CustomOAuth2UserService customAutowireConfigurer;

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

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/css/**", "/js/**", "/images/**", "/fonts/**", "/templates/**");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                    .antMatchers("/admin/**").hasRole("ADMIN")
                    .antMatchers("/user/myinfo").hasRole("MEMBER")
                    .antMatchers("/**").permitAll()
                .and()
                    .formLogin()
                    .loginPage("/user/login_page")
                    .loginProcessingUrl("/user/login")
                    .defaultSuccessUrl("/")
                    .permitAll()
                .and()
                    .csrf()
                    .ignoringAntMatchers("/h2-console/**")
                    .ignoringAntMatchers("/post/**")
                    .ignoringAntMatchers("/admin/**")
                    .ignoringAntMatchers("/video_board/**")
                .and()
                    .logout()
                    .logoutRequestMatcher(new AntPathRequestMatcher("/user/logout"))
                    .logoutSuccessUrl("/")
                    .invalidateHttpSession(true)
                .and()
                    .exceptionHandling()
                    .accessDeniedPage("/error")
                .and()
                    .oauth2Login()
                        .userInfoEndpoint()
                            .userService(customAutowireConfigurer);
        //http.csrf().disable();
        http.headers().frameOptions().disable();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(memberService).passwordEncoder(passwordEncoder());
    }
}

 

@Configuration, @EnableWebSecurity

@Configuration 클래스에 @EnableWebSecurity 어노테이션을 추가하여 Spring Security를 설정할 클래스라고 정의합니다.

WebSecurityConfigurerAdapter을 상속하면 Spring Security를 본격적으로 사용할 수 있습니다.

 

이때 WebSecurityConfigurerAdapter을 상속하면

  • configure(WebSecurity web)
  • configure(HttpSecurity http)
  • configure(AuthenticationManagerBuilder auth)

 

세 가지의 메소드가 오버라이딩 되는데 각각의 용도는 다음과 같습니다.

 

1. configure(WebSecurity) : 서비스 전체에 영향을 미치는 설정으로 사용됩니다. 예컨대 리소스를 무시하여 엔드포인트에서 해당 리소스에 대해 CSRF를 방지하는 등의 보안절차를 건너뛰게 하거나, 디버그 모드를 세팅하거나, 방화벽 설정을 커스텀하여 특정 리퀘스트를 거부할 수 있습니다. 위 예제에서도 web.ignoring 메소드를 통해 css, js, fonts, templates 파일 안의 리소스들에 대한 보안절차를 생략하고 있습니다.

 

2. configure(HttpSecurity) : HttpSecurity 클래스를 주입함으로써 특정 http 요청에 대한 웹 기반 보안을 구성할 수 있도록 도와주는 메소드 입니다. 일반적인 http 취약성에 대한 방어가 필요한 모든 엔드포인트를 여기에 지정할 수 있습니다. 특히 HttpSecurity에서 지원하는 authorizeRequest 메소드는 URL 패턴을 통한 HttpServletRequest 접근에 대한 제한권한을 획득합니다.

 

스프링 시큐리티는 기본적으로 "/login" 경로의 로그인 페이지와 "/login?error" 경로의 로그인 실패 페이지를 HttpSecurity를 상속받은FormLoginConfigurer에서 제공합니다. FormLoginConfigurer의 loginform() 메소드를 오버로드하여 로그인 페이지 접근 url을 명시하면 기본 로그인 페이지는 사라지고 해당 경로로 접근되는 페이지가 로그인 페이지로 인지됩니다. FormLoginConfigurer 클래스의 기능은 이 뿐아니라 로그인 성공, 실패시 리다이렉트 되는 경로 커스텀, 로그인 시 request를 Security FilterChain으로 보내는 url을 사용자가 지정 경로 커스텀을 지원합니다.

 

HttpSecurity클래스를 확장한 클래스 중에는 CsrfConfigurer 클래스도 있습니다. 이 클래스는 CsrfFilter을 추가시킴으로써 requireCsrfProtectionMatcher에서 지정한 방법에 대한 CSRF 보호기능을 활성화 시킵니다. 예제의 csrf 메소드는 이 클래스의 인스턴스 생성자입니다. ignoringAntMatchers() 메소드의 인자로 등록한 url에 대하여서는 CSRF 보호를 하지 않습니다.

 

LogoutConfigurer 클래스 역시 HttpSecurity를 확장했습니다. 이 클래스는 LogoutFilter를 추가하여 로그아웃 기능을 구현하고 있습니다. 본문의 logout 메소드는 LogoutConfigurer 클래스의 생성자입니다. logoutRequestMatcher 클래스는 인자로 들어오는 RequestMatcher 객체로 로그아웃을 진행하게 됩니다. AntPathRequesMatcher는  RequestMatcher을 구현하는 클래스이므로 해당 생성자에 String 타입의 url pattern을 주입함으로써 사용할 수 있습니다. LogoutSuccessfulUrl()은 로그아웃 성공 후, 정의된 url 로 리다이렉트 시키는 메소드입니다. 로그아웃 후 SecurityContext를 Session에서 제거할 것인지도 설정할 수 있습니다. 기본적으로 invalidateHttpSession(true)로 설정되어 있지만, invalidateHttpSession 메소드의 인자값을 false 로 두면 이 기능이 비활성화됩니다.

 

이외에도 확장된 ExceptionHandlingConfigurer 클래스를 이용하여 모든 에러에 대한 리다이렉트 url 을 설정할 수도 있고, OAuth2LoginConfigurer 클래스를 통해서 소셜 로그인 기능을 활성화 시킬 수 있습니다. OAuth2 를 이용한 로그인 설정은 다음 포스팅에서 진행합니다.

 

3. configure(AuthenticationManagerBuilder) : AuthenticationManager()를 획득하기 위해 AuthenticationManager()의 기본 구현에 사용됩니다. 오버로드 된 경우 AuthenticationManagerBuilder를 사용하여 AuthenticationManager를 지정해야 합니다. AuthenticationManagerBuilder의 userDetailsService() 메소드는 인자의 사용자 클래스가 UserDetailService 인터페이스를 구현하는 경우 해당 클래스를 DaoAuthenticationConfigurer 클래스의 인자에 담아 DaoAuthenticationConfigurer 객체를 리턴시킴으로써 사용자 클래스를 DaoAuthenticationConfigurer 클래스의 인자로써 인가절차를 커스텀 할 수 있게 해줍니다. 예제에서 비밀번호 암호화 메소드로 사용되는 passwordEncoder()는 AbstractDaoAuthenticationConfigurer클래스의 메소드로, 이 클래스는 경우 아래와 같이 DaoAuthenticationConfigurer에 의해 상속받고 있습니다.

 * @param <B> the type of the {@link SecurityBuilder}
 * @param <C> the type of {@link AbstractDaoAuthenticationConfigurer} this is
 * @param <U> The type of {@link UserDetailsService} that is being used

abstract class AbstractDaoAuthenticationConfigurer
<B extends ProviderManagerBuilder<B>,
C extends AbstractDaoAuthenticationConfigurer<B, C, U>,
U extends UserDetailsService>

따라서 리턴된 DaoAuthenticationConfigurer 객체에 passwordEncoder() 메소드가 수행되며 결과적으로 스프링 시큐리티의 PasswordEncoder 인터페이스를 구현한 BCryptPasswordEncoder 가 동작됨으로써 SecurityContext 패스워드에 대한 암호화가 진행되는 것입니다.

 

 

WebSecurityConfigurerAdapter를 사용하면 스프링 시큐리티의 기능들을 편리하게 사용할 수 있기 때문에 간단히 시큐리티를 적용하는 프로젝트의 경우에는 애용되는 추세입니다. 다음 포스팅에는 설정한 Spring Security를 적용하여 일반 로그인 회원가입과 카카오 로그인을 진행해 보도록 하겠습니다.