개발자의 오르막
[SpringSecurity #04] SecurityContextHolder 와 Authentication 본문
[SpringSecurity #04] SecurityContextHolder 와 Authentication
계단 2020. 9. 21. 11:10# SecurityContextHolder 와 Authentication
- SecurityContextHolder
: SecurityContext 제공, 기본적으로 ThreadLocal 을 사용한다.
ThreadLocal : 한 쓰레드 내에서 공유하는 저장소
(ThreadLocal 안에 있기 때문에 파라미터를 사용하지 않아도 데이터를 접근할 수 있음)
- SecurityContext
: Authentication 제공
- Authentication
: Principal 과 GrantAuthority 제공
- Principal
: "누구"에 해당하는 정보.
UserDetailsService 에서 리턴한 객체
객체는 UserDetails 타입
- GrantAuthority
: "ROLE_USER", "ROLE_ADMIN" 등 Principal 이 가지고 있는 권한을 나타낸다.
인증 이후, 인가 및 권한 확인할 때 이 정보를 참조한다.
- Debug 용 코드
public void dashboard() {
// SecurityContextHolder 를 통해서 로그인 한 객체를 어디서든지 사용할 수 있음.
// UsernamePasswordAuthenticationToken 타입
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
// principal 은 로그인한 객체로 User 타입 ( UserDetails 타입 안의 User )
Object principal = authentication.getPrincipal();
// 로그인한 principal 의 권한 ( Roles )
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
// 인증을 한 후에는 가지고 있지 않음
Object credentials = authentication.getCredentials();
// 인증된 사용자인지 여부
boolean authenticated = authentication.isAuthenticated();
}
# AuthenticationManager 와 Authentication
: 스프링 시큐리티에서 인증(Authentication) 은 AuthenticationManager 가 한다.
Authentication authenticate(Authentication authentication) throws AuthenticationException;
인자로 받은 Authentication 이 유효한 인증인지 확인하고 Authentication 객체를 리턴한다.
인증을 확인하는 과정에서 비활성 계정, 잘못된 비번, 잠긴 계정 등의 에러를 던질 수 있다.
- 인자로 받은 Authentication
사용자가 입력한 인증에 필요한 정보(username, password)로 만든 객체. (폼 인증의 경우)
Authentication
-> Principal : "jake"
-> Credentials : "123"
- 유효한 인증인지 확인
사용자가 입력한 password가 UserDetailService 를 통해 읽어온 UserDetails 객체에 들어있는
password와 일치하는지 확인
해당 사용자 계정이 잠겨 있진 않은지, 비활성 계정은 아닌지 등 확인
- Authentication 객체를 리턴
Authentication
principal : UserDetailsService 에서 리턴한 그 객체 (User)
Credentials : "123"
- ProviderManager 를 사용함 (시큐리티에서 제공하는 클래스)
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
AuthenticationException parentException = null;
Authentication result = null;
Authentication parentResult = null;
boolean debug = logger.isDebugEnabled();
for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
continue;
}
if (debug) {
logger.debug("Authentication attempt using "
+ provider.getClass().getName());
}
try {
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
catch (AccountStatusException | InternalAuthenticationServiceException e) {
prepareException(e, authentication);
// SEC-546: Avoid polling additional providers if auth failure is due to
// invalid account status
throw e;
} catch (AuthenticationException e) {
lastException = e;
}
}
if (result == null && parent != null) {
// Allow the parent to try.
try {
result = parentResult = parent.authenticate(authentication);
}
catch (ProviderNotFoundException e) {
// ignore as we will throw below if no other exception occurred prior to
// calling parent and the parent
// may throw ProviderNotFound even though a provider in the child already
// handled the request
}
catch (AuthenticationException e) {
lastException = parentException = e;
}
}
if (result != null) {
if (eraseCredentialsAfterAuthentication
&& (result instanceof CredentialsContainer)) {
// Authentication is complete. Remove credentials and other secret data
// from authentication
((CredentialsContainer) result).eraseCredentials();
}
// If the parent AuthenticationManager was attempted and successful then it will publish an AuthenticationSuccessEvent
// This check prevents a duplicate AuthenticationSuccessEvent if the parent AuthenticationManager already published it
if (parentResult == null) {
eventPublisher.publishAuthenticationSuccess(result);
}
return result;
}
// Parent was null, or didn't authenticate (or throw an exception).
if (lastException == null) {
lastException = new ProviderNotFoundException(messages.getMessage(
"ProviderManager.providerNotFound",
new Object[] { toTest.getName() },
"No AuthenticationProvider found for {0}"));
}
// If the parent AuthenticationManager was attempted and failed then it will publish an AbstractAuthenticationFailureEvent
// This check prevents a duplicate AbstractAuthenticationFailureEvent if the parent AuthenticationManager already published it
if (parentException == null) {
prepareException(lastException, authentication);
}
throw lastException;
}
- 위의 authentication 에 principal , credentials 로 username 과 password 가 들어옴
- 처음 인증 때는 우리가 들고온 username, password 로 인증을 못하고, 부모인 parent 를 통해
인증을 한다. 이 때 DAO Authentication 을 통해 DB 의 데이터를 활용해 인증하고 결과를 반환한다.
- AbstractUserDetailsAuthenticationProvider -> DaoAuthenticationProvider 로 디버깅을 통해
부모 클래스를 따라가다보면 UserDetails 를 반환하는 부분을 확인 할 수 있다.
- 결국 마지막에 principal 에는 User인 객체 타입으로 반환하여 Application 에서 사용할 수 있게 한다.
# ThreadLocal
- Java.lang 패키지에서 제공하는 쓰레드 범위 변수, 즉 쓰레드 수준의 데이터 저장소
같은 쓰레드 내에서만 공유 ( 같은 쓰레드라면 해당 데이터를 메소드 매개변수로 넘겨줄 필요 없음)
- SecurityContextHolder 의 기본 전략
public class AccountContext {
private static final ThreadLocal<Account> ACCOUNT_THREAD_LOCAL = new ThreadLocal<>();
public static void setAccount(Account account) {
ACCOUNT_THREAD_LOCAL.set(account);
}
public static Account getAccount() {
return ACCOUNT_THREAD_LOCAL.get();
}
}
- 임의의 컨트롤러에서 위의 setAccount 를 통해 정보 주입
@GetMapping("/dashboard")
public String dashboard(Model model, Principal principal) {
model.addAttribute("message", "Hello" + principal.getName());
AccountContext.setAccount(accountRepository.findAccountByUsername(principal.getName()));
sampleService.dashboard();
return "dashboard";
}
- 임의의 서비스에서 getAccount() 를 통해 데이터 불러오기
public void threadLocal() {
Account account = AccountContext.getAccount();
System.out.println("============");
System.out.println(account.getUsername());
}
* postHandle 의 데이터를 줄일 수 있을까?
'SpringFrameWork > SpringSecurity' 카테고리의 다른 글
[SpringSecurity #06] 스프링 시큐리티 Filter와 FilterChainProxy (0) | 2020.09.27 |
---|---|
[SpringSecurity #05] Authentication 와 SecurityContextHolder (0) | 2020.09.27 |
[SpringSecurity #03] 스프링 시큐리티 테스트 코드 작성 (0) | 2020.09.20 |
[SpringSecurity #02] 스프링 시큐리티 JPA 연동 (0) | 2020.09.20 |
[SpringSecurity #01] 스프링 시큐리티 개발환경 구성 및 사용방법 (0) | 2020.09.20 |