Spring Security с помощью JWT токена

Spring Boot Security JWT

Сегодня я расскажу как сделать Ваши 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

Видео к статье:

6

  1. Как отключить проверку токена для некоторых запросов?
    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

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

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