Сегодня я расскажу как сделать Ваши REST API защищенными и аутентифицировать запросы с помощью Spring Security JWT (Json Web Token).
На сегодняшний день, JWT – один из самых распространенных методов аутентификации запросов на Java WEB приложения. И это не спроста. Данный вид защиты и аутентификации имеет ряд преимуществ:
- удобство (не нужно при каждом запросе передавать логин и пароль);
- меньше запросов в БД (токен может содержать в себе базовую информацию о пользователе);
- простота реализации (достаточно использовать готовую библиотеку для генерации и расшифровки токена)
По сути дела, токен – это просто строка, которая генерируется по запросу пользователя, который хочет в дальнейшем вызывать защищенные ресурсы. Пользователь регистрируется в системе, потом делает запрос на генерацию токена. Дальше он может делать авторизованные запросы на сервер используя токен. Как правило, токен помещают в header запроса для удобства передачи и считывания.
При генерации токена на сервере в него помещают при надобности данные о сессии пользователя или другую необходимую информацию, шифруют с помощью алгоритмов шифрования и секретного ключа. Ведь потом токен нужно будет расшифровать когда пользователь будет делать запрос. Секретный ключ должен быть хорошо защищен. Ведь Вам не нужно чтобы данные с токена могли получить злоумышленники или другие лица.
Теперь перейдем к примеру. Для этого нам понадобиться простое Spring Boot приложение с REST API, которое мы попробуем засекюрить с помощью jwt и Spring Security.
Для того, чтобы иметь возможность использовать Spring Security нужно подключить зависимость в наш проект:
<dependency> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
После того, как необходимые зависимости будут загружены – Вы сможете использовать любой класс или метод который относится к Spring Security. Полный вид файла pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.6.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.java-master</groupId>
<artifactId>spring-security-jwt</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-security-jwt</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
Чтобы хранить данные об юзерах я создал таблицу user_table которая будет хранить логин, пароль и ссылку на роль пользователя. Роли мы будем хранить в отдельной таблице: role_table
В данном примере у меня будет две роли: АДМИН и ЮЗЕР. Полный вид моих скриптов для PostgreSQL:
-- auto-generated definition
create table role_table
(
id serial not null
constraint role_table_pk
primary key,
name varchar(20) not null
);
alter table role_table
owner to postgres;
-- auto-generated definition
create table user_table
(
id serial not null
constraint user_table_pk
primary key,
login varchar(50),
password varchar(500),
role_id integer
constraint user_table_role_table_id_fk
references role_table
);
alter table user_table
owner to postgres;
create unique index user_table_login_uindex
on user_table (login);
insert into role_table(name) values ('ROLE_ADMIN');
insert into role_table(name) values ('ROLE_USER');
После того как я создал базу данных, выполнил скрипты для создания таблицы, я добавил в таблицу ролей две записи: ROLE_ADMIN и ROLE_USER. Почему имена ролей у меня с префиксом ROLE_? Дело в том, что когда я буду использовать методы из класс настроек Spring секюрити – то в реализации этих методов спринг конкатенирует ROLE_ к имени роли. И поэтому, чтобы роли совпадали я и добавил префикс изначально к моим ролям, чтобы не добавлять потом при настройке. Вы увидите где это используется дальше в примере.
После приготовлений с базой и каркасом проекта, я пишу стандартные классы сущности и интерфейсы репозитории для доступа к базе данных.
В интерфейсе RoleEntityRepository я добавил один метод, который позволит мне находить роли по имени. Чтобы потом при сохранении пользователя я смог задать ему определенную роль.
package com.javamaster.springsecurityjwt.repository;
import com.javamaster.springsecurityjwt.entity.RoleEntity;
import org.springframework.data.jpa.repository.JpaRepository;
public interface RoleEntityRepository extends JpaRepository<RoleEntity, Integer> {
RoleEntity findByName(String name);
}
Для таблицы пользователей я добавил только метод, который будет доставать пользователя по логину. Этот метод мне будет нужен в дальнейшем для авторизации пользователя:
package com.javamaster.springsecurityjwt.repository;
import com.javamaster.springsecurityjwt.entity.UserEntity;
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserEntityRepository extends JpaRepository<UserEntity, Integer> {
UserEntity findByLogin(String login);
}
Код я даю только для того, чтобы Вы могли ориентироваться на какой мы сейчас стадии. В конце статьи я дам ссылку на проект, где Вы уже сможете посмотреть при надобности весь код полностью.
Дальше я создаю UserService в котором буду сохранять и авторизировать пользователей:
package com.javamaster.springsecurityjwt.service;
import com.javamaster.springsecurityjwt.entity.RoleEntity;
import com.javamaster.springsecurityjwt.entity.UserEntity;
import com.javamaster.springsecurityjwt.repository.RoleEntityRepository;
import com.javamaster.springsecurityjwt.repository.UserEntityRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
@Service
public class UserService {
@Autowired
private UserEntityRepository userEntityRepository;
@Autowired
private RoleEntityRepository roleEntityRepository;
@Autowired
private PasswordEncoder passwordEncoder;
public UserEntity saveUser(UserEntity userEntity) {
RoleEntity userRole = roleEntityRepository.findByName("ROLE_USER");
userEntity.setRoleEntity(userRole);
userEntity.setPassword(passwordEncoder.encode(userEntity.getPassword()));
return userEntityRepository.save(userEntity);
}
public UserEntity findByLogin(String login) {
return userEntityRepository.findByLogin(login);
}
public UserEntity findByLoginAndPassword(String login, String password) {
UserEntity userEntity = findByLogin(login);
if (userEntity != null) {
if (passwordEncoder.matches(password, userEntity.getPassword())) {
return userEntity;
}
}
return null;
}
}
Код выше предельно простой. Я только немного его прокомментирую. В методе saveUser мне на вход придет объект UserEntity в которого будут поля логин и пароль. Перед сохранением мне нужно сделать две вещи: добавить роль и закодировать пароль.
Для первого задания я решил что буду задавать всем пользователям, которые будут проходить через метод saveUser пользовательскую роль. Админов можно будет назначить напрямую через базу данных. Я иду в таблицу ролей и достаю роль по имени. Так как я в процессе подготовки уже добавил запись с именем ROLE_USER, мне найдет мою сущность и я ее добавляю к юзеру.
Для того, чтобы закодировать пароль, я воспользовался интерфейсом PasswordEncoder с реализацией BCryptPasswordEncoder. Это все из библиотеки Spring Security. Чтобы не создавать переменную через new я решил создать бин PasswordEncoder в классе настроек спринг секюрити:
package com.javamaster.springsecurityjwt.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
public class SecurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
Пока данный класс имеет только один бин, но потом я добавлю в него настройки по секюрити.
В методе findByLoginAndPassword я сначала достаю пользователя по логину а потом проверяю совпадает ли пароль в базе с паролем, который пришел на вход метода.
Дальше, чтобы протестировать мою секюрити я создал простой рест контроллер который возвращает приветствие по запросу для пользователя и админа:
package com.javamaster.springsecurityjwt.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class TestSecurityController {
@GetMapping("/admin/get")
public String getAdmin() {
return "Hi admin";
}
@GetMapping("/user/get")
public String getUser() {
return "Hi user";
}
}
Теперь, пришла очередь настроить авторизацию и защиту для наших эндпоинтов.
Для этого вернемся в наш класс SecurityConfig. Я навешал на него аннотацию @EnableWebSecurity, которая позволяет указывает что данный класс является классом настроек Spring Security.
Я также унаследовал данный класс от WebSecurityConfigurerAdapter. Если кратко – данный класс позволяет настроить всю систему секюрити и авторизации под свои нужды.
Я переопределил только метод configure(HttpSecurity http). В результате мой класс настроек приобрел вид:
package com.javamaster.springsecurityjwt.config;
import com.javamaster.springsecurityjwt.config.jwt.JwtFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtFilter jwtFilter;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.httpBasic().disable()
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/admin/*").hasRole("ADMIN")
.antMatchers("/user/*").hasRole("USER")
.antMatchers("/register", "/auth").permitAll()
.and()
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
Сначала я отключил csrf и httpBasic потому что они включены по умолчанию если Вы унаследуетесь от WebSecurityConfigurerAdapter.
Дальше я указал настройку для сессии: sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) которая будет управлять сессией юзера в системе спринг секюрити. Так как я буду авторизировать пользователя по токену, мне не нужно создавать и хранить для него сессию. Поэтому я указал STATELESS.
Дальше, я указываю что все запросы должны проходить через Spring Security: authorizeRequests().
И потом я использую antMatchers чтобы указать какие урл адреса будут доступны для определенной роли, а какие нет. Я также указал что адреса register и auth будут доступны всем независимо от роли и авторизации. Поскольку эти адреса будут служить для регистрации и авторизации пользователей.
Когда я настроил все адреса и доступа мне нужно чтобы спринг как-то увидел пользователя в системе. Для этого я указываю ему фильтр, который будет срабатывать при каждом запросе (addFilterBefore). В этом фильтре я сделаю логику, которая будет доставать данные из токена, получать юзера из базы данных и сохранять данные пользователя и его роли в Spring Security, чтобы спринг мог дальше выполнять свою работу и определять доступен ли определенный адрес для пользователя.
Чтобы манипулировать данными которые будут помещены в токен – мне сначала нужно создать логику по созданию этого самого токена, его валидации, извлечению данных пользователя из токена.
Для этого я создаю класс JwtProvider – обычный компонент для работы с jwt token. Я воспользуюсь библиотекой jsonwebtoken, функционал которой позволяет очень удобно генерировать защищенные токены и потом успешно и очень просто их расшифровывать.
Для этого я добавляю в мевен зависимость:
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
После этого мне доступен функционал библиотеки. Сначала я создаю метод generateToken, на вход которого будет приходить логин пользователя а на выходе будет строка jwt:
public String generateToken(String login) {
Date date = Date.from(LocalDate.now().plusDays(15).atStartOfDay(ZoneId.systemDefault()).toInstant());
return Jwts.builder()
.setSubject(login)
.setExpiration(date)
.signWith(SignatureAlgorithm.HS512, jwtSecret)
.compact();
}
Как видите на примере выше, я использую Jwts.builder() конструкцию которая позволяет создать этот самый токен. в setSubject я добавил логин пользователя, чтобы потом его оттуда забрать в фильтре, когда пользователь будет делать запрос. setExpiration – я указал 15 дней. В случае если пройдет 15 дней и токен не обновить – будет выброшено сообщение об ошибке в методе validateToken, который будет описан ниже. signWith – принимает на вход алгоритм подписи и кодовое слово, которое потом потребуется для расшифровки. Я сделал поле jwtSecret в файле настроект и поместил его в класс с помощью
@Value("$(jwt.secret)")
private String jwtSecret;
конструкции. Мой файл настроек (application.properties) выглядит следующим образом:
server.port=8087
spring.datasource.url=jdbc:postgresql://localhost:5432/spring_security_jwt
spring.datasource.username=postgres
spring.datasource.password=postgres
jwt.secret=javamaster
Конечно, хранить ключ расшифровки и шифровки токена в файле настроек не самая лучшая идея. Особенно если Вам дорога безопасность Ваших пользователей. Данный ключ, как и пароли к базам данных как правило хранят в специальных хранилищах. Например AWS Secret Manager.
Метод validateToken о котором я писал выше выглядит следующим образом:
public boolean validateToken(String token) {
try {
Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token);
return true;
} catch (ExpiredJwtException expEx) {
log.severe("Token expired");
} catch (UnsupportedJwtException unsEx) {
log.severe("Unsupported jwt");
} catch (MalformedJwtException mjEx) {
log.severe("Malformed jwt");
} catch (SignatureException sEx) {
log.severe("Invalid signature");
} catch (Exception e) {
log.severe("invalid token");
}
return false;
}
Метод parseClaimsJws может выбрасывать очень подробные типы исключений, которые Вы можете обработать соответствующим образом. Я вывожу в логи сообщение об ошибке и возвращаю false если валидация прошла с ошибкой.
Чтобы получить информацию о логине пользователя я написал метод getLoginFromToken:
public String getLoginFromToken(String token) {
Claims claims = Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token).getBody();
return claims.getSubject();
}
При генерации токена я в Subject сохранял токен – значит, если токен будет валидный в нем будет логин. Полный вид моего класса JwtProvider:
package com.javamaster.springsecurityjwt.config.jwt;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.SignatureException;
import io.jsonwebtoken.UnsupportedJwtException;
import lombok.extern.java.Log;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.Date;
@Component
@Log
public class JwtProvider {
@Value("$(jwt.secret)")
private String jwtSecret;
public String generateToken(String login) {
Date date = Date.from(LocalDate.now().plusDays(15).atStartOfDay(ZoneId.systemDefault()).toInstant());
return Jwts.builder()
.setSubject(login)
.setExpiration(date)
.signWith(SignatureAlgorithm.HS512, jwtSecret)
.compact();
}
public boolean validateToken(String token) {
try {
Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token);
return true;
} catch (ExpiredJwtException expEx) {
log.severe("Token expired");
} catch (UnsupportedJwtException unsEx) {
log.severe("Unsupported jwt");
} catch (MalformedJwtException mjEx) {
log.severe("Malformed jwt");
} catch (SignatureException sEx) {
log.severe("Invalid signature");
} catch (Exception e) {
log.severe("invalid token");
}
return false;
}
public String getLoginFromToken(String token) {
Claims claims = Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token).getBody();
return claims.getSubject();
}
}
После того как я закончил с настройкой ролей, доступов и токенов, я приступаю к имплементации UserDetails и UserDetailsService интерфейсов из пакета org.springframework.security.core.userdetails.
Для чего это нужно? Как я описал выше мне нужно найти пользователя в базе данных и положить его и его роли в Spring Security контекст. Но я не могу положить какой попало объект. Спринг будет понимать только объекты из “своего круга”. Поэтому мне нужно будет достать моего пользователя из базы данных и “перегнать” его в объект который будет подходящий для Spring Security. А это и есть UserDetails. UserDetailsService будет служить вспомогательным классом для этой цели.
Я создаю CustomUserDetails который имплементирую от UserDetails. Все методы которые по дефолту нужно заимплементить я переделаю под своего пользователя. Дальше я добавляю в этот класс поля логин и пароль. Добавляю поле коллекции GrantedAuthority – это интерфейс для доступов пользователя. Одна из его имплементаций – SimpleGrantedAuthority в которую можно добавить только имя роли и Spring будет давать доступ если имя роли будет совпадать с именем роли в методе hasRole (код выше). Только здесь нужно быть внимательным. Если перейти в реализацию hasRole то можно увидеть что спринг добавляет к имени роли префикс ROLE_. Поэтому, когда вы указываете например .antMatchers(“/admin/*”).hasRole(“ADMIN”) необходимо передавать в SimpleGrantedAuthority имя роли с префиксом ROLE_. Я например сделал роли пользователей изначально с префиксом ROLE_ чтобы не добавлять потом при передачи имени роли в SimpleGrantedAuthority.

Также в класс CustomUserDetails я добавил метод который конвертирует моего пользователя из базы данных в объект CustomUserDetails. В результате всех манипуляций мой код приобрел вид:
package com.javamaster.springsecurityjwt.config;
import com.javamaster.springsecurityjwt.entity.UserEntity;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.Collections;
public class CustomUserDetails implements UserDetails {
private String login;
private String password;
private Collection<? extends GrantedAuthority> grantedAuthorities;
public static CustomUserDetails fromUserEntityToCustomUserDetails(UserEntity userEntity) {
CustomUserDetails c = new CustomUserDetails();
c.login = userEntity.getLogin();
c.password = userEntity.getPassword();
c.grantedAuthorities = Collections.singletonList(new SimpleGrantedAuthority(userEntity.getRoleEntity().getName()));
return c;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return grantedAuthorities;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return login;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
Конечно Вы можете настроить данный класс под свои нужды. Например добавить поле в базу данных к таблице пользователей, которое будет отмечать активный ли пользователь или нет. А потом передавать это поле в метод isAccountNonLocked. Но я ограничился только логином и паролем и поэтому переделал чтобы все методы возвращали что с моим пользователем все с порядке.
Дальше, я создаю класс CustomUserDetailsService который будет реализовывать UserDetailsService интерфейс. В данном интерфейсе только один метод loadUserByUsername. В этом методе я достаю из базы данных моего юзера по логину, конвертирую его в CustomUser и возвращаю:
package com.javamaster.springsecurityjwt.config;
import com.javamaster.springsecurityjwt.entity.UserEntity;
import com.javamaster.springsecurityjwt.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;
@Component
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UserService userService;
@Override
public CustomUserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserEntity userEntity = userService.findByLogin(username);
return CustomUserDetails.fromUserEntityToCustomUserDetails(userEntity);
}
}
Приступаем к фильтру. Я сделал обычный класс который унаследовал от GenericFilterBean. По сути дела, GenericFilterBean обычная базовая имплементация javax.servlet.Filter который Вы наверняка использовали для фильтрации запросов на сервлет.
После наследования у нас доступен один метод для определения: doFilter.
Как Вы могли догадаться, именно этот метод будет срабатывать при работе фильтра.
Первое что мне нужно сделать – достать токен из запроса. Я ожидаю что токен будет приходить в хедере с ключом “Authorization”. Я могу достать значение хидера из запроса. После извлечения токена мне нужно проверить чтобы он начинался со слова Bearer (носитель с англ.). Почему именно с Bearer? Такой стандарт RFC6750 для токенов с которым более подробно можно ознакомиться по ссылке.
После я извлекаю нужную мне информацию из токена. Изначально я зашифрую в него логин пользователя, чтобы потом вынуть логин из токена и найти пользователя в базе данных.
Если пользователь найден я передаю его в контекст Spring Security для того чтобы пользователь был проверен на доступа и смог продолжить работу с запросом. Мой код фильтра выглядит следующим образом:
package com.javamaster.springsecurityjwt.config.jwt;
import com.javamaster.springsecurityjwt.config.CustomUserDetails;
import com.javamaster.springsecurityjwt.config.CustomUserDetailsService;
import lombok.extern.java.Log;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.GenericFilterBean;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import static org.springframework.util.StringUtils.hasText;
@Component
@Log
public class JwtFilter extends GenericFilterBean {
public static final String AUTHORIZATION = "Authorization";
@Autowired
private JwtProvider jwtProvider;
@Autowired
private CustomUserDetailsService customUserDetailsService;
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
logger.info("do filter...");
String token = getTokenFromRequest((HttpServletRequest) servletRequest);
if (token != null && jwtProvider.validateToken(token)) {
String userLogin = jwtProvider.getLoginFromToken(token);
CustomUserDetails customUserDetails = customUserDetailsService.loadUserByUsername(userLogin);
UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(customUserDetails, null, customUserDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(auth);
}
filterChain.doFilter(servletRequest, servletResponse);
}
private String getTokenFromRequest(HttpServletRequest request) {
String bearer = request.getHeader(AUTHORIZATION);
if (hasText(bearer) && bearer.startsWith("Bearer ")) {
return bearer.substring(7);
}
return null;
}
}
В строках
UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(customUserDetails, null, customUserDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(auth);
Я создаю объект UsernamePasswordAuthenticationToken из библиотеки спринг секюрити и после передаю этот объект в SecurityContextHolder.
Вот и все что нужно сделать для того, чтобы настроить Spring Security с помощью jwt токена. Осталось только дописать контроллер в котором будет 2 метода: регистрация и авторизация – генерация токена для пользователя.
package com.javamaster.springsecurityjwt.controller;
import com.javamaster.springsecurityjwt.config.jwt.JwtProvider;
import com.javamaster.springsecurityjwt.entity.UserEntity;
import com.javamaster.springsecurityjwt.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid;
@RestController
public class AuthController {
@Autowired
private UserService userService;
@Autowired
private JwtProvider jwtProvider;
@PostMapping("/register")
public String registerUser(@RequestBody @Valid RegistrationRequest registrationRequest) {
UserEntity u = new UserEntity();
u.setPassword(registrationRequest.getPassword());
u.setLogin(registrationRequest.getLogin());
userService.saveUser(u);
return "OK";
}
@PostMapping("/auth")
public AuthResponse auth(@RequestBody AuthRequest request) {
UserEntity userEntity = userService.findByLoginAndPassword(request.getLogin(), request.getPassword());
String token = jwtProvider.generateToken(userEntity.getLogin());
return new AuthResponse(token);
}
}
В регистрации мы на вход принимаем логин и пароль пользователя и вызываем метод saveUser из класса UserService.
В авторизации мы сначала находим пользователя по логину и паролю. Если такой пользователь найден – генерируем ему токен и возвращаем. В данном примере я не стал усложнять логику этих методов. По желанию можно накрутить и исключения и дополнительную валидацию. Это конечно же нужно будет добавить если Вы решите использовать этот пример дальше чем просто тестовый пример Spring Security.
Теперь, осталось только запустить приложение и посмотреть как оно работает. Для тестирования моего REST API я воспользуюсь инструментом Postman.
Первый шаг – регистрация пользователя:

Дальше – авторизация:

Здесь мы получили токен с помощью которого сможем делать запросы на сервер. Помним, что мы сохраняем польователя с ролью ЮЗЕР. Поэтому сначала попробуем вызвать урл который доступен для соответствующей роли. Не забудьте скопировать полученный токен и вставить его в авторизацию:

Если мы попытаемся сделать запрос с этим токеном на урл который доступен только пользователям с ролью АДМИН – мы получим ошибку:

Наши запросы теперь защищены от несанкционированного доступа.
Ссылка на проект: https://github.com/caligula95/spring-security-jwt
Видео к статье:
Ссылка на телеграм, где я публикую интересные заметки и анонсы статей: https://t.me/java_master
Здравствуйте, будет ли урок о том, как реализовать refresh-токен таким же способом?
привет! Спасибо!
было бы круто реализовать с formlogin.
там оказіваеться не все там тревиально. Делал на jQuery/Ajax
Так же не заметил где мы сетим в header наш Bearer + token
Когда мы в постмане выбрали пункт Authorisation Bearer от добавил в хедер нашего запроса токен с приставкой Bearer
Здравствуйте. С форм логин нужно просто поменять рест ендпоинт чтобы он принимал не json а form/data
Как отключить проверку токена для некоторых запросов?
http
.httpBasic().disable()
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers(“/api/v1/auth”, “/api/v1/form-employment”, “/api/v1/form-order”).permitAll()
.anyRequest().hasRole(“ADMIN”)
.and()
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
Фильтр все равно не пропускает запросы, помеченные как permitAll
Здравствуйте! Попробуйте поменять местами пермиты. Чтобы сначала с ролью админ а потом открытые. Также убедитесь что Вы правильно прописали паттерн для открытых урл.
уважаемый, отличная статья.
но где про refresh token?
Что именно Вы хотите знать о refresh token?)
ну заекспайрился токен. вызывайте метод auth еще раз и будет Вам свежий токен))
“`При генерации токена я в Subject ложил токен — значит, если токен будет валидный в нем будет логин.“`
ложил ЛОГИН
Спасибо за статью. У меня не работает фильтр
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.httpBasic().disable()
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers(“/rest/admin/*”).hasRole(“ADMIN”)
.antMatchers(“/rest/user/*”).hasRole(“USER”)
.and()
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
}
Хотя в списке есть:
Security filter chain: [
WebAsyncManagerIntegrationFilter
SecurityContextPersistenceFilter
HeaderWriterFilter
LogoutFilter
JwtFilter
RequestCacheAwareFilter
SecurityContextHolderAwareRequestFilter
AnonymousAuthenticationFilter
SessionManagementFilter
ExceptionTranslationFilter
FilterSecurityInterceptor
]
При запросах серверу без разницы есть токен, нет токена…
В чем может быть причина?
Заработало: заменил * на две ** и addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class) на addFilterBefore(jwtFilter, AnonymousAuthenticationFilter.class)
Вопрос вдогонку первому: почему нет в списке фильтров UsernamePasswordAuthenticationFilter?
Всем привет, у меня выскакивает ошибка при компиляции “The dependencies of some of the beans in the application context form a cycle:” между файлами jwtFilter,
customUserDetailsService, userService, securityConfig
Может кто подскажет, как ее тут решить, я брала кусочки кода и под себя делала, появилась данная ошибка, но потом взяла все под копирку, чтобы найти свою ошибку и убедиться, что тут код рабочий и все равно выскакивает данное предупреждение
навесьте аннотацию @Lazy на один из классов. по крайней мере, у меня заработало