Ссылка на проект: GitLab
Напишем простой сервис аутентификации с выдачей JWToken. Для реализации будем использовать Java 17, Maven, SpringBoot 3.2.0, h2 в памяти. Авторизация и аутентификация будет реализована на основе фильтров SpringSecurity.
Cоздадим и настроим проект https://start.spring.io/

Нам понадобится:
Web
Security
JPA
H2 Database
Lombok
Настроим подключение к нашей БД которая будет находится в памяти, так же сервер будем запускать на порту 9090

Для автоматического заполнения БД создадим пару файлов с созданием и заполнением таблиц data.sql и schema.sql.
Содержание файлов data && schema


Далее нам потребуется создать сопутствующие сущности User, Role


Аутентификация
Создадим фильтр для аутентификации и выдачи JWT
Создадим сервис для работы с JWT
Настроим SecurityFilterChain
Закончили с базовой настройкой, переходим к основному классу WebConfiguration настройки Security. В нем мы должны настроить bean SecurityFilterChain, так же создадим bean PasswordEncoder для возможности шифрования пароля пользователя.

Для шифрование пароля будем использовать BCrypt.
Далее рассмотрим SecurityFilterChain:
http.headers(headers -> headers .frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin)) .csrf(AbstractHttpConfigurer::disable) .cors(AbstractHttpConfigurer::disable);
В этой строчке мы отключаем CSRF && CORS так как для простого фунционала нам они не понадобятся.
В проде обязательно использовать CSRF && CORS
Т.к h2-console использует frame то для корректной работы нужно добавить header x-frame-options "SAMEORIGIN" или отключить его FrameOptionsConfig::disable
headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable) headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin)
Далее разрешим доступ к консоли h2 для всех пользователей и для остальных запросов требуется аутентификация
http.authorizeHttpRequests(authz -> authz.requestMatchers("/h2-console/**") .permitAll() .anyRequest() .authenticated());
Главное действие будет происходить в фильтрации запроса
http.addFilterAt(initialAuthenticationFilter, BasicAuthenticationFilter.class);
Здесь мы добавляем свой фильтр в цепочку фильтрации initialAuthenticationFilter который мы inject'им в методе
public SecurityFilterChain securityFilterChain(HttpSecurity http, InitialAuthenticationFilter initialAuthenticationFilter)

Данный класс будет расширять OncePerRequestFilter и переопределять два метода
doFilterInternal
shouldNotFilter
Данный фильтр проверяет header Authorization, если он пустой то проверяет передано ли в теле запроса JSON с именем и паролем для аутентификации, проверят пользователя и если все ок выдает JWToken.
Рассмотрим более подробно данный класс.
Для формирования JWToken и проверки пользователя создадим и заинжектим два класса
private final JwtService jwtService;
private final UsernamePasswordAuthenticationProvider authenticationProvider;

Для работы с JWT потребуются следующие библиотеки

Здесь мы будем генерировать ключ подписи и собственно сам JWT.
Keys.hmacShaKeyFor(signingKey.getBytes(StandardCharsets.UTF_8));
шифруем ключ BASE64
В методе generatedJwt строим JWT.
Задаем поля в payload и нагрузку, устанавливаем дату окончания действия и ключ подписи, тело нашего JWT будет состоять из следующих полей:
payload { "role" : "", "user_id" : "", "username" : "", "exp" : "", "sub" : ""}

Данный класс проверяет наличие пользователя и корректность пароля и возвращает аутентификацию.
Класс UserDetailsService возвращает пользователя если он имеется в базе

Вернемся к классу InitialAuthenticationFilter.
Метод doFilterInternal
из request мы извлекаем JSON с логином и паролем
проеряем пользователя
Authentication authentication = new UsernamePasswordAuthentication(username, password); authentication = authenticationProvider.authenticate(authentication);если все ок выдаем JWT в response в заголовок Authorization: Bearer *****
String jwt = jwtService.generatedJwt(authentication); response.setHeader("Authorization", HeaderValues.BEARER + jwt);
Метод shouldNotFilter
Позволяет применить фильтр к отпределенному/ым uri
В данном случае применится к запросам на uri /login
Проверка
curl -v -d '{"username":"admin", "password":"123"}' \ -H "Content-Type: application/json" \ -X POST http://localhost:9090/login

