Как стать автором
Обновить

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

Уровень сложностиПростой
Время на прочтение6 мин
Количество просмотров13K

Ссылка на проект: 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

Теги:
Хабы:
Всего голосов 5: ↑3 и ↓2+3
Комментарии8

Публикации

Истории

Работа

Java разработчик
347 вакансий

Ближайшие события

7 – 8 ноября
Конференция byteoilgas_conf 2024
МоскваОнлайн
7 – 8 ноября
Конференция «Матемаркетинг»
МоскваОнлайн
15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань