Як налаштувати авторизацію та автентифікацію Java Spring Boot JWT

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

У цьому дописі я спробую пояснити, чому я навчився та застосував у своєму проекті, щоб поділитися своїм досвідом та, сподіваюся, допомогти деяким людям.

Ми почнемо з короткого огляду теорії JWT та її роботи. Потім ми розглянемо, як це реалізувати у програмі Spring Boot.

Основи JWT

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

У процесі автентифікації JWT фронт-енд (клієнт) спочатку надсилає деякі облікові дані для автентифікації (ім’я користувача та пароль у нашому випадку, оскільки ми працюємо над веб-додатком).

Потім сервер (у нашому випадку додаток Spring) перевіряє ці облікові дані, і якщо вони дійсні, генерує JWT і повертає їх.

Після цього кроку клієнт повинен надати цей маркер у заголовку авторизації запиту у формі «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(); }

Для хеша пароля, ми визначимо BCrypt боб в @SpringBootApplication і анотування основного класу наступним чином :

Ми будемо викликати методи цього компонента, коли нам потрібно хешувати пароль.

Нам також потрібен UserController для збереження користувачів. Ми створюємо контролер, коментуємо його @RestController і визначаємо відповідне відображення.

У нашому додатку ми зберігаємо користувача на основі об'єкта DTO, який передається з інтерфейсу. Ви також можете передати об'єкт User у @RequestBody .

Після передачі об’єкта DTO ми зашифровуємо поле пароля за допомогою компонента 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 . Ви можете використовувати цю функцію, щоб залишатися в курсі ваших кінцевих точок.

Ми скасовуємо attemptAuthentication і successfulAuthentication методу UsernameAuthenticationFilter класу.

Функція tryAuthentication запускається, коли користувач намагається увійти в наш додаток. Він читає облікові дані, створює з них користувача POJO, а потім перевіряє облікові дані для автентифікації.

Ми передаємо ім’я користувача, пароль та порожній список. Порожній список представляє повноваження (ролі), і ми залишаємо його таким, як є, оскільки в нашому додатку ще немає ролей.

Якщо автентифікація успішна, запускається метод успішної автентифікації . Параметри цього методу передаються Spring Security за кадром.

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. :)