본문 바로가기

Spring/Spring Security

Spring Security Architecture

반응형

SecurityContextHolder와 Authentication

https://docs.spring.io/spring-security/site/docs/5.1.5.RELEASE/reference/htmlsingle/#core-components 

SecurityContextHolder

  • SecurityContext 제공, 기본적으로 ThreadLocal을 사용한다.
    • ThreadLocal 이란, 하나의 스레드 내에 국한하여 공유할 수 있는 기능 (자세한 설명은 아래에)
  • SecurityContextHolder만 알고 있으면, 인증 정보를 가져올 수 있다.

SecurityContext

  • Authentication 제공.

 

Authentication

  • PrincipalGrantAuthority 정보를 제공.
  • Principal
    • 인증된 사용자 정보 즉, “누구"에 해당하는 정보. 
    • UserDetailsService에서 리턴한 그 객체.
    • 객체는 UserDetails 타입.
      • UserDetails
        • 애플리케이션이 가지고 있는 유저 정보와 스프링 시큐리티가 사용하는 Authentication 객체 사이의 어댑터.
  • GrantAuthority
    • “ROLE_USER”, “ROLE_ADMIN”등 Principal이 가지고 있는 “권한”을 나타낸다.
    • 인증 이후, 인가 및 권한 확인할 때 이 정보를 참조한다. 

        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        UserDetails userDetails = (UserDetails) authentication.getPrincipal();
        Collection<? extends GrantedAuthority> grantedAuthorities = authentication.getAuthorities();

이때, principal은 UserDetailsService의 구현체인 AccountService에서 리턴한 UserAccount 타입이다.


@Service
public class AccountService implements UserDetailsService {

	// ....
    
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        Account account = accountRepository.findByUsername(username);
        if (account == null) {
            throw new UsernameNotFoundException(username);
        }
        return new UserAccount(account);
    }

}

UserDetailsService

  • 유저 정보를 UserDetails 타입으로 가져오는 DAO (Data Access Object) 인터페이스.
  • 스프링 데이터 JPA 등을 이용할 수 있음 )
  • UserDetailServiceAutoConfiguration
    • spring-boot-starter-security 이용
    • Spring Boot Auto Configuration 기능에서 자동으로 설정해주는 기능으로 유저 정보를 생성해준다.
@Configuration
@ConditionalOnClass(AuthenticationManager.class)
@ConditionalOnBean(ObjectPostProcessor.class)
@ConditionalOnMissingBean({ AuthenticationManager.class, AuthenticationProvider.class, UserDetailsService.class })
public class UserDetailsServiceAutoConfiguration {

	private static final String NOOP_PASSWORD_PREFIX = "{noop}";

	private static final Pattern PASSWORD_ALGORITHM_PATTERN = Pattern.compile("^\\{.+}.*$");

	private static final Log logger = LogFactory.getLog(UserDetailsServiceAutoConfiguration.class);

	@Bean
	@ConditionalOnMissingBean(
			type = "org.springframework.security.oauth2.client.registration.ClientRegistrationRepository")
	@Lazy
	public InMemoryUserDetailsManager inMemoryUserDetailsManager(SecurityProperties properties,
			ObjectProvider passwordEncoder) {
		SecurityProperties.User user = properties.getUser();
		List roles = user.getRoles();
		return new InMemoryUserDetailsManager(
				User.withUsername(user.getName()).password(getOrDeducePassword(user, passwordEncoder.getIfAvailable()))
						.roles(StringUtils.toStringArray(roles)).build());
	}

	private String getOrDeducePassword(SecurityProperties.User user, PasswordEncoder encoder) {
		String password = user.getPassword();
		if (user.isPasswordGenerated()) {
			logger.info(String.format("%n%nUsing generated security password: %s%n", user.getPassword()));
		}
		if (encoder != null || PASSWORD_ALGORITHM_PATTERN.matcher(password).matches()) {
			return password;
		}
		return NOOP_PASSWORD_PREFIX + password;
	}

}
  • @ConditionalOnMissingClassBean 애노테이션이 붙은 것을 확인할 수 있다.
    • SecurityProperties 에 정의되어있는 기본 User 정보를 바탕으로 UserDetail을 생성한다.
    • AuthenticationManager.class, AuthenticationProvider.class, UserDetailsService.class 클래스의 빈이 없을 경우, 빈으로 등록되도록 되어있다.
  • 그러나, 콘솔에 유저 정보가 출력되기 때문에 좋은 방법은 아니다.
  • 대신, 프로퍼티 파일에 유저 정보를 변경하여, 콘솔에 출력되지 않도록 할 수도 있다.
    1. spring.security.user.name
    2. spring.security.user.password
    3. spring.security.user.role
  • 그러나, 이 역시 단 하나의 유저정보만 생성 가능하다.

Spring Boot에서 제공해주는 자동설정 기능은 WebSecurityConfigurerAdapter 빈이 없을 경우 활성화된다.

