Введение в принцип Spring Security, анализ исходного кода - процесс аутентификации

Spring Boot

основной принцип

В архитектуре, где внешний и внутренний интерфейсы разделены, авторизационная аутентификация в основном включает два основных процесса:

  1. Имя пользователя и пароль обмениваются на токен (Token), который является неизменным для обеспечения безопасности разрешений.
  2. Через некоторое время пользователям не нужно вводить имя пользователя и пароль, и через токен они могут получить доступ к интерфейсу, ограниченному управлением разрешениями.

Идти дальше,

  1. Процесс 1 заключается в получении информации о пользователе, разрешениях и т. д. из базы данных с помощью имени пользователя и пароля и преобразовании их в идентифицируемую идентификационную информацию (т. е. аутентификацию) в структуре безопасности (здесь — Spring Security), которая рассматривается как успешный вход в систему, а затем преобразовать необходимую информацию в учетные данные, которые будут доступны в течение определенного периода времени - Токен.
  2. Процесс 2 заключается в анализе токена и преобразовании его в идентифицируемую идентификационную информацию в структуре безопасности.Посредством идентифицируемой идентификационной информации структура затем определяет, может ли разрешение получить доступ к конечной точке.

Можно видеть, что в процессах 1 и 2 первая половина одинакова, все они предназначены для преобразования учетных данных (первое — это имя пользователя и пароль, второе — токен) в идентификационную информацию, идентифицируемую платформой. На этом этапе мы рассматриваем его как认证Процесс. Вторая половина — это логика работы для каждой успешной аутентификации. Генерировать токены в процессе 1 относительно просто, а вторая половина процесса 2 — это логика принятия решений по управлению разрешениями в фреймворке безопасности (Spring Security), то есть логика определения возможности доступа.Рассматриваем этот шаг так как授权Процесс.

Следующее будет认证а также授权В двух частях.

Сертификация

Цепочка фильтров SecurityFilterChain

Spring Security использует метод проектирования filterChain.Большинство основных функций реализуются фильтрами.При запуске проекта вы можете увидеть существующие фильтры в журнале, которые можно найти в журнале, подобном следующемуDefaultSecurityFilterChain, который является SecurityFilterChain

2019-03-14 16:43:02.369  INFO 27251 --- [  restartedMain] o.s.s.web.DefaultSecurityFilterChain     : Creating filter chain: org.springframework.security.web.util.matcher.AnyRequestMatcher@1, [org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@1d88a93d, org.springframework.security.web.context.SecurityContextPersistenceFilter@184d52d7, org.springframework.security.web.header.HeaderWriterFilter@29d86b1e, org.springframework.security.web.authentication.logout.LogoutFilter@2ce28138, org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@320a4f73, com.yang.security.config.JwtAuthorizationTokenFilter@37e7a410, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@534e475b, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@39137df7, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@7c42403f, org.springframework.security.web.session.SessionManagementFilter@1fa2ad2b, org.springframework.security.web.access.ExceptionTranslationFilter@65869e97, org.springframework.security.web.access.intercept.FilterSecurityInterceptor@163d3c44]

Извлекая каждый фильтр, мы видим, что это порядок цепочки фильтров.

1. WebAsyncManagerIntegrationFilter
2. SecurityContextPersistenceFilter
3. HeaderWriterFilter
4. LogoutFilter
5. **UsernamePasswordAuthenticationFilter**
6. **JwtAuthorizationTokenFilter**
7. RequestCacheAwareFilter
8. SecurityContextHolderAwareRequestFilter
9. SessionManagementFilter
10. ExceptionTranslationFilter
11. FilterSecurityInterceptor

Здесь мы в основном говорим оUsernamePasswordAuthenticationFilterИ сопутствующий код, кстати, реализуем самиJwtAuthenticationFilterи прилегающие районы.

Пример: официальный фильтр — процесс UsernamePasswordAuthenticationFilter

UsernamePasswordAuthenticationFilter

UsernamePasswordAuthenticationFilter, как следует из названия, представляет собой фильтр, используемый для обработки входов в систему с использованием имени пользователя и пароля. Все основные методы фильтраdoFilter, doFilter фильтра находится в его родительском абстрактном классе, фильтру нужно только реализоватьattemptAuthenticationметод.

Исходный код выглядит следующим образом (не полный исходный код, выберите важные части для объяснения логики):

public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
  
  public Authentication attemptAuthentication(HttpServletRequest request,
			HttpServletResponse response) throws AuthenticationException {

	String username = obtainUsername(request);
	String password = obtainPassword(request);
	
	// 根据用户名密码构造AuthenticationToken
	UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
				username, password); 
	
    // 将AuthenticationToken放入AuthenticationProvider进行认证
	return this.getAuthenticationManager().authenticate(authRequest); 
  }
}

