Spring Анализ безопасности (1) — Процесс авторизации
При изучении Spring Cloud, когда я столкнулся с контентом, связанным со службой авторизации oauth, я всегда мало знал об этом, поэтому я решил сначала изучить и разобраться с контентом, связанным с авторизацией и аутентификацией, принципами и дизайном Spring Security и Spring Security Oauth2. . Данная серия статей написана для закрепления впечатления и понимания в процессе обучения.Если есть какие-то нарушения, просьба сообщить.
Окружение проекта:
JDK1.8
Spring boot 2.x
Spring Security 5.x
1. Простая демонстрация безопасности
1. Пользовательская реализация UserDetailsService
Настройте класс MyUserDetailsUserService, реализуйте метод loadUserByUsername() интерфейса UserDetailsService и просто возвращайте объект User, предоставленный Spring Security. Чтобы облегчить демонстрацию управления разрешениями Spring Security позже, здесь используетсяAuthorityUtils.commaSeparatedStringToAuthorityList("admin")Для учетной записи пользователя настроена информация о правах администратора. В реальных проектах пользователей, их роли и разрешения можно получить, обратившись к базе данных здесь.
@Component
public class MyUserDetailsUserService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 不能直接使用 创建 BCryptPasswordEncoder 对象来加密, 这种加密方式 没有 {bcrypt} 前缀,
// 会导致在 matches 时导致获取不到加密的算法出现
// java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null" 问题
// 问题原因是 Spring Security5 使用 DelegatingPasswordEncoder(委托) 替代 NoOpPasswordEncoder,
// 并且 默认使用 BCryptPasswordEncoder 加密(注意 DelegatingPasswordEncoder 委托加密方法BCryptPasswordEncoder 加密前 添加了加密类型的前缀) https://blog.csdn.net/alinyua/article/details/80219500
return new User("user", PasswordEncoderFactories.createDelegatingPasswordEncoder().encode("123456"), AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
}
}
Обратите внимание, что Spring Security 5 не используется в началеNoOpPasswordEncoderв качестве кодировщика паролей по умолчанию, но по умолчанию используетDelegatingPasswordEncoderВ качестве своего кодировщика шифра его метод кодирования использует имя кодировщика шифра в качестве префикса + поручает различным кодировщикам шифра реализовать кодирование.
Здесь idForEncode — это сокращенное имя кодировщика паролей, доступ к которому можно получить с помощьюPasswordEncoderFactories.createDelegatingPasswordEncoder()Внутренняя реализация видит, что по умолчанию используется префикс bcrypt, то есть BCryptPasswordEncoder.
public class PasswordEncoderFactories {
public static PasswordEncoder createDelegatingPasswordEncoder() {
String encodingId = "bcrypt";
Map<String, PasswordEncoder> encoders = new HashMap();
encoders.put(encodingId, new BCryptPasswordEncoder());
encoders.put("ldap", new LdapShaPasswordEncoder());
encoders.put("MD4", new Md4PasswordEncoder());
encoders.put("MD5", new MessageDigestPasswordEncoder("MD5"));
encoders.put("noop", NoOpPasswordEncoder.getInstance());
encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
encoders.put("scrypt", new SCryptPasswordEncoder());
encoders.put("SHA-1", new MessageDigestPasswordEncoder("SHA-1"));
encoders.put("SHA-256", new MessageDigestPasswordEncoder("SHA-256"));
encoders.put("sha256", new StandardPasswordEncoder());
return new DelegatingPasswordEncoder(encodingId, encoders);
}
}
2. Настройте конфигурацию безопасности Spring
Определить класс конфигурации SpringSecurityConfig и наследоватьWebSecurityConfigurerAdapterПереопределите его метод configure(HttpSecurity http).
@EnableWebSecurity Проверьте исходный код аннотации, в основном обратитесь к WebSecurityConfiguration.class и добавьте аннотацию @EnableGlobalAuthentication, которая здесь не будет представлена, нам нужно только понять, что добавление аннотации @EnableWebSecurity активирует функцию безопасности.
formLogin() использует форму входа в систему (адрес запроса по умолчанию — /login).В Spring Security 5 значение по умолчанию httpBasic() старой версии было заменено на formLogin() , чтобы указать, что форма входа в систему все еще настроена один раз.
authorizeRequests() начинает запрашивать конфигурацию разрешений
antMatchers() использует сопоставление путей в стиле Ant, где конфигурация соответствует / и /index.
пользователи allowAll() могут свободно получать доступ
anyRequest() соответствует всем путям
authentication() доступна после входа пользователя в систему.
3. Настроить html и протестировать интерфейс
Создайте новый index.html в каталоге resources/static и определите внутри него кнопку для доступа к тестовому интерфейсу.
Создать интерфейс в стиле отдыха для получения информации о пользователе
@RestController
public class TestController {
@GetMapping("/get_user/{username}")
public String getUser(@PathVariable String username){
return username;
}
}
4. Запустите тест проекта
1. Доступ к localhost:8080 осуществляется напрямую без какой-либо блокировки
2. Нажмите кнопку Test Verification Permission Control, чтобы перейти на страницу входа в систему безопасности по умолчанию.
3, с помощью MyUserDetailsUserService определен пользователь учетной записи по умолчанию: 123456 Перейти после успешного входа в систему / интерфейс get_user
Во-вторых, анализ конфигурации @EnableWebSecurity.
Помните, что @EnableWebSecurity относится к классу конфигурации WebSecurityConfiguration и аннотации @EnableGlobalAuthentication? Среди них WebSecurityConfiguration — это конфигурация, связанная с авторизацией, а @EnableGlobalAuthentication настраивает конфигурацию, связанную с аутентификацией, в следующем разделе.
Сначала мы смотрим исходный код WebSecurityConfiguration, мы можем четко найтиspringSecurityFilterChain()метод.
Этот метод сначала определит, является ли webSecurityConfigurers пустым, и загрузит объект WebSecurityConfigurerAdapter по умолчанию, если он пуст.Поскольку сам пользовательский SpringSecurityConfig наследует объект WebSecurityConfigurerAdapter, наша пользовательская конфигурация безопасности обязательно будет загружена (если вы хотите знать, как ее загрузить и см. метод WebSecurityConfiguration.setFilterChainProxySecurityConfigurer()).
Мы смотрим на метод websecurity.build(), реализующий фактический вызов метода AbstractConfigureDSecurityBuilder.Dobuild(), который реализован внутри следующего:
Мы ориентируемся наperformBuild()метод, см. его реализацию подкласса метода HttpSecurity.performBuild(), его внутренние фильтры сортировки и создаетDefaultSecurityFilterChainобъект.
Мы можем просмотреть журнал запуска проекта. Вы можете видеть, что следующий рисунок ясно печатает этот журнал и печатает все имена фильтров. ==(Обратите внимание на напечатанную здесь цепочку фильтров, все наши процессы авторизации основаны на расширении этой цепочки фильтров)==
Тогда возникает другой вопрос: как загружаются фильтры в методе HttpSecurity.performBuild()? В это время вам нужно проверить метод WebSecurityConfigurerAdapter.init().Этот метод внутренне вызывает метод getHttp() для возврата объекта HttpSecurity (видя здесь, мы должны думать, что фильтры — это данные, добавляемые в этот метод), и как в нагрузку он вводиться не будет.
Анализ @EnableWebSecurity занял так много времени,На самом деле наиболее важным моментом является создание DefaultSecurityFilterChain.То есть мы часто используем цепочку ответственности фильтра безопасности.Далее мы анализируем процесс авторизации вокруг фильтров в этой DefaultSecurityFilterChain.
3. Анализ процесса авторизации
Процесс авторизации Security можно понимать как обработку различных фильтров и, наконец, завершение авторизации. Тогда давайте взглянем на распечатанную ранее цепочку фильтров, здесь для удобства еще раз выложим картинку
Здесь мы сосредоточимся только на следующих важных фильтрах:
SecurityContextPersistenceFilter Этот фильтр в основном отвечает за следующие вещи:
Получите объект SecurityContext (контекст безопасности, аналогичный ApplicaitonContext) из запрашивающего сеанса с помощью метода (SecurityContextRepository)repo.loadContext().Если запрашивающий сеанс не создает аутентификацию (ключевой объект аутентификации по умолчанию, так как только этот раздел говорит об авторизации, она пока не будет представлена. ) объект SecurityContext со свойством null
SecurityContextHolder.setContext() помещает объект SecurityContext в SecurityContextHolder для управления (SecurityContextHolder по умолчанию использует стратегию ThreadLocal для хранения информации аутентификации)
Поскольку это реализовано в finally, объект SecurityContext будет очищен от SecurityContextHolder через SecurityContextHolder.clearContext() в конце
Поскольку это реализовано в finally, объект SecurityContext будет помещен в сеанс через repo.saveContext() в конце
Мы устанавливаем точку останова в SecurityContextPersistenceFilter, запускаем проект, посещаем localhost:8080 и выполняем отладку, чтобы увидеть реализацию:
Мы ясно видим, что создается объект SecurityContext с нулевой аутентификацией, и мы можем видеть конкретную цепочку фильтров, вызываемую запросом. Далее, давайте взглянем на внутреннюю обработку finally
Вы обнаружите, что аутентификация в SecurityContxt здесь представляет собой информацию аутентификации с именем анонимный пользователь (анонимный пользователь), поскольку запрос вызывает AnonymousAuthenticationFilter , Security по умолчанию создает доступ анонимного пользователя.
Глядя на буквальное значение фильтра, вы можете видеть, что это фильтр, который авторизуется, получая пароль учетной записи в запросе.Согласно соглашению, обязанности этого фильтра разобраны:
Используйте requireAuthentication(), чтобы определить, запрашивать ли /login по POST
Вызовите метод tryAuthentication() для аутентификации, создайте объект UsernamePasswordAuthenticationToken с аутентифицированным атрибутом false (т. е. неавторизованным) внутри и передайте его методу AuthenticationManager().authenticate() для аутентификации, а затем верните аутентифицированный = истинный аутентификация прошла успешно (то есть авторизация прошла успешно). ) Объект UsernamePasswordAuthenticationToken
Поместите аутентификацию в сеанс через sessionStrategy.onAuthentication()
Вызовите интерфейс onAuthenticationSuccess AuthenticationSuccessHandler через successAuthentication() для успешной обработки (вы можете написать логику успешной обработки, унаследовав AuthenticationSuccessHandler) successAuthentication(request, response, chain, authResult);
Вызовите интерфейс onAuthenticationFailure AuthenticationFailureHandler с помощью failedAuthentication() для обработки сбоя (вы можете написать собственную логику обработки сбоев, унаследовав AuthenticationFailureHandler)
Посмотрим на логику обработки официального исходного кода:
С точки зрения исходного кода весь процесс на самом деле очень ясен: от принятия решения о том, следует ли обрабатывать, до аутентификации и, наконец, оценки результата аутентификации, успех аутентификации и неудача аутентификации обрабатываются соответственно.
Отладьте и посмотрите результат, на этот раз мы запрашиваем localhast:8080/get_user/test , поскольку у нас нет разрешения, мы перейдем непосредственно к интерфейсу входа в систему, сначала мы вводим неверный пароль учетной записи, чтобы увидеть, является ли ошибка аутентификации последовательной. с нашим резюме.
Результат такой же, как и ожидалось.Вы можете удивиться, почему подсказка здесь на китайском языке.Это означает, что Security 5 начал поддерживать китайский язык, что показывает, что китайские программисты становятся все более и более важными в мире! ! !
На этот раз введите правильный пароль и просмотрите возвращенную информацию об объекте аутентификации:
Вы можете видеть, что на этот раз он успешно вернул authticated = ture , информацию об учетной записи пользователя без пароля, а также содержит информацию о правах администратора, которую мы определили. Отпустите точку останова, поскольку обработчиком успеха по умолчанию в Security является SimpleUrlAuthenticationSuccessHandler, этот обработчик перенаправит на ранее доступный адрес, который является localhost:8080/get_user/test. На этом весь процесс заканчивается. Нет, мы еще на один шаг, Сессия, мы видим Сессию из куков браузера:
3. Базовый фильтр аутентификации
BasicAuthenticationFilter похож на UsernameAuthenticationFilter, но разница все же очевидна,BasicAuthenticationFilter в основном получает информацию о параметре авторизации из заголовка, а затем вызывает аутентификацию.После успешной аутентификации он напрямую обращается к интерфейсу, в отличие от процесса UsernameAuthticationFilter, который переходит через AuthenticationSuccessHandler.. Я не буду публиковать код здесь.Студенты, которые хотят знать, могут посмотреть исходный код напрямую. Следует отметить, однако, что обработчик успеха onSuccessfulAuthentication() BasicAuthenticationFilter является пустым методом.
Чтобы протестировать BasicAuthenticationFilter, нам нужно заменить formLogin() в SpringSecurityConfig на httpBasic() для поддержки BasicAuthenticationFilter, перезапустить проект и получить доступ к тому же
localhast:8080/get_user/test. В настоящее время, поскольку у вас нет разрешения на доступ к этому адресу интерфейса, на странице появится окно входа в систему. Учащиеся, знакомые с Security4, должны быть знакомы с ним. Аналогичным образом, после того, как мы вводим пароль учетной записи, смотрим отладочные данные:
В это время мы можем получить параметр авторизации, а затем проанализировать полученную информацию об учетной записи и пароле для аутентификации.Мы проверяем, что информация об объекте аутентификации, возвращаемая после успешной аутентификации, на самом деле такая же, как и в UsernamePasswordAuthticationFilter, и, наконец, снова вызываем следующий фильтр Поскольку аутентификация прошла успешно, он напрямую войдет в FilterSecurityInterceptor для проверки авторизации.
4. Фильтр анонимной аутентификации
Почему здесь упоминается AnonymousAuthenticationFilter, в основном потому, что нет такого понятия как отсутствие учетной записи в Security (описание здесь может быть не очень понятное, но общий смысл такой), этот AnonymousAuthenticationFilter специально предназначен для этого сотрудника Security, который используется в Если все фильтры не проходят аутентификацию, автоматически создается анонимный пользователь по умолчанию с анонимными правами доступа. Помните анонимное сообщение авторизации, которое мы видели, когда говорили о SecurityContextPersistenceFilter? Если вы не помните, вы должны оглянуться назад, и я не буду описывать это здесь.
5. Фильтр перевода исключений
ExceptionTranslationFilter на самом деле не выполняет никакой фильтрации, но не стоит недооценивать его роль, его самая большая и мощная вещь заключается в том, что он захватывает AuthenticationException и AccessDeniedException.Если возникает исключение, это эти два исключения, для обработки будет вызван метод handleSpringSecurityException() . Мы моделируем ситуацию AccessDeniedException (нет разрешения, нет исключения доступа), сначала нам нужно изменить интерфейс /get_user:
Добавить в контроллер
@EnableGlobalMethodSecurity(prePostEnabled =true) Включить контроль доступа на уровне метода безопасности
Добавьте @PreAuthorize("hasRole('user')") в интерфейс, чтобы разрешить доступ только к учетным записям с ролью пользователя (помните роль администратора, когда мы получили учетную запись пользователя по умолчанию?)
@RestController
@EnableGlobalMethodSecurity(prePostEnabled =true) // 开启方法级别的权限控制
public class TestController {
@PreAuthorize("hasRole('user')") //只允许user角色访问
@GetMapping("/get_user/{username}")
public String getUser(@PathVariable String username){
return username;
}
}
Перезапустите проект, вернитесь к интерфейсу /get_user, введите правильный пароль учетной записи и обнаружите, что возвращается страница ошибки со статусом 403, что аналогично процессу, который мы делали ранее. Отладьте, посмотрите на обработку:
Хорошо видно, что объектом исключения является AccessDeniedException, а информация об исключении заключается в том, что доступ запрещен. Давайте посмотрим на метод обработки accessDeniedHandler.handle() после исключения AccessDeniedException и введем метод handle() класса AccessDeniedHandlerImpl. метод сначала определит, настроена ли система. Если отображается errorPage (страница ошибки), если нет, установите код состояния 403 непосредственно в ответ.
6. Фильтрбезопасностиперехватчик
FilterSecurityInterceptor — последний и самый важный во всей цепочке фильтров Security, его основная функция — определить, есть ли у пользователя, успешно прошедшего аутентификацию, разрешение на доступ к интерфейсу, основной метод обработки — вызов super.beforeInvocation родительского класса ( AbstractSecurityInterceptor).(fi), давайте разберемся с потоком обработки этого метода:
Получите информацию о разрешении, требуемую текущим адресом доступа, с помощью getSecurityMetadataSource().getAttributes().
Получите информацию о разрешениях текущего пользователя доступа с помощью функции authenticationIfRequired().
Используйте механизм голосования, чтобы судить о правах через accessDecisionManager.decide(), если решение терпит неудачу, исключение AccessDeniedException вызывается напрямую.
Весь процесс не выглядит сложным, в основном он разделен на 3 части: первая — получение информации о разрешении адреса доступа, вторая — получение информации о разрешении текущего пользователя доступа, и, наконец, используется механизм голосования. чтобы определить, имеет ли пользователь право.
4. Личное резюме
整个授权流程核心的就在于这几次核心filter的处理,这里我用序列图来概况下这个授权流程
(PS: Если картинка не ясна, вы можете посетить адрес проекта на github)
В этой статье описан код процесса авторизации, вы можете получить доступ к модулю безопасности в репозитории кода, адрес проекта на github:GitHub.com/bug9/весна…
Если вы заинтересованы в них, добро пожаловать, пометьте, подпишитесь, добавьте в избранное и вперед, чтобы поддержать!