Видим, что появился header
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJyb2xlIjpbIlJPTEVfQURNSU4iLCJST0xFX1VTRVIiXSwidXNlcl9pZCI6IjEiLCJ1c2VybmFtZSI6ImFkbWluIiwiZXhwIjoxNzAzMTU1NzAxLCJzdWIiOiJhZG1pbiJ9.aNHtaBa-7WDXO_MMl83MG9wxTO0MnMmEwdjgzSOrh0g

Теперь можно перейти к части авторизации
Создадим класс JwtAuthorizationFilter

Этапы работы фильтра:
Применение фильтра ко всем запросам кроме "/login"
Проверка и получение header Authorization в запросе
Проверка валидности JWT
Получение пользователя, роли
Создание авторизации
За применение фильтра ко всем запросам кроме "/login" у нас отвечает метод shouldNotFilter(HttpServletRequest request).
Для проверки валидности JWT добавим в класс JwtService следующие методы:
@Override public Claims getClaims(String jwt) { return Jwts.parserBuilder() .setSigningKey(generatedSecretKey()) .build() .parseClaimsJws(jwt) .getBody(); } @Override public boolean isValidJwt(Jwt jwt) { Claims claims = Jwts.parserBuilder() .setSigningKey(generatedSecretKey()) .build() .parseClaimsJws(jwt.getToken()) .getBody(); Optional<User> user = userRepository.findByUsername(String.valueOf(claims.get(ClaimField.USERNAME))); return claims.getExpiration().after(new Date()) && user.isPresent(); }
Соответственно в isValidJwt мы проверяем существует ли пользователь, истекла ли дата выдачи. Если подпись JWT не будет соответствовать то будет выброшено исключение.
В getClaims получим мапу<String, Object> значений которые были добавлены в JWT при формировании.
Далее получаем пользователя, права из JWT и формируем authentication.
List<GrantedAuthority> authorities = (List<GrantedAuthority>) roles.stream() .map(role -> new SimpleGrantedAuthority(role.toString())) .collect(Collectors.toList()); UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthentication(username, null, authorities); SecurityContextHolder.getContext().setAuthentication(authentication);
SecurityContextHolder, в нем содержится информация о текущем контексте безопасности приложения, который включает в себя подробную информацию о пользователе(Principal) работающем в настоящее время с приложением.
SecurityContext, содержит объект Authentication и в случае необходимости информацию системы безопасности, связанную с запросом от пользователя.
Authentication представляет пользователя.
GrantedAuthority отражает разрешения выданные пользователю в масштабе всего приложения, например ROLE_USER, ROLE_ADMIN.
Т.к. SecurityContextHolder должен содержать изформацию об Authentication, то добавляем ее в SecurityContext. Теперь остальные фильтры требующие Authentication будут проверять разрешения у данного пользователя и соответственно предоставлять доступ к тем данным которые разрешены.
Теперь что бы наш фильтр заработал нужно добавить его в список фильтров

@Bean public SecurityFilterChain securityFilterChain(HttpSecurity http, InitialAuthenticationFilter initialAuthenticationFilter, JwtAuthorizationFilter jwtAuthorizationFilter) throws Exception { http .addFilterAt(initialAuthenticationFilter, BasicAuthenticationFilter.class) .addFilterAt(jwtAuthorizationFilter, BasicAuthenticationFilter.class); // ******************************* // Some code // ******************************* return http.build(); } }
Для тестирования создадим котнроллер UserController

Доступ по правам можно сделать через FilterChain, а также с помощью аннотации@PreAuthorize.
Что бы данная анотация сработала требуется добавить аннотацию @EnableMethodSecurity

Протестируем
Запрос без авторизации
curl -v http://localhost:9090/user/get/myname

Получили ответ 403 Forbidden
Авторизуемся под User
Получаем токен

Получаем имя пользователя под которым авторизовались
http://localhost:9090/user/get/myname

Попробуем получить доступ к данным у которых отличаются права доступа
http://localhost:9090/user/get/all

Получаем response 403 forbidden. Ограничения работают.
Данный пример демонстрирует как достаточно быстро и просто поднять сервис аутентификации, авторизации, выдачи JWT.
Ссылка на проект: GitLab
