Инфраструктура аутентификации инвентаря: процесс аутентификации SpringSecurity

Java
Инфраструктура аутентификации инвентаря: процесс аутентификации SpringSecurity

Общая документация:Каталог статей
Github : github.com/black-ant

Введение

В прошлой статье говорилось о том, как работает фильтр безопасности, а в этой статье рассмотрим процесс аутентификации в безопасности.

2. Распространение аутентификационной информации

2.1 Базовая информация об объекте SecurityContext

Основная информация о безопасностиSecurityContext, давайте посмотрим, как определяется и распространяется информация аутентификации

SecurityContextHolderгде Spring Security хранит детали аутентифицированного. Spring Security не заботится о том, как заполняется SecurityContextHolder. Если он содержит значение, используйте его в качестве текущего аутентифицированного пользователя.

SecurityContext.jpg

Создание контекста безопасности

// 表明用户已通过身份验证的最简单方法是直接设置 SecurityContextHolder
SecurityContext context = SecurityContextHolder.createEmptyContext(); 
// 生成 Authentication 认证对象
Authentication authentication =new TestingAuthenticationToken("username", "password", "ROLE_USER"); 
context.setAuthentication(authentication);
// SecurityContextHolder 中设置 context
SecurityContextHolder.setContext(context); 

Получить аутентифицированного пользователя

// Step 1 : 获取 SecurityContext
SecurityContext context = SecurityContextHolder.getContext();
// Step 2 : 获取 Authentication 及其相关信息
Authentication authentication = context.getAuthentication();
String username = authentication.getName();
Object principal = authentication.getPrincipal();
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();

Связанная логика SecurityContextHolder

image.png

  • По умолчанию SecurityContextHolder используетThreadLocalдля хранения этих данных (PS: также можно настроить через SecurityContextHolder.MODE global)
    • Во-первых, установить системные свойства
    • Второй — вызвать статический метод для SecurityContextHolder.
  • Spring Security FilterChainProxy гарантирует, что SecurityContext всегда очищается
// SecurityContextHolder 提供了以下的参数

// 提供了三种不同的Mode
public static final String MODE_THREADLOCAL = "MODE_THREADLOCAL";
public static final String MODE_INHERITABLETHREADLOCAL = "MODE_INHERITABLETHREADLOCAL";
public static final String MODE_GLOBAL = "MODE_GLOBAL";

public static final String SYSTEM_PROPERTY = "spring.security.strategy";
// 策略类型名
private static String strategyName = System.getProperty(SYSTEM_PROPERTY);
private static SecurityContextHolderStrategy strategy;
private static int initializeCount = 0;


// Node 1 : 类初始化 , 这里因为有静态初始化块 , 所以上面从才可以总结通过静态类来设置
static {
    initialize();
}

// 进行了初始化操作
private static void initialize() {
    if (!StringUtils.hasText(strategyName)) {
        strategyName = MODE_THREADLOCAL;
    }
    // 这里可以看到 , 有三种不同的Mode , 分别对应三种不同的策略
    if (strategyName.equals(MODE_THREADLOCAL)) {
        strategy = new ThreadLocalSecurityContextHolderStrategy();
    }else if (strategyName.equals(MODE_INHERITABLETHREADLOCAL)) {
        strategy = new InheritableThreadLocalSecurityContextHolderStrategy();
    }else if (strategyName.equals(MODE_GLOBAL)) {
        strategy = new GlobalSecurityContextHolderStrategy();
    }else {
        try {
            // 反色获取
            Class<?> clazz = Class.forName(strategyName);
            Constructor<?> customStrategy = clazz.getConstructor();
            strategy = (SecurityContextHolderStrategy) customStrategy.newInstance();
        } catch (Exception ex) {
            ReflectionUtils.handleReflectionException(ex);
        }
    }
    initializeCount++;
}

// Node 2 : setContext 逻辑 , 通过策略调用
public static void setContext(SecurityContext context) {
    strategy.setContext(context);
}


GlobalSecurityContextHolderStrategy :где SecurityContext — статическая переменнаяInheritableThreadLocalSecurityContextHolderStrategy :который содержитThreadLocal<SecurityContext> ThreadLocalSecurityContextHolderStrategy :ничем не отличается от предыдущего

2.2 Процесс SecurityContext

FilterChainProxyAbstractAuthenticationProcessingFilterDatabaseAuthenticationFilterAuthenticationManagerProviderвызов doFilterпопытка вызоваАутентификацияВызов провайдера через МенеджерПозвоните провайдеру, чтобы запросить аутентификациюЗатем аутентификацияВернуть аутентификациюВернуть аутентификациюЗдесь будет вызван соответствующий обработчик для окончательной обработки аутентификации.FilterChainProxyAbstractAuthenticationProcessingFilterDatabaseAuthenticationFilterAuthenticationManagerProvider

Шаг 1. Позвоните провайдеру, чтобы он обработал ситуацию, когда аутентификация возвращается после завершения аутентификации.

// 回忆一下 , 之前 Filter 中 , 调用 AuthenticationManager 开始了 Provider 的流程
DatabaseUserToken authRequest = new DatabaseUserToken(username, password);
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);

// 往外层追溯一下 , 可以看到 , 其核心被调用的是抽象类 AbstractAuthenticationProcessingFilter
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) 

Шаг 2 : Обработка провайдером

AuthenticationManager здесь в основном ProviderManager, в основном это, мы сохраняем только более важную логику:

public Authentication authenticate(Authentication authentication)
			throws AuthenticationException {
    Class<? extends Authentication> toTest = authentication.getClass();
    AuthenticationException lastException = null;
    Authentication result = null;
    Authentication parentResult = null;
    boolean debug = logger.isDebugEnabled();
    
    for (AuthenticationProvider provider : getProviders()) {
        // 每个 Provider 都会重写 supports , 此处判断是否支持该 Provider
        if (!provider.supports(toTest)) {
            continue;
        }

        try {
            // 此处调用具体的 Provider 执行
            result = provider.authenticate(authentication);
            if (result != null) {
                copyDetails(authentication, result);
                break;
            }
        }catch (AccountStatusException|InternalAuthenticationServiceException e) 
                prepareException(e, authentication);
                throw e;
        }catch (AuthenticationException e) {
                lastException = e;
        }
    }
    // 这里还有个补偿策略 ,如果当前 AuthenticationManager 处理不了 , 会由 父类处理
    // 暂时没想清楚具体的使用场景 , 可能适用于细粒度权限这种
    if (result == null && parent != null) {	
        try {
            result = parentResult = parent.authenticate(authentication);
        }catch (AuthenticationException e) {
            lastException = e;
        }
    }

    if (result != null) {
        if (eraseCredentialsAfterAuthentication
					&& (result instanceof CredentialsContainer)) {
            ((CredentialsContainer) result).eraseCredentials();
        }
        // 发布认证成功的时间
        if (parentResult == null) {
            eventPublisher.publishAuthenticationSuccess(result);
        }
        return result;
    }

    if (lastException == null) {
        lastException = new ProviderNotFoundException(messages.getMessage(
					"ProviderManager.providerNotFound",
					new Object[] { toTest.getName() },
					"No AuthenticationProvider found for {0}"));
    }
    prepareException(lastException, authentication);
    throw lastException;
}

Как видите, в этот момент провайдер вернул аутентификацию обратно.

Шаг 3. Обработка фильтра абстрактной аутентификации.

Со второго шага Prodiver возвращает Authentication , который, наконец, передается в AbstractAuthenticationProcessingFilter.

// AbstractAuthenticationProcessingFilter 伪代码 : 

// Step 1 : 进行预认证
authResult = attemptAuthentication(request, response);

// Step 2 : Session 策略处理 , 这里的 sessionStrategy 是 NullAuthenticatedSessionStrategy , 其里面是空实现 
sessionStrategy.onAuthentication(authResult, request, response) 

// Step 3 : 成功后执行容器处理
successfulAuthentication(request, response, chain, authResult)    
    |- SecurityContextHolder.getContext().setAuthentication(authResult) // 果然来了 , 把 authResult 放入 SecurityContext
    |- rememberMeServices.loginSuccess(request, response, authResult) // 记住我功能的处理 , RememberFilter 会对这个进行处理
    |- successHandler.onAuthenticationSuccess(request, response, authResult) // SavedRequestAwareAuthenticationSuccessHandler

До сих пор аутентификация, сгенерированная провайдером, была успешно помещена в контейнер.

Расширить SavedRequestAwareAuthenticationSuccessHandler для обработки результатов Success

Подводя итог, нужно настроить отношение кеша и прыжка >>>

C- SavedRequestAwareAuthenticationSuccessHandler
    P- RequestCache requestCache : Request 缓存工具 , 用户获取缓存的 Request 对象
    M- onAuthenticationSuccess
        - SavedRequest savedRequest = requestCache.getRequest(request, response) : 先获取缓存的对象
        - String targetUrlParameter = getTargetUrlParameter() : 这里是看看有没有成功的跳转地址 
        	?- 如果想实现不同用户不同跳转 ,定制这里
        - clearAuthenticationAttributes(request) : 删除与身份验证相关的临时数据,这些数据可能在身份验证过程中存储在会话中 , 避免敏感信息泄露
        - String targetUrl = savedRequest.getRedirectUrl();
        - getRedirectStrategy().sendRedirect(request, response, targetUrl);
			?- 重定向出去

Authentication001.jpg

Выше приведена блок-схема ведомого устройства аутентификации и сбоя аутентификации, вы можете увидеть конкретный класс обработки:

Обобщите, что делают успешная аутентификация и неудачная аутентификация соответственно:

Если аутентификация не удалась:

  • Контекстодержатель безопасности очищается.
  • Вызовите RememberMeServices.loginFail. Это не работает, если не настроена функция Rememberme.
  • Вызовите AuthenticationFailureHandler.

При этом сравните успешность аутентификации:

  • SessionAuthenticationStrategy будет уведомлен о новых входах в систему
  • Настройте аутентификацию в SecurityContextHolder, затем SecurityContextPersistenceFilter сохранит SecurityContext в HttpSession.
  • Вызовите RememberMeServices.loginSuccess. Если запомнить меня не настроено, это не работает
  • ApplicationEventPublisher публикует интерактивные подключения для проверки подлинности
  • Call AuthenticationSuccessHandler

3. Повторное посещение и выход

Выше упоминалось, что произошло во время процесса аутентификации, здесь мы смотрим на повторное посещение после завершения аутентификации >>>

3.1 Доступ после аутентификации

// 前面说了 , 认证完成后会写入 SecurityContextHolder , Security 通过判断 SecurityContext 来校验用户
// 同理 , 下次访问的时候同样通过该方式 :


// Step 1 : SecurityContextPersistenceFilter 拦截到请求

// Step 2 : 从请求中获取 SecurityContext
SecurityContext contextBeforeChainExecution = repo.loadContext(holder);

// 从 HttpSessionSecurityContextRepository 中获取
C- HttpSessionSecurityContextRepository
    SecurityContext context = readSecurityContextFromSession(httpSession);

// 此处断点可以看到认证信息 : 
org.springframework.security.core.context.SecurityContextImpl@45eed422: 
Authentication: com.security.demo.token.DatabaseUserToken@45eed422: 
Principal: gang; 
Credentials: [PROTECTED]; 
Authenticated: true; 
Details: org.springframework.security.web.authentication.WebAuthenticationDetails@fffdaa08: RemoteIpAddress: 127.0.0.1; 
SessionId: A58D946FBFCB17743E2E0A44DBAB7A76; 
Granted Authorities: ROLE_USER	

// Step 3 : finally 此处将 SecurityContext 进行了设置
SecurityContext contextAfterChainExecution = SecurityContextHolder.getContext();
SecurityContextHolder.clearContext();
repo.saveContext(contextAfterChainExecution, holder.getRequest(),holder.getResponse());



PS: поскольку он основан на управлении сеансом, срок его действия истечет через некоторое время.
Конечно, это модель на основе сеанса, и жизненный цикл эквивалентен сеансу, но обычно используется схема более длительного жизненного цикла, напримерAccessToken , Cookieи т. д., в то время как сеанс просто поддерживает временное состояние аутентификации

3.2 выход из системы

Выход Связанный класс:

  • PersistentTokenBasedRememberMeServices
  • TokenBasedRememberMeServices
  • CookieClearingLogoutHandler
  • CsrfLogoutHandler
  • SecurityContextLogoutHandler
  • HeaderWriterLogoutHandler

Точно так же Logout также имеет фильтр и обработчик.

  • LogoutFilter
  • SimpleUrlLogoutSuccessHandler
  • HttpStatusReturningLogoutSuccessHandler

Как и в предыдущем анализе Filter, его ядро ​​по-прежнему проходит через LogoutFilter:

this.handler.logout(request, response, auth);
logoutSuccessHandler.onLogoutSuccess(request, response, auth);

C- SecurityContextLogoutHandler : 核心类 , 处理 Context 
	public void logout(HttpServletRequest request, HttpServletResponse response,
			Authentication authentication) {
		Assert.notNull(request, "HttpServletRequest required");
		if (invalidateHttpSession) {
			HttpSession session = request.getSession(false);
			if (session != null) {
				logger.debug("Invalidating session: " + session.getId());
				session.invalidate();
			}
		}

		if (clearAuthentication) {
             // 此处将 SecurityContext 设置为了 null
			SecurityContext context = SecurityContextHolder.getContext();
			context.setAuthentication(null);
		}

		SecurityContextHolder.clearContext();
	}

Суммировать :

На данный момент прочитан полный жизненный цикл безопасности, на самом деле он очень простой, если подытожить, то это:

  • Фильтр Принимать бизнес-решения
  • AuthenticationManager определяет метод проверки
  • Провайдер выполняет проверку подлинности
  • Обработка результатов обработчика была изменена извне

В следующей главе давайте подробно рассмотрим логику конфигурации Security, чтобы увидеть, что происходит внизу.