Простой сервис аутентификации и авторизации по JWT на основе фильтров SpringSecurity

—

от автора

Ссылка на проект: GitLab

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

Cоздадим и настроим проект https://start.spring.io/

SpringInitializer

SpringInitializer

Нам понадобится:

  • Web

  • Security

  • JPA

  • H2 Database

  • Lombok

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

application.properties

application.properties

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

Содержание файлов data && schema
data.sql

data.sql
schema.sql

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

User
Role

Role

Аутентификация

  • Создадим фильтр для аутентификации и выдачи JWT

  • Создадим сервис для работы с JWT

  • Настроим SecurityFilterChain

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

WebConfiguration.jav

WebConfiguration.jav

Для шифрование пароля будем использовать 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)

InitialAuthenticationFilter.java

InitialAuthenticationFilter.java

Данный класс будет расширять OncePerRequestFilter и переопределять два метода

  • doFilterInternal

  • shouldNotFilter

Данный фильтр проверяет header Authorization, если он пустой то проверяет передано ли в теле запроса JSON с именем и паролем для аутентификации, проверят пользователя и если все ок выдает JWToken.

Рассмотрим более подробно данный класс.

Для формирования JWToken и проверки пользователя создадим и заинжектим два класса

private final JwtService jwtService;
private final UsernamePasswordAuthenticationProvider authenticationProvider;

JwtService.java

JwtService.java

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

pom.xml

pom.xml

Здесь мы будем генерировать ключ подписи и собственно сам JWT.

Keys.hmacShaKeyFor(signingKey.getBytes(StandardCharsets.UTF_8));

шифруем ключ BASE64

В методе generatedJwt строим JWT.

Задаем поля в payload и нагрузку, устанавливаем дату окончания действия и ключ подписи, тело нашего JWT будет состоять из следующих полей:

payload {    "role" : "",   "user_id" : "",   "username" : "",   "exp" : "",   "sub" : ""}
UsernamePasswordAuthenticationProvider.java

UsernamePasswordAuthenticationProvider.java

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

Класс UserDetailsService возвращает пользователя если он имеется в базе

UserService.java

UserService.java

Вернемся к классу 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
Содержание JWToken

Содержание JWToken

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

Создадим класс JwtAuthorizationFilter

JwtAuthorizationFilter.java

JwtAuthorizationFilter.java

Этапы работы фильтра:

  • Применение фильтра ко всем запросам кроме «/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 будут проверять разрешения у данного пользователя и соответственно предоставлять доступ к тем данным которые разрешены.

Теперь что бы наш фильтр заработал нужно добавить его в список фильтров

WebConfiguration.java

WebConfiguration.java
@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

UserController.java

UserController.java

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

Что бы данная анотация сработала требуется добавить аннотацию @EnableMethodSecurity

SpringSecurityJwtApplication.java

SpringSecurityJwtApplication.java

Протестируем

  • Запрос без авторизации

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

Попробуем получить доступ к данным у которых отличаются права доступа. Получаем response 403 forbidden

Получаем response 403 forbidden. Ограничения работают.

Данный пример демонстрирует как достаточно быстро и просто поднять сервис аутентификации, авторизации, выдачи JWT.

Ссылка на проект: GitLab


ссылка на оригинал статьи https://habr.com/ru/articles/781066/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *