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