AuthenticationManager

AuthenticationManagerподдерживаетList<AuthenticationProvider>; сначала поAuthenticationProviderизsupportsспособ проверить, поддерживается ли типAuthenticationToken; если поддерживается, используйтеauthenticateсертификация, сертификация будетAuthenticationTokenПревратить в сертифицированногоAuthentication.

public class ProviderManager implements AuthenticationManager, MessageSourceAware,
		InitializingBean {

	private List<AuthenticationProvider> providers = Collections.emptyList();
	private AuthenticationManager parent;
	private boolean eraseCredentialsAfterAuthentication = true;

    // 遍历Providers
	public Authentication authenticate(Authentication authentication)
			throws AuthenticationException {
		
        
		for (AuthenticationProvider provider : getProviders()) {
		    // 如果Authentication不符合,跳过后边步骤,继续循环
			if (!provider.supports(toTest)) {
				continue;
			}

            // 如果Authentication符合,则使用该Provider进行authenticate操作
			result = provider.authenticate(authentication);
            
			if (result != null) {
                copyDetails(authentication, result);
            	break;
			}
		}

		if (result != null) {
			if (eraseCredentialsAfterAuthentication
					&& (result instanceof CredentialsContainer)) {
				((CredentialsContainer) result).eraseCredentials();
			}
			return result;
		}
	}
	
}

DaoAuthenticationProvider

Далее поговорим о том, какAuthenticationTokenСертификация. НижеDaoAuthenticationProviderРодительский абстрактный класс, основной метод в родительском классеauthenticateметод, в то время как подклассы используют только реализациюretrieveUserметод, который вызываетUserDetailsServiceизloadUserByUsername. Для нас, пользователей, все, что нам нужно сделать, это реализоватьUserDetailsService, переопределить метод в нем,loadUserByUsernameПолучить логин и пароль из базы, а что касается последующей проверки, то по факту это делаетсяAbstractUserDetailsAuthenticationProviderМы уже сделали.

public abstract class AbstractUserDetailsAuthenticationProvider implements
		AuthenticationProvider, InitializingBean, MessageSourceAware {

	public Authentication authenticate(Authentication authentication)
			throws AuthenticationException {
	  
		String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED": authentication.getName();
        
		// DaoAuthenticationProvider中重载retrieveUser方法,而该方法中的核心方法就是UserDetailsService的loadUserByUsername
		user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
		
		// preCheck
		preAuthenticationChecks.check(user);
		
		additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
		
		// postCheck
		postAuthenticationChecks.check(user);
        
		// 检查成功没有问题,则创建Authentication示例
		return createSuccessAuthentication(principalToReturn, authentication, user);
	}
	
	public boolean supports(Class<?> authentication) {
		return (UsernamePasswordAuthenticationToken.class
				.isAssignableFrom(authentication));
	}
}

В заключение

  1. UsernamePasswordAuthenticationFilter.doFilterПолучить логин и пароль, сгенерироватьUsernamePasswordAuthenticationToken;
  2. будетUsernamePasswordAuthenticationTokenсдаватьDaoAuthenticationProviderпроверять;
  3. DaoAuthenticationProviderпройти черезUserDetailsService.loadUserByUsernameПолучите имя пользователя, пароль, полномочия и другую информацию из базы данных и сравните их; если сравнение прошло успешно, сгенерированныйAuthentication;
  4. UsernamePasswordAuthenticationFilterбудетAuthenticationположить вSecurityContextHolder, аутентификация прошла успешно;

Реальный!

Практика: напишите свой собственный фильтр — JwtAuthenticationFilter

Основная функция процесса 2 – разобрать токен, преобразовать его в аутентификацию, которую можно идентифицировать в Spring Security, и поместить в контекст. Этот шаг выполняется с помощью JwtAuthenticationFilter. Его принцип такой же, как у UsernamePasswordAuthenticationFilter. Давайте вкратце смотрите, как небольшое практическое занятие.

JwtAuthorizationTokenFilter

напиши первымJwtAuthorizationTokenFilter. Мы напрямую расширяемAbstractAuthenticationProcessingFilterЭтот абстрактный класс, потому что я хочу использовать егоrequiresAuthenticationМетод определяет, нужно ли конечной точке доступа проходить через фильтр, в то же время нам нужно реализоватьRequestMatchСоответствующая информация о доступе, конкретная реализация не указана ниже, вы можете обратиться к кодуSkipUrlMatcherРеализуйте собственную бизнес-логику.

