Spring Security
- огромный модуль в Spring с перечнем инструментами для устранения уязвимостей
Пример конфига от OTUS (базовый): https://github.com/OtusTeam/Spring/blob/master/2024-03/spring-23/3.x-style/src/main/java/ru/otus/spring/security/SecurityConfiguration.java
Пример простого конфига (свой):
https://github.com/timofeev-vadim-96/library-api
Зависимость
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!--для ТЕСТОВ безопаности-->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<version>${spring-security.version}</version>
</dependency>
Аутентификация
- доказать сервису, что это мы
- вход в систему по логину и паролю
- вход по отпечатку пальца или face-id
- ввод pin-кода на экране блокировки
- ответ на секретный вопрос
Авторизация
- определение уровня прав пользователя после его аутентификации
- разница между игроком в игре и модератором или админом
- авторизация в банке позволяет управлять своим счетом, но не чужими
- авторизация в интернет-магазине позволяет купить товар, но не изменить его описание и все, что связано с его модерацией
Основные принципы безопасности
:
- Минимальные привилегии (права пользователя). Например, сделать так, чтобы обычный пользователь не мог удалить или изменить данные в сервисе
- Защита данных (пароли + шифрование БД с персональными данными)
- Аудит и мониторинг (регулярно проверять безопасность системы и анализировать случаи неправомерного доступа пользователями)
- Обработка ошибок и исключений (корректно обрабатывать исключения, чтобы они не выдали информацию злоумышленникам)
- Обновления и патчи (исправлять ошибки в приложении)
Основные компоненты Spring Security
- Аутентификация
- Авторизация
- Защита от атак
- Шифрование паролей
Механизмы авторизации
Spring Security
- По URL-ам
- Методы в сервисах, контроллерах
- По бизнес-объектам (документы, пользователи, счета и т.д.) - с помощью ACL (Access Control List)
Механизмы аутентификации
- HTTP Basic autentification
- Form-based autentification
- HTTP X.509 (через сертификаты)
- OpenID/OAuth2.0
- LDAP, Active Directory
Сценарий Аутентификации
SS:
- Полученные имя и пароль с помощью Authentication фильтра помещаются в UsernamePasswordAuthenticationToken (реализует Authentication) в SecurityContext
- Созданный AuthToken передаётся для проверки в AuthenticationManager через другие фильтры
- AuthenticationManager возвращает полностью инициализированный объект Authentication
- Устанавливается контекст безопасности посредством вызова SecurityContextHolder. getContext().setAuthentication(...)
- С этого момента пользователь считается аутентифицированным
- Кстати достать можно в Spring MVC контроллере: SecurityContextHolder.getContext().getAuthentication()
Фильтры
Spring Security:
- ForceEagerSessionFilter
- ChannelProcessingFilter
- SecurityContextPersistenceFilter
- CorsFilter, CsrfFilter
- Фильтр(ы) первичной аутентификации
- Фильтр(ы) вторичной аутентификации
- SessionManagementFilter
- ExceptionTranslationFilter
- AuthorizationFilter //отвечает за авторизацию по URL
Как Spring Security встраивает свои фильтры в общую цепочку фильтров:
- внедряется прокси DelegatingFilterProxy со списком кастомных фильтров Spring Security
Модели прав доступа
в Spring Security:
- Ролевая модель - у пользователей есть роли (разрешения)
- ACL (Access Control List) – очень навороченная модель (для очень сложной кастомной настройки разрешений пользователей), на основе бизнес-сущностей (аналогичная правам доступа к файлам и папкам в ОС: чтение/запись/изменение/удаление)
Отличие roles от authorities: авторитис с префиксом ROLE_, роли без. Пример авторити: ROLE_ADMIN, пример роли: ADMIN
Добавить иерархию ролям
(очень редко используется)
@Bean
public RoleHierarchyImpl roleHierarchy() {
RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl();
roleHierarchy.setHierarchy("ROLE_ADMIN > ROLE_USER"); //тогда у админа есть доступ везде, где есть доступ у юзера
return roleHierarchy;
}
Проверка прав
- Проверка прав осуществляется в AccessDecisionManager
- AccessDecisionManager при проверке прав опирается на GrantedAuthorities
Авторизация для методов
(через AOP - выполняются после фильтров, когда запрос уже доходит до DispetcherServlet)
3 способа:
- Аннотацию @Secured("ROLE_ADMIN") – можно несколько авторитис
- Аннотации JSR-250 - очень старая штука
- Spring Security аннотации позволяющие поддерживающие EL выражения (самая круть)
Включить авторизацию для методов
@EnableMethodSecurity(
securedEnabled = true,
prePostEnabled = true
)
public class MethodSecurityConfiguration {
}
Атрибуты аннотации @EnableMethodSecurity:
- pre-post-annotations
- jsr250-annotations (https://en.wikipedia.org/wiki/ JSR_250)
- secured-annotations Все отключены по умолчанию.
Аннотации, поддерживающие EL
(expression language) - для методов
- @PreAuthorize (ТОП 1) - обычно юзается для проверки на основе ролей
- @PreFilter - работает очень похожим образом, однако фильтрация применяется к списку, который передается в качестве входного параметра аннотированному методу.
- принимает параметры
- filterObject - для ссылки на объект фильтрации в EL выражении
- filterTarget - для определения имени параметра для фильтрации
- @PostAuthorize - юзать для проверки соответствия полей результирующего объекта после отработки метода
- @PostFilter (ТОП 2) - определяет правило для фильтрации возвращаемой коллекции(не массива!!) методом путем применения этого правила к каждому элементу в списке. Если вычисленное значение равно true, элемент будет сохранен в списке. В противном случае элемент будет удален.
Авторизация для методов класса
пример:
@PreAuthorize("hasRole('USER') && {new java.util.Random().nextInt()%2 == 0}")
public String onlyUser() {
return "My love";
}
@PostAuthorize ("returnObject.owner == authentication.name")
public Book getBook();
@Secured("ADMIN")
public void onlyAdmin() {
}
@PostFilter("hasRole('MANAGER') or filterObject.assignee == authentication.name")
List<Task> findAll() {
// ...
@PreFilter("hasRole('MANAGER') or filterObject.assignee == authentication.name")
Iterable<Task> save(Iterable<Task> entities) {
// ...
}
}
Базовый алгоритм аутентификации
первый запрос без параметров - не авторизован, второй запрос с параметрами пользователя и пароля - уже может быть авторизован и предоставлен доступ к запрашиваемому ресурсу
Способы аутентификации
- HTTP Basic/Digest authentication - форма аутентификации будет предоставляться самим браузером.
- логин и пароль передаются в HTTP-заголовке "Authorization" в кодировщике Base64
- Form-based authentication (Customizer.withDefaults() - будет стандартная форма, но можно реализовать свою)
- логин и пароль из формы передаются в качестве requestParam в теле Post-запроса
- HTTP X.509 (сертификаты)
- OpenID/OAuth
- LDAP
- Можно реализовать свой механизм
HTTP Basic
, (юзать только между микросервисами в локальной сети)
- BasicAuthenticationFilter читает из заголовка HTTP запроса имя и пароль и использует для аутентификации
- BasicAuthenticationFilter соответствует RFC-1945
преимущества и недостатки:
Преимущества:
- Простота механизма
- Поддержка всеми браузерами
- Идеально для микросервисов
Недостатки:
- Имя пользователя и пароль передаются в открытом виде (необходимо использовать HTTPS)
Form-based
, пример кастомной реализации:
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http)
throws Exception {
http.formLogin(
fm -> fm.loginPage("/extlogin")
.loginProcessingUrl("/extlogin_process") //редирект
.usernameParameter("extuser") //requestParam логина
.passwordParameter("extpass") //requestParam пароля
);
}
Цепочка фильтров
при form-based аутентификации:
- DelegatingFilterProxy - вставляет список кастомных фильтров в общую цепочку спринга
- FilterChainProxy - хранит список фильтров
- SecurityContextFilterProxy - хранит данные об аутентифицированных пользователях
- UsernamePasswordAuthenticationFilter - проверяет юзака на аутентификацию
- ProviderManager - юзает провайдеров для аутентификации пользователя
- DaoAuthenticationProvider - тащит из UserDetailsService данные о пользователе
Основные компоненты
Spring Security:
- SecurityContext - контейнер, в котором хранится информация о текущем аутентифицированном пользователе (объекты типа Authentication). Отражает текущий контекст безопасности
- Authentication - объект, представляющий собой аутентифицированного пользователя и его привелегии (token)
- principal = login (учетная запись пользователя). Основная реализация = UserDetails
- credentials = пароль
- authorities = права (в спринге - должны начинаться на "ROLE_". Пример: ROLE_ADMIN)
- UserDetails (доменный объект, представляющий пользователя) - инфа о пользователе (логи, пароль, роли)
- UserDetailsService - сервис, подгружающий UserDetails из БД. Реализации: InMemoryDaoImpl, JdbcDaoImpl (во втором случае провались внутрь, реализация уже есть для реляционных БД, ИМЕНА ТАБЛИЦ должны совпадать с внутренней реализацией)
- GrantedAuthority - разрешение на какое-то действие
InMemoryDaoImpl - реализация UserDetailsService
@Bean
public InMemoryUserDetailsManager userDetailsService() {
UserDetails user = User
.builder()
.username( "user" )
.password( "password" )
.roles( "USER" )
.build();
return new InMemoryUserDetailsManager( user );
}
Алгоритм работы
Spring Security
- BasicAuthenticationFilter - это если аутентификация на базе httpBasic()
- AuthenticationManager - интерфейс объекта, выполняющего аутентификацию. Чаще всего делегирует AuthenticationProvider-ам, которых может быть несколько
- AuthenticationProvider - скорее всего будет залазить в UserDetailsService
Реализации AuthenticationManager-a
- вся левая часть - для выдачи разрешения по URL
- правая часть - для доступа к методам сервисов и сущностям через @Secured и @PreAuthorized
Цепочку фильтров по порядку можно посмотреть в дебаг-режиме в классе
FilterChainProxy
Пример своей реализации AuthenticationManager
class SampleAuthenticationManager implements AuthenticationManager {
public Authentication authenticate(Authentication auth) throws
AuthenticationException {
Collection<GrantedAuthority> roles = Collections.emptyList();
if (auth.getName().equals(auth.getCredentials())) {
return new UsernamePasswordAuthenticationToken(
auth.getName(), auth.getCredentials(), roles);
}
throw new BadCredentialsException("Bad Credentials");
}
}
Кастомно аутентифицировать пользователя
(функционал уже есть в Spring Security)
String name = "<USERNAME>";
String password = "<PASSWORD>";
AuthenticationManager am = new SampleAuthenticationManager();
try {
Authentication request =
new UsernamePasswordAuthenticationToken(name, password);
Authentication result = am.authenticate(request);
SecurityContextHolder.getContext().setAuthentication(result);
} catch (AuthenticationException e) {
System.out.println("Authentication failed.");
}
Режимы работы контектса и где он хранится
SecurityContextHolder
- то, на каком уровне многопоточности доступен контекст безопасности:
- MODE_THREADLOCAL (дефолт) - контекст безопасности доступен из потока, в котором происходит HTTP-запрос
- MODE_INHERITABLETHREADLOCAL - контекст доступен в дочерних потоках
- MODE_GLOBAL - для десктоп-приложений, где 1 приложение = 1 пользователь
2024
Конфигурация Spring Security
2023
Туториал
Виды хакерских атак
:
- CSRF (межсайтовая подделка запроса) - выполнение пользователем нежелательных действий (не сознательно)
- XSS (межсайтовый скриптинг) - вставка хакером злонамеренного скрипта в веб-страницу
- SQL-инъекции.
- Атаки с перехватом сессии (Session Hijacking) - зачастую перехват в рамках wifi-роутера вашей сессии на сайте и работа от лица авторизованного пользователя
@Secured
- для определения ролей, которые могут получить доступ к ручке
@GetMapping("/any")
@Secured({"USER", "ADMIN"})
public String getPublic(){
return "any";
}
Конфигурация Spring Security
+ @Configuration
Для сборки старого типа
проекта Spring Security 5.7.10:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.14</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
Способы настройки Spring Security
:
- наследоваться от WebSecurityConfigurerAdapter и реализовать метод configure(HttpSecurity http)
- создать бин класса SecurityFilterChain (HttpSecurity http) с версии 3.x, в классе @Configuration
Пример
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable() //отрубаем стандартную проверку, вместо нее нужны JWT-токены
.authorizeRequests().antMatchers("/public/**").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/#")
.permitAll()
.and()
.logout()
.permitAll();
}
//здесь просто создание пользователя
@Bean
public UserDetailsService userDetailsService(){
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(
User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("USER")
.build());
return manager;
}
}
- antMatchers - все запросы по адресу будут доступны всем
- anyRequest().authenticated() - все остальные запросы только аутентифицированным пользователям
- loginPage() и logOut() - доступны всем
в обязательном порядке переопределяется метод configureGlobal
Как делается с помощью дефолтного Spring Security в настоящее время
:
дефолтная защита
в Spinrg Security
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
http
.authorizeHttpRequests((authz) -> authz
.anyRequest()//любой запрос
.authenticated()//должен быть аутентифицирован
)
.httpBasic(Customizer.withDefaults()); //дефолтная форма авторизации
return http.build();
}
по дефолту
:
- страница входа использует логин: user и пароль: сгенерированный в терминале
- для выхода "/logout"
- Создать конфигурационный файл, в котором включим веб-безопасность с помощью аннотации
@EnableWebSecurity
ИЛИ повесить над main-ом
SecurityFilterChain
- конфиг спринга по доступу к ресурсам приложения пользователями
@Configuration
@EnableWebSecurity
public class WebSecurityConfig{
/**
* Бин, позволяющий разграничить доступ в зависимости от прав
* @param httpSecurity - такой бин уже существует под капотом спринга
*/
@Bean
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
return httpSecurity
.authorizeHttpRequests(registry-> registry
//requestMatchers вместо antMatchers в версии 6.0
.antMatchers("/user/**").hasAnyAuthority("user", "admin") //любая из ролей
.antMatchers("/admin/**").hasAuthority("admin") //конкретная роль
.antMatchers("/auth/**").authenticated() //любой авторизованный
.antMatchers("/any/**").permitAll() //доступ всем
.anyRequest().denyAll() //для всех остальных ресурсов - запрет всем (необязательно)
)
.formLogin(Customizer.withDefaults()) //если не авторизовались по фильтрам выше - выдать форму авторизации
.build();
}
}
UserDetailsService
- класс, который будет обрабатывать запросы и давать доступ
@RequiredArgsConstructor
@Component
public class CustomUserDetailService implements UserDetailsService {
private PersonDao dao;
/**
* Определяет, есть ли такой пользователь в БД и какие у него права
*/
@Override
public UserDetails loadUserByUsername(String login) throws UsernameNotFoundException {
PersonEntity person = dao.findByLogin(login).orElseThrow(()->
new UsernameNotFoundException(String.format("пользователь с логином %s не найден.", login)));
return new User(person.getLogin(), person.getPassword(), List.of(new SimpleGrantedAuthority(person.getRole())));
}
}
- SimpleGrantedAuthority у пользака должна начинаться с "ROLE_". Например: ROLE_USER
PasswordEncoder
- класс, шифрующий пароли и определяющий их соответствие
Стандартные реализации PasswordEncoder
:
- ShaPasswordEncoder
- Md5PasswordEncoder
- Md4PasswordEncoder
- PlaintextPasswordEncoder
- BCryptPasswordEncoder - TOP!!! Добавляет соль (левый набор символов) к паролю перед шифрованием
PasswordEncoder
@Component
@Profile("security_common")
public class CustomPasswordEncoder implements PasswordEncoder {
/**
* Метод шифрования данных в тот вид, в котором будут храниться данные
*/
@Override
public String encode(CharSequence rawPassword) {
//шифруем данные
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
return passwordEncoder.encode(rawPassword);
}
/**
* Сравнение шифрованного пароля с паролем из БД для АВТОРИЗАЦИИ
*/
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
return passwordEncoder.matches(rawPassword, encodedPassword);
}
}
//ТОЖЕ САМОЕ БУДЕТ ЕСЛИ ЧЕРЕЗ БИН
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
Чтобы задать PasswordEncoder, который ничего не делает
:
return NoOpPasswordEncoder.getInstance();
Anonymous Authentication
- поведение по умолчанию для не аутентифицированных юзаков. Обычно используется для открытых ресурсов
- Основная идея – иметь пользователя с самыми дохлыми правами
AnonymousAuthenticationToken
- Реализация интерфейса Authentication для анонимной аутентификации
- Хранит список GrantedAuthority для анонимной аутентификации
AnonymousAuthenticationFilter
- Аутентифицирует пользователя как анонимного в случае если отсутствует другая аутентификация
- Находится в цепочке после всех настроенных фильтров аутентификации
Пример добавления анонимному юзеру прав
public SecurityFilterChain securityFilterChain (HttpSecurity http)
throws Exception {
http.formLogin(Customizer.withDefaults())
.anonymous(a -> a.principal("anonymousUser") //сюда можно также положить объект класса, наследуемого от UserDetails
.authorities("ROLE_ANONYMOUS") //права анонимного юзера
);
}
Remember Me Authentication
- Добавит галочку "запомнить меня" под формой аутентификации
- Remember Me аутентификация позволяет сохранять информацию о пользователе между HTTP сессиями
- Использование Remember Me аутентификации позволяет при очередном обращении к приложению использовать ранее введённые имя и пароль
- Обычно реализуется через сохранение cookie в браузере
- Работает только с form-based аутентификацией
- Simple Hash-Based Token подход (кстати переписываем обычно на JWT) //вся необходимая инфа для аутентификации есть в токене - по ДЕФОЛТУ
- Persistent Token подход //должен ходить в хранилище токенов
- RememberMeAuthenticataionFilter проверяет есть ли кука, и пытается залогинить пользака по принципалу и кредам этого пользака
Cookie
содержит закодированную в Base64 строку с:
- Именем пользователя
- Временем экспирации токена
- Md5(хэш-код) от имени + пароль + время экспирации + ключ
Включить remember-me
@Bean
public SecurityFilterChain securityFilterChain (HttpSecurity http)
throws Exception {
http.rememberMe(rm -> rm.key("AnyKey") //здесь ключ нужен рандомный для сложности
.tokenValiditySeconds(600));
}
Simple hash-based token
:
base64(
username + ":" + expirationTime + ":" +
md5Hex(
username + ":" +
expirationTime + ":" + password
+ ":" + key
)
)
key - для усиления хэш-кода
Persistent Token
подход для remember-me
- Альтернатива подходу Simple Hash-Based Token
- Основное отличие - содержимое Remember-Me cookie
- Для работы необходимо хранилище для RememberMe токенов
- Токены в хранилище доступны по ключу (значения из cookie)
Тестирование Spring-Security:
@WebMvcTest(PagesController.class)
@Import(SecurityConfiguration.class) //импортировать конфиг безопаности
public class PagesControllerTest {
@Autowired
private MockMvc mockMvc;
//моковый аутентифизированный юзер
@WithMockUser(
username = "admin",
authorities = {"ROLE_ADMIN"}
)
@Test
public void testAuthenticatedOnAdmin() throws Exception {
mockMvc.perform(get("/authenticated"))
.andExpect(status().isOk());
}
}
Как собирается безопасность на реальных проектах в настоящее время
OAuth 2.0
OAuth 2.0 — это протокол авторизации, позволяющий выдать одному сервису (приложению) права на доступ к ресурсам пользователя на другом сервисе. Протокол избавляет от необходимости доверять приложению логин и пароль, а также позволяет выдавать ограниченный набор прав, а не все сразу.
Система сервер (хранит ресуры) - авторизационный сервер (хранит информацию о пользователях) - клиент
Зависимость oauth
:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
В качестве авторизационного сервера может выступать KeyCloak Алгоритм работы:
- просто скачать и разорхивировать (возможно, может понадобится настроить переменные среды)
- getting started на сайте -> docker -> выполнить команду
docker run -p 8080:8080 -e KEYCLOAK_ADMIN=admin -e KEYCLOAK_ADMIN_PASSWORD=admin -v ./config/keycloak/import:/opt/keycloak/data/import quay.io/keycloak/keycloak:24.0.2 start-dev --import-realm
application.yml для работы с oath 2.0
:
spring:
datasource:
url: jdbc:h2:mem:test
username: root
password:
driver-class-name: org.h2.Driver
security:
oauth2:
resourceserver:
jwt:
issuer-uri: http://localhost:8080/realms/master #по этому адресу работает котейнер oauth 2.0, и из этой же информации наш сервер узнает, валиден ли JWT-токен пользователя
server:
port: 8081
Настройка работы с oauth 2.0:
Создать нового клиента (клиент - это приложение) в oauth 2.0:
- запускаем контейнер
- заходим на порт работы (дефолт=8080)
- создаем новый realm (здесь только придумать ему название)
- создаем необходимые группы (например, исполнители и логисты)
- создаем новые роли (ROLE_LOGISTICAN и др.) и связываем их с группами
- clients -> create client
- general settings -> имя в id прописать
- capacity config -> рис. ниже (client authentication только для общения между серверами)
- login settings -> ничего
Создание нового клиента в oauth 2.0
- client authentication ползунок - если обмен инфы будет между бэкэндами. Секретный ключ не стоит исопльзовать для связи с фронтом
Настройка нового пользователя в клиенте:
здесь email просто будет в JWT-токене и не играет роль при авторизации
далее: установить пароль пользователю:
users -> credentials -> set password
вся информация по oauth 2.0
в формате JSON: Realm settings -> OpenID Endpoint Configuration
Проверка получения токена авторизации oauth
с помощью Postman:
- grant_type = password - значит, что мы хотим получить токен авторизации
- grant_type = refresh_token - значит, что мы хотим обновить токен. В этом случае нужно еще одно поле refresh_token = скопировать весь refresh_token из ответа и убрать password
- client_secret - секретный пароль, созданный при создании клиента, чтобы вообще предоставлять возможность к нам обращаться. (обычно, только для связи между серверами. При связи с фронтом будет отсутствовать)
Находится: client -> credentials -> client secret
jwt.io
(сайт) - для удобного вида JWT-токена
jwt-token может хранить публичную информацию, такую как:
- exp - время создания
- roles
access token получается по дефолту на 60 минут, refresh token - на 1800 минут (время тоже содержится в ответе от сервера)
Конфиг спринга для работы с oauth 2.0:
SecurityFilterChain
- конфиг спринга по доступу через oauth, где приложение - сервер ресурсов
@Configuration
@EnableWebSecurity
@Profile("security_oauth")
public class WebSecurityConfig{
/**
* Бин, позволяющий разграничить доступ в зависимости от прав
* @param httpSecurity - такой бин уже существует под капотом спринга
*/
@Bean
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
//для того, чтобы вытащить роли из JWT-токена, который в формате JSON
JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
converter.setJwtGrantedAuthoritiesConverter(source -> {
Map<String, Object> claim = source.getClaim("realm_access"); //достаем заголовок
List<String> roles = (List<String>) claim.get("roles"); //достаем роли из заголовка
return roles.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
});
return httpSecurity
.authorizeHttpRequests(registry-> registry
//requestMatchers вместо antMatchers в версии 6.0
.antMatchers("/user/**").hasAnyAuthority("user", "admin") //любая из ролей
.antMatchers("/admin/**").hasAuthority("admin") //конкретная роль
.antMatchers("/auth/**").authenticated() //любой авторизованный
.antMatchers("/any/**").permitAll() //доступ всем
.anyRequest().denyAll() //для всех остальных ресурсов - запрет всем (необязательно)
)
// .oauth2ResourceServer(configurer-> configurer
// .jwt(Customizer.withDefaults())) //если токен валидный - то происходит авторизация
//здесь мы говорим - когда придет токен, с помощью конвертера извлекать из него роли
.oauth2ResourceServer(configurer->configurer
.jwt(jwtConfigurer -> jwtConfigurer.jwtAuthenticationConverter(converter)))
.build();
}
}
использование паттернов
в requestMatchers/antMatchers: /book/* - все урлы, после которых идет еще один элемент. /book/** - все урлы с любым продолжением
Проверка работы через Postman приложения + oauth 2.0
:
- также получаем токен, как было выше (по адресу oauth, дефолт=8080)
- отправляем гет-запрос, с 1 параметров хедере: Authorization = Bearer eyJhbGci..... (access token), по адресу=8081 или любой другой кастомный
Чтобы создать роли и задать их пользователям в oauth
:
- realms roles -> create role
- users -> конкретный пользователь -> role mapping -> assign role
CSRF
(Cross Site Request Forgery) - дефолтная защита он небезопасных запросов (все, исключая GET-запросы). По дефолту не пропускает их.
Как пропускать: (лишнее закоммитил)
- отключить защиту
// @Bean
// public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
// return httpSecurity
// .authorizeHttpRequests(registry-> registry
// .requestMatchers("/ui/issue/**").hasAuthority("manager")
// .requestMatchers("/ui/reader/**", "/ui/issue/**").hasAuthority("admin") //конкретная роль
// .requestMatchers("/ui/book/**").authenticated() //любой авторизованный
// .requestMatchers("/book/**", "/issue/**", "/reader/**").permitAll() //доступ всем
// .anyRequest().denyAll() //для всех остальных ресурсов - запрет всем (необязательно)
// )
// .formLogin(Customizer.withDefaults()) //если не авторизовались по фильтрам выше - выдать форму авторизации
//вообще вырубить защиту
.csrf(AbstractHttpConfigurer::disable)
//допустить к исполнению пост запрос на такой адрес
.csrf(httpSecurityCsrfConfigurer -> httpSecurityCsrfConfigurer.ignoringRequestMatchers("/book"))
// .build();
// }
Варианты настройки создания сессий в Spring Security
:
- SessionCreationPolicy.ALWAYS — сессия будет всегда создаваться (если она не существует).
- SessionCreationPolicy.NEVER — Spring Security никогда не создаст HttpSession, но будет использовать его, если он уже существует (доступен через сервер приложений).
- SessionCreationPolicy.IF_REQUIRED — Spring Security будет создавать HttpSession только при необходимости (по умолчанию).
- SessionCreationPolicy.STATELESS — Spring Security никогда не создаст HttpSession и не будет использовать его для получения SecurityContext.
- Для защиты приложений на базе Spring WebFlux
- Нереактивный Spring Security совсем не прикручивается к Spring WebFlux
- отсутствует анонимная аутентификация
- нет режима без сессий
- Многие вещи теперь написаны под Mono/Flux
Чтобы врубить Reactive Spring Security
:
@EnableWebFluxSecurity
Простенький пример настройки:
@EnableWebFluxSecurity
@Configuration
public class SecurityConfiguration {
@Bean
public SecurityWebFilterChain springWebFilterChain( ServerHttpSecurity http ) {
return http
.authorizeExchange((exchanges)->exchanges
.pathMatchers( HttpMethod.GET, "/authenticated.html" ).authenticated()
.pathMatchers( "/person" ).hasAnyRole( "USER" )
.anyExchange().denyAll()
)
.httpBasic( Customizer.withDefaults())
.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
@Bean
public ReactiveUserDetailsService userDetailsService() {
UserDetails user = User
.withUsername( "user" )
.password( "password" )
.roles( "USER" )
.build();
return new MapReactiveUserDetailsService( user );
}
}
ACL (Access Control List) - когда нужно осуществлять доступ к каждому отдельному бизнес-объекту по правам каждого отдельного пользователя
Зависимости
<!-- Spring Security ACL и зависимости-->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-acl</artifactId>
</dependency>
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache-core</artifactId>
<version>${ehcache-core.version}</version>
<type>jar</type>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
</dependency>
ACL пример OTUS: https://github.com/OtusTeam/Spring/blob/master/2024-05/spring-24/ACL/src/main/java/ru/otus/spring/model/NoticeMessage.java
Чтобы юзать ACL
, нужно создать ряд таблиц:
- acl_class - таблица, где будут хранится все классы приложения, к которым могут быть применены права доступа
- id - первичный ключ
- class - имя java-класса
- acl_sid - таблица, где хранятся имена пользователей (или роли - но зачем?), у которых могут быть права к тем или иным бизнес-объектам
- id - первичный ключ
- principal - определяет тип security identiity (0/1 - роль/имя пользователя)
- sid - содержит security identity (логин пользователя или название authority)
- acl_object_identity - таблица, расширяющая сущность в таблице классов дополнительными параметрами
- id - первичный ключ
- object_id_class - ссылка на ACL_CLASS
- object_id_identity - индетификатор бизнес сущности
- parent_object - ссылка на родительский класс
- owner_sid - ссылка на ACL_SID определяющая владельца объекта
- acl_entry - таблица для сущностей доступа. Хранит непосредственно права + id пользаков + id бизнес-объектов.
- id - первичный ключ
- ace_order - порядок проверки прав. Например, если у пользователя права administer permission, то их стоит проверить первыми, т.к. они включают в себя остыльные
- audit_success - нужно ли логировать в случае успеха использ. ACE (acl_entry)
- audit_failure - нужно ли логировать в случае неудачи использ. ACE (acl_entry)
- mask - права
- acl_object_identity - ссылка на таблицу ACL_OBJECT_IDENTITY (что)
- sid - пользак (кому)
- granting - тип назначения прав (1 - разрешающие, 0 - запрещающие)
SQL для создания таблиц ACL
:
CREATE TABLE IF NOT EXISTS acl_sid (
id bigint(20) NOT NULL AUTO_INCREMENT,
principal tinyint(1) NOT NULL,
sid varchar(100) NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY unique_uk_1 (sid,principal)
);
CREATE TABLE IF NOT EXISTS acl_class (
id bigint(20) NOT NULL AUTO_INCREMENT,
class varchar(255) NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY unique_uk_2 (class)
);
CREATE TABLE IF NOT EXISTS acl_entry (
id bigint(20) NOT NULL AUTO_INCREMENT,
acl_object_identity bigint(20) NOT NULL, объект
ace_order int(11) NOT NULL, порядок фильтра
sid bigint(20) NOT NULL, пользак
mask int(11) NOT NULL, маска прав
granting tinyint(1) NOT NULL, запрещающие или разрешающие
audit_success tinyint(1) NOT NULL,
audit_failure tinyint(1) NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY unique_uk_4 (acl_object_identity,ace_order)
);
CREATE TABLE IF NOT EXISTS acl_object_identity (
id bigint(20) NOT NULL AUTO_INCREMENT,
object_id_class bigint(20) NOT NULL,
object_id_identity bigint(20) NOT NULL,
parent_object bigint(20) DEFAULT NULL,
owner_sid bigint(20) DEFAULT NULL,
entries_inheriting tinyint(1) NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY unique_uk_3 (object_id_class,object_id_identity)
);
ALTER TABLE acl_entry
ADD FOREIGN KEY (acl_object_identity) REFERENCES acl_object_identity(id);
ALTER TABLE acl_entry
ADD FOREIGN KEY (sid) REFERENCES acl_sid(id);
--
-- Constraints for table acl_object_identity
--
ALTER TABLE acl_object_identity
ADD FOREIGN KEY (parent_object) REFERENCES acl_object_identity (id);
ALTER TABLE acl_object_identity
ADD FOREIGN KEY (object_id_class) REFERENCES acl_class (id);
ALTER TABLE acl_object_identity
ADD FOREIGN KEY (owner_sid) REFERENCES acl_sid (id);
Битовые маски для acl_entry.mask
:
- bit 0 - read permission
- bit 1 - write permission
- bit 2 - create permission
- bit 3 - delete permission
- bit 4 - administer permission (не включает в себя остальные, но можно проверять на них и только)
@PreAuthorize
- проверка прав текущего пользователя на входящий объект
@PreAuthorize("hasPermission(#customer, 'READ')") //проверяет у текущего пользователя права на чтение объекта "customer"
public List<BankAccount> getBankAccounts(
Customer customer) {
...
}
@PreFilter
- проверка прав текущего пользователя на входящие объекты и их фильтрация
//здесь если у текущего пользователя нет permission на READ одного из customers, то они будут удалены из списка
@PreFilter(
value = "hasPermission(filterObject,'READ')",
filterTarget = "customers" //ссылка на объект для фильтрации
)
public List<BankAccount> getBankAccounts(
List<Customer> customers) {
...
}
@PostAuthorize
- проверка прав текущего пользователя на возвращенный объект
@PostAuthorize("hasPermission(returnObject, 'WRITE')")
public BankAccount getBankAccount() {
...
}
@PostFilter
- проверка прав текущего пользователя на каждый объект из возвращенной коллекции
@PostFilter("hasPermission(filterObject, 'READ')")
public List<BankAccount> getBankAccounts(
Customer customer) {
...
}