개발자의 오르막

[SpringSecurity #04] SecurityContextHolder 와 Authentication 본문

SpringFrameWork/SpringSecurity

[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 의 데이터를 줄일 수 있을까?

 

Comments