@Configuration
@ConditionalOnClass({WebSecurityConfigurerAdapter.class})
@ConditionalOnMissingBean({WebSecurityConfigurerAdapter.class})
@ConditionalOnWebApplication(
    type = Type.SERVLET
)
public class SpringBootWebSecurityConfiguration {
    public SpringBootWebSecurityConfiguration() {
    }

    @Configuration
    @Order(2147483642)
    static class DefaultConfigurerAdapter extends WebSecurityConfigurerAdapter {
        DefaultConfigurerAdapter() {
        }
    }
}

대신, Spring Security Config 파일 생성으로 시큐리티 설정을 커스터마이징할 수 있다.

WebSecurityConfigurerAdapter를 상속받아, configure 메소드를 재정의해주면 된다.

// Add this annotation to an @Configuration class to have the Spring Security configuration defined in any WebSecurityConfigurer or more likely by extending the WebSecurityConfigurerAdapter base class and overriding individual methods:
   @Configuration
   @EnableWebSecurity
   public class MyWebSecurityConfiguration extends WebSecurityConfigurerAdapter {
  
   	@Override
   	public void configure(WebSecurity web) throws Exception {
   		web.ignoring()
   		// Spring Security should completely ignore URLs starting with /resources/
   				.antMatchers("/resources/**");
   	}
  
   	@Override
   	protected void configure(HttpSecurity http) throws Exception {
   		http.authorizeRequests().antMatchers("/public/**").permitAll().anyRequest()
   				.hasRole("USER").and()
   				// Possibly more configuration ...
   				.formLogin() // enable form based log in
   				// set permitAll for all URLs associated with Form Login
   				.permitAll();
   	}
  
   	@Override
   	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
   		auth
   		// enable in memory based authentication with a user named "user" and "admin"
   		.inMemoryAuthentication().withUser("user").password("password").roles("USER")
   				.and().withUser("admin").password("password").roles("USER", "ADMIN");
   	}
  
   	// Possibly more overridden methods ...
   }
  • 하지만, 유저 정보를 코드를 통해서 생성을 해줘야한다는 문제가 있고, 생성된 유저 정보가 소스에 저장되어 있기 때문에 보안상 좋지 않다.
  • 따라서, 데이터베이스에 유저 정보를 저장하고, 그 정보를 이용하여 인증은하는 방식으로 개선되어야 한다.

UserDetaisService의 구현체 클래스인 AccountService 를 구현하고,

@Service
public class AccountService implements UserDetailsService {

    AccountRepository accountRepository;

    PasswordEncoder passwordEncoder;

    public AccountService(final AccountRepository accountRepository, final PasswordEncoder passwordEncoder) {
        this.accountRepository = accountRepository;
        this.passwordEncoder = passwordEncoder;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        Account account = accountRepository.findByUsername(username);
        if (account == null) {
            throw new UsernameNotFoundException(username);
        }
        return new UserAccount(account);
   }

SecurityConfig extends WebSecurityConfigurerAdapter 클래스에 다음과 같이 명시적으로 어떠한 클래스를 사용할 것인지 설정해줄 수 있다.

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Autowired
    AccountService accountService;
    
    @Override
    protected void configure(final AuthenticationManagerBuilder auth) throws Exception {

        auth.userDetailsService(accountService);
    }
}

그러나, AccountService를 빈으로 등록만 해주면, 명시적으로 지정해주지 않아도 해당 빈을 사용하여, 인증을 하도록 되어있다.

AuthenticationManager와 Authentication

스프링 시큐리티에서 인증(Authentication)은 AuthenticationManager가 한다.

Authentication authenticate(Authentication authentication) throws AuthenticationException;

  • 인자로 받은 Authentication이 유효한 인증인지 확인하고 Authentication 객체를 리턴한다.
  • 인증을 확인하는 과정에서 비활성 계정, 잘못된 비번, 잠긴 계정 등의 에러를 던질 수 있다.

인자로 받은 Authentication

  • 사용자가 입력한 인증에 필요한 정보(username, password)로 만든 객체. (폼 인증인 경우)
  • Authentication
    - Principal: “juns”/ "password"
    - Credentials: USER_ROLE

유효한 인증인지 확인

  • 사용자가 입력한 password가 UserDetailsService를 통해 읽어온 UserDetails 객체에 들어있는 password와 일치하는지 확인
  • 해당 사용자 계정이 잠겨 있진 않은지, 비활성 계정은 아닌지 등 확인

Authentication 객체를 리턴

  • Authentication
    - Principal: UserDetailsService에서 리턴한 그 객체 (User)
    - Credentials: GrantedAuthorities

ThreadLocal

Java.lang 패키지에서 제공하는 쓰레드 범위 변수. 즉, 쓰레드 수준의 데이터 저장소.

  • 같은 쓰레드 내에서만 공유.
  • 따라서 같은 쓰레드라면 해당 데이터를 메소드 매개변수로 넘겨줄 필요 없음.
  • SecurityContextHolder의 기본 전략.
public class AccountContext {

	// thread local account 변수 선언
	private static final ThreadLocal<Account> ACCOUNT_THREAD_LOCAL = new ThreadLocal<>();

