본문으로 바로가기

 로그인 상태인 회원만 글을 작성하게 하려면 세션을 활용해볼 수 있다. 하지만 이 경우 로그인 시에 세션을 만들어 반환해주고, 기능에 따라 세션을 자체적으로 계속 체크해주어야한다. 물론 세션은 여러 문제가 있기 때문에 JWT를 이용할 것인데 스프링 시큐리티를 사용하면 편리하게 인증 및 인가를 구현할 수 있다고 한다. 사실 JWT만 이용하면 되지 않을까 생각을 했었는데, 스프링 시큐리티를 이용하면 패스워드 암호화나 페이지 권한 등 직접 개발해야하는 문제를 모두 도와준다고 한다. 아직은 어떠한 장점이 있는지 잘 모르기 때문에 천천히 공부하면서 알아가보려고 한다.

 

스프링 시큐리티(Spring Security)란?

  • Spring 기반의 애플리케이션의 보안(인증과 권한, 인가 등)을 담당하는 스프링 하위 프레임워크
  • 기본적으로 세션 & 쿠키 방식으로 인증한다.
  • 인증과 권한에 대한 부분을 Filter 흐름에 따라 처리하고 있다.
  • 보안과 관련해서 체계적으로 많은 옵션을 제공해주기 때문에 개발자 입장에서 로직을 하나하나 작성하지 않아도 된다는 장점이 있다.
  • 인증관리자(Authentication Manager)와 접근 관리자(Access Decision Manager)를 통해 사용자의 리소스 접근을 관리한다.
    • 인증 관리자는 UserNamePasswordAuthenticationFilter, 접근 관리자는 FilterSecurityInterceptor가 수행한다.

 

인증(Authorizatoin)과 인가(Authentication)

인증(Authentication): 해당 사용자가 본인이 맞는지 확인하는 절차(로그인)
인가(Authorizatoin): 인증된 사용자가 요청한 자원에 접근 가능한지 결정하는 절차(권한부여, 허가)
접근 주체(Principal): 보호받는 자원에 접근하는 유저
비밀번호(Credential): 자원에 접근하는 유저의 비밀번호
  • 스프링 시큐리티는 기본적으로 인증 절차를 거친 후에 인가 절차를 진행한다.
    • 인가 과정에서 해당 리소스에 대한 접근 권한이 있는지 확인한다.
  • 스프링 시큐리티는 이러한 인증과 인가를 위해 Principal을 아이디로, Credential을 비밀번호로 사용하는 Credential 기반의 인증 방식을 사용한다.

 

스프링 시큐리티 구조(Spring Security Architecture)

https://velog.io/@dailylifecoding/spring-security-authentication-process-flow

  1. Client에서 로그인 요청
  2. UsernamePasswordAuthenticationFIlter에서 Id와 Password를 담은 인증객체(Authentication)를 생성
  3. 인증관리자(AuthenticationManager)에게 인증객체를 넘기며 인증처리를 위임
  4. 인증관리자(AuthenticationManager)는 적절한 Provider(AuthenticationProvider)에게 인증처리를 위임
  5. 해당 Provider는 전달받은 인증 객체를 가지고 실제 인증 처리 역할을 함
    1. UserDetailsService 인터페이스의 loadUserByUsername(username) 메서드를 호출해서 유저 객체 요청
      1. Repository에 findById() 메서드로 유저 객체 조회
      2. 유저가 존재한다면 UserDetails 타입으로 반환(못찾으면 예외 발생)
  6. 인증관리자(AuthenticationManager)에서 Password 검증 시작
    1. 인증객체의 Password와 반환받은 UserDetails의 Password를 비교(실패 시 예외 발생)
    2. 성공한 인증객체(UserDetals와 authorities를 담은 인증 후 토큰 객체 Authentication)를 UsernamePasswordAuthenticationFilter에 전달
  7. SecurityContext에 저장
  8. 이후 전역적으로 SecurityContextHolder에서 인증 객체를 가져와 사용

 

SecurityConfig

