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
- Principal과 GrantAuthority 정보를 제공.
- Principal
- 인증된 사용자 정보 즉, “누구"에 해당하는 정보.
- UserDetailsService에서 리턴한 그 객체.
- 객체는 UserDetails 타입.
- UserDetails
- 애플리케이션이 가지고 있는 유저 정보와 스프링 시큐리티가 사용하는 Authentication 객체 사이의 어댑터.
- UserDetails
- 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 클래스의 빈이 없을 경우, 빈으로 등록되도록 되어있다.
- @ConditionalOn* 에 대한 설명
wonwoo.ml/index.php/post/20
- @ConditionalOn* 에 대한 설명
- 그러나, 콘솔에 유저 정보가 출력되기 때문에 좋은 방법은 아니다.
- 대신, 프로퍼티 파일에 유저 정보를 변경하여, 콘솔에 출력되지 않도록 할 수도 있다.
- spring.security.user.name
- spring.security.user.password
- 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 ...
}
- 하지만, 유저 정보를 코드를 통해서 생성을 해줘야한다는 문제가 있고, 생성된 유저 정보가 소스에 저장되어 있기 때문에 보안상 좋지 않다.
- 따라서, 데이터베이스에 유저 정보를 저장하고, 그 정보를 이용하여 인증은하는 방식으로 개선되어야 한다.
- UserDetails 를 리턴하는 loadUserByUserName 메소드를 오버라이드하여 구현한다.
- AccountRepository : 데이터 베이스의 User 정보
- PasswordEncoder : Spring Security 에서 제공하는 패스워드 인코더
juns-lee.tistory.com/entry/UserDetailService-PasswordEncoderFactories
- UserDetails 를 리턴하는 loadUserByUserName 메소드를 오버라이드하여 구현한다.
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
스프링 시큐리티가 제공하는 필터들
- WebAsyncManagerIntergrationFilter
- SecurityContextPersistenceFilter
- HeaderWriterFilter
- CsrfFilter
- LogoutFilter
- UsernamePasswordAuthenticationFilter
- DefaultLoginPageGeneratingFilter
- DefaultLogoutPageGeneratingFilter
- BasicAuthenticationFilter
- RequestCacheAwareFtiler
- SecurityContextHolderAwareReqeustFilter
- AnonymouseAuthenticationFilter
- SessionManagementFilter
- ExeptionTranslationFilter
- 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 |