	public static void setAccount( final Account account ) {
		ACCOUNT_THREAD_LOCAL.set( account );
	}

	public static Account getAccount() {
		return ACCOUNT_THREAD_LOCAL.get();
	}
}

@Service
public class SampleService {

	public void dashboard() {

		// thread local 객체에서 가져온 Account 객체
		Account account = AccountContext.getAccount();
		System.out.println( "======================" );
		System.out.println( account.getUsername() );
	}
}



Authencation과 SecurityContextHodler

AuthenticationManager가 인증을 마친 뒤 리턴 받은 Authentication 객체의 행방은?

 

UsernamePasswordAuthenticationFilter

  • 폼 인증을 처리하는 시큐리티 필터
  • 인증된 Authentication 객체를 SecurityContextHolder에 넣어주는 필터
  • SecurityContextHolder.getContext().setAuthentication(authentication)
return this.getAuthenticationManager().authenticate(authRequest);

 

SecurityContextPersisenceFilter

  • SecurityContext를 HTTP session에 캐시(기본 전략)하여 여러 요청에서 Authentication을 공유할 수 있 공유하는 필터.
  • SecurityContextRepository를 교체하여 세션을 HTTP session이 아닌 다른 곳에 저장하는 것도 가능하다

스프링 시큐리티 Filter와 FilterChainProxy

스프링 시큐리티가 제공하는 필터들

  1. WebAsyncManagerIntergrationFilter
  2. SecurityContextPersistenceFilter
  3. HeaderWriterFilter
  4. CsrfFilter
  5. LogoutFilter
  6. UsernamePasswordAuthenticationFilter
  7. DefaultLoginPageGeneratingFilter
  8. DefaultLogoutPageGeneratingFilter
  9. BasicAuthenticationFilter
  10. RequestCacheAwareFtiler
  11. SecurityContextHolderAwareReqeustFilter
  12. AnonymouseAuthenticationFilter
  13. SessionManagementFilter
  14. ExeptionTranslationFilter
  15. FilterSecurityInterceptor

이 모든 필터는 FilterChainProxy가 호출한다.

어떠한 인증 방식을 사용하냐에 따라 filter 개수가 달라짐

@Configuration
@Order( Ordered.LOWEST_PRECEDENCE - 15 )
public class SecurityConfig extends WebSecurityConfigurerAdapter {

	@Override
	protected void configure( final HttpSecurity http ) throws Exception {
		http
			.antMatcher( "/account/**" ) // 패턴 매칭
			.authorizeRequests() // 요청을 어떻게 인가할 지에 대한 패턴 설정
			.anyRequest().permitAll();

	}
}
@Configuration
@Order( Ordered.LOWEST_PRECEDENCE - 100 )
public class AnotherSecurityConfig extends WebSecurityConfigurerAdapter {

	@Override
	protected void configure( final HttpSecurity http ) throws Exception {
		http.authorizeRequests() // 요청을 어떻게 인가할 지에 대한 패턴 설정
			.mvcMatchers( "/", "/info", "/account/**" ).permitAll() // spring security filter 내에서 match할 때 사용하는 정보
			.anyRequest().authenticated() // 그 외의 모든 request는 인증한 사용자들에 한해 허용
		;
		http.formLogin(); // 스프링 시큐리티 기본 제공 form login을 사용하겠다
		http.httpBasic();
	}
}

FilterChainProxy가 설정(WebSecueityConfigurerAdapter를 상속받은 클래스의 구현)에 따라 달라지는 필터들을 관리하고, 실행함.

 

 



DelegatingFilterProxy와 FilterChainProxy

서블릿 컨테이너(tomcat)는 서블릿 스펙을 지원한다.
https://tomcat.apache.org/tomcat-5.5-doc/servletapi/javax/servlet/Filter.html

DelegatingFilterProxy

  • 일반적인 서블릿 필터. (스프링이 제공해 주는 구현체
  • 서블릿 필터 처리를 스프링에 들어있는 빈으로 위임하고 싶을 때 사용하는 서블릿 필터.
  • 타겟 빈 이름을 설정한다.
  • 스프링 부트 없이 스프링 시큐리티 설정할 때는 AbstractSecurityWebApplicationInitializer를 사용해서 등록.
  • 스프링 부트를 사용할 때는 자동으로 등록 된다. (SecurityFilterAutoConfiguration)

FilterChainProxy

  • 보통 “springSecurityFilterChain” 이라는 이름의 빈으로 등록된다.

DelegatingFilterProxy는 스프링의 빈으로 등록되어 있는 FilterChainProxy에게 작업을 위임

빈의 이름을 알아야 함. ( DEFAULT_FILTER_NAME = "springSecurityFilterChain" )

 

 

 

반응형

'Spring > Spring Security' 카테고리의 다른 글

스프링 시큐리티 아키텍처  (0) 2020.01.06
ExceptionTranslationFilter  (0) 2020.01.06
AccessDecisionManager  (0) 2019.12.12
Spring Security OAuth2  (0) 2019.08.05
Authentication Handler  (0) 2019.08.02