@Configuration
@EnableWebSecurity // 필터 체인 관리 시작 어노테이션
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) // 특정 주소 접근시 권한 및 인증을 위한 어노테이션 활성화
public class SecurityConfig extends WebSecurityConfigurerAdapter{
	
	@Bean
	public BCryptPasswordEncoder encodePwd() {
		return new BCryptPasswordEncoder();
	}
	
	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http.csrf().disable();
		http.authorizeRequests()
			.antMatchers("/user/**").authenticated()
			//.antMatchers("/admin/**").access("hasRole('ROLE_ADMIN') or hasRole('ROLE_USER')")
			//.antMatchers("/admin/**").access("hasRole('ROLE_ADMIN') and hasRole('ROLE_USER')")
			.antMatchers("/admin/**").access("hasRole('ROLE_ADMIN')")
			.anyRequest().permitAll()
		.and()
			.formLogin()
			.loginPage("/loginForm")
			.loginProcessingUrl("/loginProc")
			.defaultSuccessUrl("/");
            
		http.addFilterBefore(new MyFilter1(), UsernamePasswordAuthenticationFilter.class); // 필터 등록
	}
}
  • configure(HttpSecurity http)
    • @EnableWebSecurity : 필터 체인 관리 시작 어노테이션으로 스프링 시큐리티 필터가 스프링 필터 체인에 등록됨
    • @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) : 특정 주소 접근시 권한 및 인증을 위한 어노테이션 활성화
      • prePostEnabled : @preAuthorize 와 @postAuthorize어노테이션 활성화
        • @preAuthorize 는 컨트롤러의 특정 메서드가 실행되기 직전에 실행됨
        • @preAuthorize("hasRole('ROLE_MANAGER') or hasRole('ROLE_ADMIN')") 로 여러 개의 접근 권한 설정 가능
      • securedEnabled : @secured 어노테이션 활성화
        • 컨트롤러의 특정 메서드에서 @secured("ROLE_ADMIN") 로 접근 권한을 간단하게 걸 수 있음
      • 이러한 속성으로 특정 어노테이션에만 권한을 걸 수 있고, configure 에서 .antMatchers("/admin/**").access("hasRole('ROLE_ADMIN') and hasRole('ROLE_USER')")와 같이 글로벌하게 걸수도 있음
    • loginProcessingUrl : 해당 url 주소가 호출되면 시큐리티가 낚아채서 대신 로그인을 진행해줌
    • defaultSuccessUrl: 성공 시 해당 url로 이동
  • BCryptPasswordEncoder
    • 비밀번호 암호화

 

UserDetails 구현체

// Authentication 객체에 저장할 수 있는 유일한 타입
@Data
public class PrincipalDetails implements UserDetails{

	private User user;

	public PrincipalDetails(User user) { // 생성자
		super();
		this.user = user;
	}

	@Override
	public String getPassword() { return user.getPassword(); }

	@Override
	public String getUsername() { return user.getUsername(); }

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

	@Override
	public boolean isAccountNonLocked() { return true

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

	@Override
	public boolean isEnabled() {
		// 1년동안 회원이 로그인을 안하면/계정 탈퇴를 하면 휴면 계정으로 하기로 함
		// [현재 시간] - user.getLoginDate() => 1년을 초과하면 return false;
		return true;
	}

	@Override
	public Collection<? extends GrantedAuthority> getAuthorities() { // 해당 User의 권한을 리턴
		Collection<GrantedAuthority> collet = new ArrayList<GrantedAuthority>();
		collet.add(()->{ return user.getRole();});
		return collet;
	}

}
  • 과정
    1. 시큐리티가 /login 주소 요청이 오면 낚아채서 로그인을 진행시킴
    2. 로그인 진행이 완료가 되면 시큐리티 sessin을 만들어 Security ContextHolder에 저장
      • 시큐리티에 들어갈 수 있는 오브젝트가 정해져있음(Authentication 타입 객체)
      • Authentication 안에 User 정보가 있어야 됨
      • User 오브젝트 타입도 정해져있음(UserDetails 타입 객체)
  • Security Session ⇒ Authentication ⇒ UserDetails(PrincipalDetails) 순서로 접근
    • Security Session 내부(Authentication 내부( UserDetails(PrincipalDetails)))

 

UserDetailsService 구현체

@Service
public class PrincipalDetailsService implements UserDetailsService{