Далее мы получимaccess_tokenразбирать, конвертироватьUserDetails, User в Step1 в коде — это его конкретная реализация. Мы знаем, что jwt на самом деле зашифрован, и только с помощью нашего собственного разбора ключа мы можем успешно проверить и получить внутреннюю информацию. Фактически, на этом шаге мы проверили подлинность и доступность информации (шаг 1) и напрямую сгенерировалиJwtAuthenticationToken(Шаг 2), аутентификация здесь была проверена и введенаAuthenticationManager.authenticateПроцесс получает аутентификационную информацию, распознаваемую фреймворком.Authentication. Поместите аутентифицированную идентификационную информацию в контекст, и процесс аутентификации будет завершен.

public class JwtAuthorizationTokenFilter extends AbstractAuthenticationProcessingFilter {

  public JwtAuthorizationTokenFilter(RequestMatcher matcher) {
    super(matcher);
  }

  @Override
  public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException {

    HttpServletRequest httpServletRequest = (HttpServletRequest) request;
    HttpServletResponse httpServletResponse = (HttpServletResponse) response;
    
    // Step0. 首先判断访问的端点是否需要经过该过滤器
    if (!requiresAuthentication(httpServletRequest, httpServletResponse)) {
      filterChain.doFilter(httpServletRequest, httpServletResponse);
      return;
    }
    String token = httpServletRequest.getHeader("Authorization");

    // Step1. 将token转换成UserDetails(这里的User是自己写的UserDetail的实现)
    User user = JwtUtil.accessToken2User(token.substring(7));

    // Step2. 将UserDetails转换成Authentication,这里的JwtAuthenticationToken即为Authentication的实现,
    // 一般而言,将UserDetails放入Authentication的principle中,之后如果需要可通过Authentication.getPrinciple的方法把UserDetails取出来
    JwtAuthenticationToken authenticationToken = new JwtAuthenticationToken(user, token, user.getAuthorities());

    // Step3. 这一步将AuthenticationToken交由AuthenticationProvider处理,转换成Authentication
    final Authentication authentication = getAuthenticationManager().authenticate(authenticationToken);

    // Step4. 将得到的Authentication实例放入Holder,则认证完成
    SecurityContextHolder.getContext().setAuthentication(authentication);

    // Step5. 进入之后的过滤器处理
    filterChain.doFilter(httpServletRequest, httpServletResponse);
  }
}

JwtAuthenticationProvider

давайте посмотрим на обычайJwtAuthenticationProvider. Из предыдущего раздела мы знаем, чтоAuthenticationManager.authenticateПроцесс на самом деле через бетонAuthenticationProviderГотово, у нас есть фронтJwtAuthenticationToken, мы специально реализуем процесс, который обрабатывает этот экземплярAuthenticationProvider. В этом методе реализацииauthenticateПроцесс, который я передам напрямуюauthentication(примерjwtAuthenticationToken) возвращается напрямую, потому что процесс синтаксического анализа JWT должен расшифровать и проверить JWT, поэтому входящийJwtAuthenticationTokenЭто уже было проверено, поэтому я не делал здесь слишком много обработки.

public class JwtAuthenticationProvider implements AuthenticationProvider {

  @Override
  public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    return authentication;
  }

  @Override
  public boolean supports(Class<?> authentication) {
    return (JwtAuthenticationToken.class.isAssignableFrom(authentication));
  }
}

подведем итоги того, что мы сделали

  1. реализовать пользовательскийAuthorizationTokenFilter,выполнитьdoFilterметод, который представляет собой весь процесс аутентификации.
  2. Получите информацию о запросе (информация, полученная в этом разделе, — это JWT, а в предыдущем разделе — имя пользователя и пароль) и сгенерируйте запрос.AuthenticationToken(JwtAuthenticationToken создан в этом разделе, UsernamePasswordToken в предыдущем разделе)
  3. будетAuthenticationTokenсдаватьAuthenticationProviderпроверить, вsupportsметод, чтобы проверить, поддерживается ли типAuthenticationToken,существуетauthenticateспособ завершения процесса проверки.
  4. будет сертифицированAuthenticationЭкземпляр помещается в контекст безопасностиSecurityContextHolder, процесс аутентификации завершен.

Код, используемый в статье, был загружен на gitee для справки:git ee.com/bauhinia/…

Исходный код Spring Security огромен и сложен, а мой уровень ограничен, в статье неизбежно будут ошибки, упущения и непонятные выражения, пожалуйста, заплатите за это. Добро пожаловать на общение и надеемся добиться прогресса вместе со всеми.