Как настроить авторизацию и аутентификацию Java Spring Boot JWT

В прошлом месяце у меня была возможность реализовать JWT-аутентификацию для побочного проекта. Раньше я работал с JWT в Ruby on Rails, но весной это был мой первый раз.

В этом посте я попытаюсь объяснить, что я узнал и применил в своем проекте, чтобы поделиться своим опытом и, надеюсь, помочь некоторым людям.

Мы начнем с краткого обзора теории, лежащей в основе JWT, и того, как она работает. Затем мы рассмотрим, как реализовать его в приложении Spring Boot.

Основы JWT

JWT или JSON Web Tokens (RFC 7519) - это стандарт, который в основном используется для защиты REST API. Несмотря на то, что это относительно новая технология, она быстро набирает популярность.

В процессе аутентификации JWT внешний интерфейс (клиент) сначала отправляет некоторые учетные данные для аутентификации (в нашем случае имя пользователя и пароль, поскольку мы работаем над веб-приложением).

Затем сервер (в нашем случае приложение Spring) проверяет эти учетные данные и, если они действительны, генерирует JWT и возвращает его.

После этого шага клиент должен предоставить этот токен в заголовке авторизации запроса в форме «Bearer TOKEN». Серверная часть проверяет действительность этого токена и разрешает или отклоняет запросы. Маркер может также хранить роли пользователей и разрешать запросы на основе данных полномочий.

Реализация

Теперь давайте посмотрим, как мы можем реализовать механизм входа и сохранения JWT в реальном приложении Spring.

Зависимости

Вы можете увидеть список зависимостей Maven, которые использует наш пример кода ниже. Обратите внимание, что основные зависимости, такие как Spring Boot и Hibernate, не включены в этот снимок экрана.

Сохранение пользователей

Мы начнем с создания контроллеров для безопасного сохранения пользователей и их аутентификации на основе имени пользователя и пароля.

У нас есть модельная сущность под названием User. Это простой класс сущности, который отображается в таблицу USER . Вы можете использовать любые свойства, которые вам нужны, в зависимости от вашего приложения.

У нас также есть простой класс UserRepository для сохранения пользователей. Нам нужно переопределить метод findByUsername, поскольку мы будем использовать его при аутентификации.

public interface UserRepository extends JpaRepository{ User findByUsername(String username); }

Мы никогда не должны хранить пароли в открытом виде в базе данных, потому что многие пользователи, как правило, используют один и тот же пароль для нескольких сайтов.

Существует множество различных алгоритмов хеширования, но наиболее часто используется BCrypt, который является рекомендуемым методом безопасного хеширования. Вы можете проверить эту статью для получения дополнительной информации по теме.

@Bean public BCryptPasswordEncoder bCryptPasswordEncoder() { return new BCryptPasswordEncoder(); }

Чтобы хэшировать пароль, мы определим bean- компонент BCrypt в @SpringBootApplication и аннотируем основной класс следующим образом:

Мы будем вызывать методы этого bean-компонента, когда нам нужно хешировать пароль.

Нам также нужен UserController для сохранения пользователей. Мы создаем контроллер, аннотируем его с помощью @RestController и определяем соответствующее сопоставление.

В нашем приложении мы сохраняем пользователя на основе объекта DTO, который передается из внешнего интерфейса. Вы также можете передать объект User в @RequestBody .

После передачи объекта DTO мы шифруем поле пароля, используя созданный ранее bean BCrypt . Вы также можете сделать это в контроллере, но лучше поместить эту логику в класс обслуживания.

@Transactional(rollbackFor = Exception.class) public String saveDto(UserDto userDto) { userDto.setPassword(bCryptPasswordEncoder.encode(userDto.getPassword())); return save(new User(userDto)).getId(); }

Фильтр аутентификации

Нам нужна аутентификация, чтобы убедиться, что пользователь действительно тот, за кого себя выдают. Для этого мы будем использовать классическую пару имени пользователя и пароля.

Вот шаги для реализации аутентификации:

  1. Создайте наш фильтр аутентификации, который расширяет UsernamePasswordAuthenticationFilter
  2. Создайте класс конфигурации безопасности, который расширяет WebSecurityConfigurerAdapter, и примените фильтр.

Вот код для нашего фильтра аутентификации - как вы, возможно, знаете, фильтры являются основой Spring Security.

Давайте рассмотрим этот код шаг за шагом.

Этот класс расширяет UsernamePasswordAuthenticationFilter, который является классом по умолчанию для аутентификации по паролю в Spring Security. Мы расширяем его, чтобы определить нашу собственную логику аутентификации.

Мы вызываем метод setFilterProcessesUrl в нашем конструкторе. Этот метод устанавливает URL-адрес входа по умолчанию для предоставленного параметра.

Если вы удалите эту строку, Spring Security по умолчанию создаст конечную точку «/ login» . Он определяет для нас конечную точку входа, поэтому мы не будем определять конечную точку входа в нашем контроллере явно.

После этой строки наша конечная точка входа будет / api / services / controller / user / login . Вы можете использовать эту функцию, чтобы соответствовать вашим конечным точкам.

We override the attemptAuthentication and successfulAuthentication methods of the UsernameAuthenticationFilter class.

The attemptAuthentication function runs when the user tries to log in to our application. It reads the credentials, creates a user POJO from them, and then checks the credentials to authenticate.

We pass the username, password, and an empty list. The empty list represents the authorities (roles), and we leave it as is since we do not have any roles in our application yet.

If the authentication is successful, the successfulAuthentication method runs. The parameters of this method are passed by Spring Security behind the scenes.

The attemptAuthentication method returns an Authentication object that contains the authorities we passed while attempting.

We want to return a token to user after authentication is successful, so we create the token using username, secret, and expiration date. We need to define the SECRET and EXPIRATION_DATE now.

We create a class to be a container for our constants. You can set the secret to whatever you want, but the best practice is making the secret key as long as your hash. We use the HS256 algorithm in this example, so our secret key is 256 bits/32 chars.

The expiration time is set to 15 minutes, because it is the best practice against secret key brute-forcing attacks. The time is in milliseconds.

We have prepared our Authentication filter, but it is not active yet. We also need an Authorization filter, and then we will apply them both through a configuration class.

This filter will check the existence and validity of the access token on the Authorization header. We will specify which endpoints will be subject to this filter in our configuration class.

Authorization Filter

The doFilterInternal method intercepts the requests then checks the Authorization header. If the header is not present or doesn’t start with “BEARER”, it proceeds to the filter chain.

If the header is present, the getAuthentication method is invoked. getAuthentication verifies the JWT, and if the token is valid, it returns an access token which Spring will use internally.

This new token is then saved to SecurityContext. You can also pass in Authorities to this token if you need for role-based authorization.

Our filters are ready, and now we need to put them into action with the help of a configuration class.

Configuration

We annotate this class with @EnableWebSecurity and extend WebSecurityConfigureAdapter to implement our custom security logic.

We autowire the BCrypt bean that we defined earlier. We also autowire the UserDetailsService to find the user’s account.

The most important method is the one which accepts an HttpSecurity object. Here we specify the secure endpoints and filters that we want to apply. We configure CORS, and then we permit all post requests to our sign up URL that we defined in the constants class.

You can add other ant matchers to filter based on URL patterns and roles, and you can check this StackOverflow question for examples regarding that. The other method configures the AuthenticationManager to use our encoder object as its password encoder while checking the credentials.

Testing

Let’s send a few requests to test if it works properly.

Here we send a GET request to access a protected resource. Our server responds with a 403 code. This is the expected behavior because we haven’t provided a token in the header. Now let’s create a user:

To create a user, we send a post request with our User DTO data. We will use this user to login and get an access token.

Great! We got the token. After this point, we will use this token to access protected resources.

We provide the token in the Authorization header and we are now allowed access to our protected endpoint.

Conclusion

In this tutorial I have walked you through the steps I took when implementing JWT authorization and password authentication in Spring. We also learned how to save a user securely.

Thank you for reading – I hope it was helpful to you. If you are interested in reading more content like this, feel free to subscribe to my blog at //erinc.io. :)