	@Autowired
	private UserRepository userRepository;
	
	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		User user = userRepository.findByUsername(username);
		if(user == null) {
			return null;
		}
		return new PrincipalDetails(user);
	}

}
  • .loginProcessingUrl("/loginProc") 으로 login 요청이 오면 자동으로 UserDetailsService 타입으로 IoC 되어있는 loadUserByUsername 함수가 실행됨
  • loadUserByUsername(): 해당 함수에서 UserRepository를 이용해 User가 있는지 확인

 

스프링 시큐리티 필터(Spring Security Filter)

Servlet Filter

HTTP 요청 ➙ WAS ➙ 필터 ➙서블릿(디스패처 서블릿) ➙ 스프링 인터셉터 ➙ 컨트롤러
  • DispatcherServlet이 요청을 받기 전에 다양한 서블릿 필터들이 올 수 있다.
    • DispatcherServlet: Spring MVC의 프론트 컨트롤러로, Spring Boot는 DispatcherServlet을 서블릿으로 자동 등록하여 모든 경로에 대해 매핑한다.
  • 필터는 클라이언트와 자원 사이에서 요청과 응답 정보를 이용해 다양한 처리를 하는데 목적이 있다.
  • Filter들은 여러 개가 연결되어있어 Filter Chain이라고도 불린다.

 

Security의 Filter

https://velog.io/@seongwon97/Spring-Security-Filter%EB%9E%80

  • 스프링 시큐리티는 다양한 기능을 가진 필터들을 기본적으로 제공하고, 이렇게 제공되는 필터들을 Security Filter Chain이라고 한다.
  • 스프링 시큐리티는 서블릿 필터를 기반으로 동작한다.
    • 사용자의 요청이 서블릿에 전달되기 전에 필터의 생명주기를 이용해서 인증과 권한 작업을 수행한다.
    • 하지만 서블릿 컨테이너는 스프링 컨테이너에 등록된 빈을 인식할 수 없다.
      • Spring Bean은 스프링 컨테이너에서 생성및 관리하는 컴포넌트들이고, ServletFilter는 서블릿 컨테이너에서 생성 및 관리하는 필터들이기 때문에 서로 실행되는 위치가 다르기 때문
    • 이 때문에 스프링 시큐리티는 DelegatingFilterProxy라는 서블릿 필터의 구현체를 제공한다.
      • DelegatingFilterProxy는 서블릿 매커니즘으로 서블릿의 필터도 등록될 수 있고, 스프링에 등록된 빈을 가져와 의존성 주입을 할 수 있다.
      • 즉, 서블릿 필터를 구현한 스프링 빈에게 요청을 위임해주는 대리자 역할의 서블릿 필터
  • 스프링 시큐리티는 DelegatingFilterProxy라는 필터를 만들어 FilterChainProxy에 넣고, 그 아래에 Security Filter Chain 그룹을 등록한다.
    • FilterChainProxy는 DelegatingFilterProxy를 통해 받은 요청과 응답을 SecurityFilterChain에 전달하고 작업을 위임하는 역할을 한다.
    • DelegatingFilterProxy에서 바로 SecurityFilterChain을 실행할 수 있지만 중간에 FilterChainProxy를 두는 이유
      • 서블릿을 지원하는 시작점 역할을 하기 위해서
      • 서블릿에 문제가 발생할 경우, FilterChainProxy의 문제라는 것을 알 수 있다.
  • SecurityFilterChain은 인증을 처리하는 여러 개의 시큐리티 필터를 담는 필터 체인으로, 매칭되는 URL에 따라 적용되는 Filter Chain을 다르게 할 수 있다.

 

Security Filter 종류

  • SecurityContextPersistenceFilter: SecurityContextRepository에서 SecurityContext를 가져오거나 저장하는 역할
  • LogoutFilter: 설정된 로그아웃 URL로 오는 요청을 감시하고 해당 유저를 로그아웃 처리
  • UsernamePasswordAuthenticationFilter: (ID와 Password를 사용하는 실제 form 기반 인증을 처리) 설정된 로그인 URL로 오는 요청을 감시하고 유저 인증 처리
  • ConcurrentSessionFilter: 동시 접속을 허용할지 체크
  • RememberMeAuthenticationFilter: 세션이 사라지거나 만료되더라도, 쿠키 또는 DB를 사용하여 저장된 토큰 기반으로 인증을 처리
  • AnonymousAuthenticationFilter: 사용자 정보가 인증되지 않았다면 익명 사용자 토큰을 반환
  • SessionManagementFilter: 로그인 후, 인증된 사용자와 관련된 세션 작업을 처리
  • ExceptionTranslationFilter: 필터 체인 내에서 발생되는 인증, 인가 예외 처리
  • FilterSecurityInterceptor: 권한부여와 관련한 결정을 AccessDecisionManager에게 위임함으로써 권한 부여 결정 및 접근 제어 처리

 

Filter 생성

public class MyFilter1 implements Filter {
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        System.out.println("Filter 1");
        filterChain.doFilter(servletRequest, servletResponse);  // 다음 필터로 넘어가라는 의미
    }
}
  • Servlet 아래에 있는 Filter 인터페이스 구현
  • 해당 필터를 처리하고 다시 다음 필터로 넘겨주는 doFilter() 호출을 해주어야 한다.
  • [참고] 클라이언트에서 JWT 토큰을 담아 요청을 보내면, JWT 필터를 생성해서 JWT 토큰 검증을 제일 먼저 할 수 있다.

 

Filter 등록

 

1. SecurityConfig에 추가

@Configuration
@EnableWebSecurity // 필터 체인 관리 시작 어노테이션
public class SecurityConfig extends WebSecurityConfigurerAdapter{
	
	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http.addFilterBefore(new MyFilter1(), UsernamePasswordAuthenticationFilter.class); // 필터 등록
	}
}
  • addFilter(Filter filter)
    • 에러 발생 → SecurityFilter가 아니기 때문
    • SpringSecurityFilterChain에 등록되지 않았기 때문에 등록하고 싶으면 addFilterBefore나 addFilterAfter를 사용해야 한다.
  • SpringSecurityFilter보다 이전, 이후에 실행
    • addFilterBefore(Filter filter, Class<? extends Filter> beforeFilter)
    • addFilterAfter(Filter filter, Class<? extends Filter> afterFilter)

 

2. FilterConfig 생성

@Configuration
public class FilterConfig {

    @Bean
    public FilterRegistrationBean<MyFilter2> filter2() {
        FilterRegistrationBean<MyFilter2> bean = new FilterRegistrationBean<>(new MyFilter2());
        bean.addUrlPatterns("/*");  // 모든 요청에 대해서 필터 적용
        bean.setOrder(0);   // 낮은 숫자일수록 우선순위가 높음

        return bean;
    }
}
  • SecurityConfg에서 추가하지 않고, 따로 FilterConfig를 생성하여 등록하는 방법
  • FilterRegistrationBean을 생성하여 MyFilter1을 빈으로 등록시켜준다.

 

필터 실행

  • MyFilter1: SecurityConfg에 등록한 필터
    • http.addFilterBefore(new MyFilter1(), UsernamePasswordAuthenticationFilter.class);
  • MyFilter2: FilterRegistrationBean에 등록한 필터
    • public FilterRegistrationBean<MyFilter2> filter2() { }
  • FilterRegistrationBean에 등록한 필터보다 SpringSecurityFilter에 등록한 필터가 가장 최우선 순위에 있다.
    • 결국 SpringSecurityFilter가 직접 만든 MyFilter2보다 먼저 실행된다.

 

 

참고