Регистрация и авторизация в php с json web token
Содержание:
Использование в клиент-серверных приложениях
Access и refresh токены
- Access-токен — это токен, который предоставляет доступ его владельцу к защищенным ресурсам сервера. Обычно он имеет короткий срок жизни и может нести в себе дополнительную информацию, такую как IP-адрес стороны, запрашивающей данный токен.
- Refresh-токен — это токен, позволяющий клиентам запрашивать новые access-токены по истечении их времени жизни. Данные токены обычно выдаются на длительный срок.
Схема работы
Как правило, при использовании JSON токенов в клиент-серверных приложениях реализована следующая схема:
- Клиент проходит аутентификацию в приложении (к примеру, с использованием логина и пароля)
- В случае успешной аутентификации, сервер отправляет клиенту access- и refresh-токены.
- При дальнейшем обращении к серверу, клиент использует access-токен. Сервер проверяет токен на валидность и предоставляет клиенту доступ к ресурсам
- В случае, если access-токен становится не валидным, клиент отправляет refresh-токен, в ответ на который сервер предоставляет два обновленных токена.
- В случае, если refresh-токен становится не валидным, клиент опять должен пройти процесс аутентификации (п. 1).
Преимущества
JWT имеет ряд преимуществ над куки:
- При использовании куки сервер должен хранить информацию о выданных сессиях, в то время как использование JWT не требует хранения дополнительных данных о выданных токенах: все, что должен сделать сервер — это проверить подпись.
- Сервер может не заниматься созданием токенов, а предоставить это внешним сервисам.
- В JSON-токенах можно хранить дополнительную полезную информацию о пользователях. Как следствие — более высокая производительность. В случае c куки иногда необходимо осуществлять запросы для получения дополнительной информации. При использовании JWT эта информация может быть передана в самом токене.
- JWT делает возможным предоставление одновременного доступа к различным доменам и сервисам.
What is the JSON Web Token structure?
In its compact form, JSON Web Tokens consist of three parts separated by dots (), which are:
- Header
- Payload
- Signature
Therefore, a JWT typically looks like the following.
Let’s break down the different parts.
Header
The header typically consists of two parts: the type of the token, which is JWT, and the signing algorithm being used, such as HMAC SHA256 or RSA.
For example:
Then, this JSON is Base64Url encoded to form the first part of the JWT.
Payload
The second part of the token is the payload, which contains the claims. Claims are statements about an entity (typically, the user) and additional data.
There are three types of claims: registered, public, and private claims.
-
: These are a set of predefined claims which are not mandatory but recommended, to provide a set of useful, interoperable claims. Some of them are: iss (issuer), exp (expiration time), sub (subject), aud (audience), and .
-
: These can be defined at will by those using JWTs. But to avoid collisions they should be defined in the IANA JSON Web Token Registry or be defined as a URI that contains a collision resistant namespace.
-
: These are the custom claims created to share information between parties that agree on using them and are neither registered or public claims.
An example payload could be:
The payload is then Base64Url encoded to form the second part of the JSON Web Token.
Signature
To create the signature part you have to take the encoded header, the encoded payload, a secret, the algorithm specified in the header, and sign that.
For example if you want to use the HMAC SHA256 algorithm, the signature will be created in the following way:
The signature is used to verify the message wasn’t changed along the way, and, in the case of tokens signed with a private key, it can also verify that the sender of the JWT is who it says it is.
Putting all together
The output is three Base64-URL strings separated by dots that can be easily passed in HTML and HTTP environments, while being more compact when compared to XML-based standards such as SAML.
The following shows a JWT that has the previous header and payload encoded, and it is signed with a secret.

If you want to play with JWT and put these concepts into practice, you can use to decode, verify, and generate JWTs.

Why should we use JSON Web Tokens?
Let’s talk about the benefits of JSON Web Tokens (JWT) when compared to Simple Web Tokens (SWT) and Security Assertion Markup Language Tokens (SAML).
As JSON is less verbose than XML, when it is encoded its size is also smaller, making JWT more compact than SAML. This makes JWT a good choice to be passed in HTML and HTTP environments.
Security-wise, SWT can only be symmetrically signed by a shared secret using the HMAC algorithm. However, JWT and SAML tokens can use a public/private key pair in the form of a X.509 certificate for signing. Signing XML with XML Digital Signature without introducing obscure security holes is very difficult when compared to the simplicity of signing JSON.
JSON parsers are common in most programming languages because they map directly to objects. Conversely, XML doesn’t have a natural document-to-object mapping. This makes it easier to work with JWT than SAML assertions.
Regarding usage, JWT is used at Internet scale. This highlights the ease of client-side processing of the JSON Web token on multiple platforms, especially mobile.
Comparison of the length of an encoded JWT and an encoded SAML
If you want to read more about JSON Web Tokens and even start using them to perform authentication in your own applications, browse to the JSON Web Token landing page at Auth0.
Problem
Authorization is one of the first problems developers face upon starting a new project. And one of the most common types of authorization (from my experience) is the token-based authorization (usually using JWT).
From my perspective, this article looks like «what I wanted to read two weeks ago». My goal was to write minimalistic and reusable code with a clean and straightforward interface. I had the next requirements for my implementation of the auth management:
- Tokens should be stored in local storage
- Tokens should be restored on page reload
- Access token should be passed in the network requests
- After expiration access token should be updated by refresh token if the last one is presented
- React components should have access to the auth information to render appropriate UI
- The solution should be made with pure React (without Redux, thunk, etc..)
For me one of the most challenging questions were:
- How to keep in sync React components state and local storage data?
- How to get the token inside fetch without passing it through the whole elements tree (especially if we want to use this fetch in thunk actions later for example)
But let’s solve the problems step by step. Firstly we will create a to store tokens and provide possibility to listen to changes. After that, we will create an , actually wrapper around to create hooks for React components, fetch on steroids and some additional methods. And in the end, we will look at how to use this solution in the project.
Структура
Токен JWT состоит из трех частей: заголовка (header), полезной нагрузки (payload) и подписи или данных шифрования. Первые два элемента — это JSON объекты определенной структуры. Третий элемент вычисляется на основании первых и зависит от выбранного алгоритма (в случае использования не подписанного JWT может быть опущен). Токены могут быть перекодированы в компактное представление (JWS/JWE Compact Serialization): к заголовку и полезной нагрузке применяется алгоритм кодирования Base64-URL, после чего добавляется подпись и все три элемента разделяются точками («.»).
К примеру, для заголовка и полезной нагрузки, которые выглядят следующим образом:
{
"alg" "HS512",
"typ" "JWT"
}
{
"sub" "12345",
"name" "John Gold",
"admin" true
}
Получим следующее компактное представление (переводы строки добавлены для наглядности):
eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9. eyJzdWIiOiIxMjM0NSIsIm5hbWUiOiJKb2huIEdvbGQiLCJhZG1pbiI6dHJ1ZX0K. LIHjWCBORSWMEibq-tnT8ue_deUqZx1K0XxCOXZRrBI
Заголовок
В заголовке указывается необходимая информация для описания самого токена.
Обязательный ключ здесь только один:
alg: алгоритм, используемый для подписи/шифрования (в случае не подписанного JWT используется значение «none»).
Необязательные ключи:
- typ: тип токена (type). Используется в случае, когда токены смешиваются с другими объектами, имеющими JOSE заголовки. Должно иметь значение «JWT».
- cty: тип содержимого (content type). Если в токене помимо зарегистрированных служебных ключей есть пользовательские, то данный ключ не должен присутствовать. В противном случае должно иметь значение «JWT»
Полезная нагрузка
В данной секции указывается пользовательская информация (например, имя пользователя и уровень его доступа), а также могут быть использованы некоторые служебные ключи. Все они являются необязательными:
- iss: чувствительная к регистру строка или URI, которая является уникальным идентификатором стороны, генерирующей токен (issuer).
- sub: чувствительная к регистру строка или URI, которая является уникальным идентификатором стороны, о которой содержится информация в данном токене (subject). Значения с этим ключом должны быть уникальны в контексте стороны, генерирующей JWT.
- aud: массив чувствительных к регистру строк или URI, являющийся списком получателей данного токена. Когда принимающая сторона получает JWT с данным ключом, она должна проверить наличие себя в получателях — иначе проигнорировать токен (audience).
- exp: время в формате Unix Time, определяющее момент, когда токен станет не валидным (expiration).
- nbf: в противоположность ключу exp, это время в формате Unix Time, определяющее момент, когда токен станет валидным (not before).
- jti: строка, определяющая уникальный идентификатор данного токена (JWT ID).
- iat: время в формате Unix Time, определяющее момент, когда токен был создан. iat и nbf могут не совпадать, например, если токен был создан раньше, чем время, когда он должен стать валидным.
What is JSON Web Token?
JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret (with the HMAC algorithm) or a public/private key pair using RSA or ECDSA.
Although JWTs can be encrypted to also provide secrecy between parties, we will focus on signed tokens. Signed tokens can verify the integrity of the claims contained within it, while encrypted tokens hide those claims from other parties. When tokens are signed using public/private key pairs, the signature also certifies that only the party holding the private key is the one that signed it.
Конечная точка аутентификации для выдачи JWT-токена
Тут собственно выдается JWT-токен. Пользователь делает POST-запрос с именем и паролем по адресу /authenticate, а в ответ получает сгенерированынй токен. Токен генерится методом generateToken() из утилиты выше.
@RestController
public class AuthenticationController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JWTUtil jwtTokenUtil;
@PostMapping("/authenticate")
@ResponseStatus(HttpStatus.OK)
public AuthResponse createAuthenticationToken(@RequestBody AuthRequest authRequest){
Authentication authentication;
try {
authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(authRequest.getName(), authRequest.getPassword()));
System.out.println(authentication);
} catch (BadCredentialsException e) {
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "Имя или пароль неправильны", e);
}
final String jwt = jwtTokenUtil.generateToken((UserDetails) authentication.getPrincipal());
System.out.println(jwt);
return new AuthResponse(jwt);
}
}
Запрос имеет такой формат:
public class AuthRequest {
private String name;
private String password;
// геттеры сеттеры
}
А ответ такой:
public class AuthResponse {
private String jwtToken;
// геттер и сеттер
}
Если имя и пароль верные, токен возвращается в AuthResponse, а если нет – выбрасывается исключение и на фронтенд приходит сообщение об ошибке.
Фронтенд сохраняет у себя JWT-токен, и потом использует его при каждом запросе.
Немного о “разлогине”
Если пользователь хочет выйти, токен должен быть уничтожен на фронтенде. На бэкенде (в нашем Spring приложении) он продолжит действовать до истечения своего срока. А вообще теоретически можно сделать черный список токенов, но не в этом примере.
Еще чтобы разлогинить всех пользователей, достаточно поменять секретный код. Но разлогинены будут все сразу.
В этом проблема JWT-токена – нужды обходные пути для его инвалидации.
Перейдем ко второй принципиальной части – фильтру, проверяющему токен при каждом запросе.
Фильтр, проверяющий JWT-токен при каждом запросе
Итак, JWT-токен выдан, клиент его нам отправляет при каждом запросе, надо этот токен при каждом запросе проверять (и извлекать из него имя пользователя). Для этого напишем фильтр JWTFilter. Он расширяет OncePerRequestFilter и происходит в нем следующее:
- При каждом запросе из заголовка Authorization берем JWT-токен (он начинается с префикса “Bearer“).
- Извлекаем из него имя пользователя (которое записывали при формировании токена).
- Проверяем подпись (validateToken())
- Если все ок, устанавливаем в SecurityContext объект Authencation (а в Authencation – UserDetails). Так нужно для Spring Security.
- Если с токеном не все ок, то фильтр не пропустит запрос в контроллер к защищенному /url.
@Component
public class JWTFilter extends OncePerRequestFilter {
@Autowired
private JWTUtil jwtUtil;
@Autowired
CustomUserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
final String authorizationHeader = request.getHeader("Authorization");
String username = null;
String jwt = null;
if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
jwt = authorizationHeader.substring(7);
username = jwtUtil.extractUsername(jwt);
}
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
//если подпись неправильная, то SignatureException
if (this.jwtUtil.validateToken(jwt, userDetails)) {
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
usernamePasswordAuthenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
}
}
chain.doFilter(request, response);
}
}
Summary
We did it. It was an interesting journey, but it also has the end (maybe even happy).
We started with the understanding of problems with storing authorization tokens. Then we implemented a solution and finally looked at the examples of how it might be used in the React application.
As I told before, you can find my implementation on GitHub in the library. It solves a bit more generic problem and does not make assumptions about the structure of the object with tokens or how to update the token, so you will need to provide some extra arguments. But the idea of the solution is the same and the repository also contains instructions on how to use it.
Here I can say Thank you for the reading of the article and I hope it was helpful for You.
JWT токен vs Simple Hash-Based Remember-Me токен
JWT-токен (JSON Web Token) похож на Simple Hash-Based Remember-Me токен, рассмотренный в предыдущей статье (только для использования JWT-токена кода потребуется написать больше):
- Выдается он клиенту после успешного ввода имени и пароля.
- С последующими запросами клиент отправляет его серверу в заголовке, сервер проверяет его подлинность путем простой калькуляции. Удостоверяется что клиент есть тот, за кого себя выдает.
- JWT-токен продолжает работать и после того, как сервер перезапущен.
-
JWT-токен работает, если серверов несколько (с балансировщиком нагрузки). На конкретный сервер ничего не завязано: сессий нет, данных, хранящихся в сессии нет.
Есть и отличия:
- В JWT-токене можно хранить больше данных, чем в Simple Hash-Based Remember-Me токене. В JWT-токене есть специальная часть Payload – полезная нагрузка, туда можно записать роли, например. Но можно и обойтись. Обычно достаточно имени пользователя. Ничего секретного в Payload записывать нельзя.
- Чтобы JWT-токен использовать в Spring Security, потребуется написать больше кода, чем с Remember-Me (тут настройка была минимальна).
- Для вычисления подписи в JWT-токене не используется пароль пользователя.
Поскольку сессий нет, статус взаимодействия клиента и сервера хранится на клиенте, а не на сервере (на сервере состояние хранить негде).
Примечания
- (англ.). auth0.com. Дата обращения 11 декабря 2017.
- Bradley, John, Sakimura, Nat, Jones, Michael. (англ.). tools.ietf.org. Дата обращения 20 декабря 2017.
- Bradley, John, Sakimura, Nat, Jones, Michael. (англ.). tools.ietf.org. Дата обращения 20 декабря 2017.
- (англ.). auth0.com. Дата обращения 20 декабря 2017.
- Ryan Boyd. Getting Started with OAuth 2.0. — O’Reilly media, 2012. — С. 56.
- (англ.). auth0.com. Дата обращения 21 декабря 2017.
- Justin Richer and Antonio Sanso. OAuth 2 in Action. — Manning Publications, 2017. — С. 252—253. — 360 с. — ISBN 9781617293276.
- (англ.). auth0.com. Дата обращения 21 декабря 2017.
- (англ.). auth0.com. Дата обращения 21 декабря 2017.
- (англ.). auth0.com. Дата обращения 20 декабря 2017.
- Bradley, John, Sakimura, Nat, Jones, Michael. (англ.). tools.ietf.org. Дата обращения 20 декабря 2017.
- Bradley, John, Sakimura, Nat, Jones, Michael. (англ.). tools.ietf.org. Дата обращения 20 декабря 2017.
- auth0.com. (англ.). jwt.io. Дата обращения 20 декабря 2017.
Задача
Ранее мы писали приложения с формой логина, которую предоставляет Spring Security (SS) по умолчанию. В этой статье так не получится, поскольку по умолчанию SS не поддерживает JWT. Сейчас будет чистый REST-сервис без фронтенда. Подразумевается, что фронтенд написан отдельно: например, на каком-нибудь JavaScript-фрейворке.
Для отправки запросов мы будем использовать программу POSTMAN. Например, для “входа” с именем/паролем и получения JWT-токена. А также для запроса защищенных страниц.
В этом примере наш старый REST-контроллер останется, а настройка Spring Security не особо поменяется – скорее, она дополнится.
- Мы добавим в приложение конечную точку /authenticate для аутентификации. Сюда приходят имя и пароль от пользователя. Приложение проверяет пароль, и если он верный, высылает пользователю в ответ JWT-токен.
- Во всех дальнейших запросах пользователь обязан высылать в заголовке JWT-токен, наше приложение проверяет подлинность токена в специально написанном фильтре JWTFilter и, если он корректен, пропускает запрос дальше.
Подготовка
REST-контроллер
Итак, наш основной REST-контроллер остается прежним:
@RestController
public class HelloController {
@GetMapping("/")
public String hello() {
return "Hello";
}
// сюда доступ разрешен только user и admin
@GetMapping("/user")
public String user() {
return "User";
}
// сюда доступ разрешен только admin
@GetMapping("/admin")
public String admin() {
return "Admin";
}
}
Он нужен для того, чтобы запретить к нему доступ и потом разрешить только авторизованным пользователям.
Аутентификация с пользовательским UserDetailsService
Настройка аутентификации такая:
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private CustomUserDetailsService userDetailsService;
@Bean
public PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
//...
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
}
}
Подробнее об аутентификации с UserDetailsService есть статья.
Если кратко, мы переопределяем метод loadUserByUsername(), чтобы Spring Security понимал, как взять пользователя по его имени из хранилища. Имея этот метод, SS может сравнить переданный пароль с настоящим и аутентифицировать пользователя (либо не аутентифицировать).
CustomUserDetailsService:
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private MyUserRepository dao;
@Override
public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
MyUser myUser= dao.findByLogin(userName);
if (myUser == null) {
throw new UsernameNotFoundException("Unknown user: "+userName);
}
UserDetails user = User.builder()
.username(myUser.getLogin())
.password(myUser.getPassword())
.roles(myUser.getRole())
.build();
return user;
}
}
Пользователи хранятся в In-Memory базе данных H2. Работаем через Hibernate.
Модель пользователя:
@Entity
public class MyUser {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
long id;
private String login;
private String password;
private String position;
private String role;
// геттеры/сеттеры
}
И репозиторий:
@Repository
public interface MyUserRepository extends JpaRepository<MyUser, Long> {
MyUser findByLogin(String login);
}
А данными заполняем базу на старте приложения с помощью data.sql (этот файл надо положить в папку /resources, а в application.yml включить его запуск):
insert into my_user(login, position, password, role) values('user', '1', 'password', 'USER');
insert into my_user( login, position, password, role) values('admin', '2', 'password', 'ADMIN');
А теперь перейдем собственно в JWT-токену.
Библиотека для работы с JWT-токеном
Для работы с JWT добавим Maven-зависимость:
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
Для работы с JWT-токеном уже написаны утилиты, так что используем готовую. В ней есть метод формирования токена, валидации токена, методы извлечения имени пользователя и других данных.
Во всех методах извлечения данных JWT-токен заодно проверяется на предмет не истек ли он, и валидна ли подпись.
Эти методы утилиты нам пригодятся:
@Service
public class JWTUtil {
@Value("${jwt.secret}")
private String SECRET_KEY;
@Value("${jwt.sessionTime}")
private long sessionTime;
// генерация токена (кладем в него имя пользователя)
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
return createToken(claims, userDetails.getUsername());
}
//извлечение имени пользователя из токена
public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}
// валидация токена
public Boolean validateToken(String token, UserDetails userDetails) {
final String username = extractUsername(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
// другие методы
}
Это была подготовка. Перейдем, наконец, к написанию своего кода, касающегося JWT.
Vulnerabilities
JSON web tokens may contain session state. But if project requirements allow session invalidation before JWT expiration, services can no longer trust token assertions by the token alone. To validate the session stored in the token is not revoked, token assertions must be checked against a data store. This renders the tokens no longer stateless, undermining the primary advantage of JWTs.
Security consultant Tim McLean reported vulnerabilities in some JWT libraries that used the field to incorrectly validate tokens. While these vulnerabilities were patched, McLean suggested deprecating the field altogether to prevent similar implementation confusion.
With proper design, developers can address algorithm vulnerabilities by taking precautions:
- Never let the JWT header alone drive verification
- Know the algorithms
- Use an appropriate key size
Software security architect Kurt Rodarmer points out additional JWT design vulnerabilities around cryptographic signing keys and a significant vulnerability that exposes a library’s JSON parser to open attack. This is a direct result of choosing JSON to express the token header, and is more difficult to mitigate.
Standard fields
The internet drafts define the following standard fields («claims») that can be used inside a JWT claim set:
| code | name | description |
|---|---|---|
| Issuer | Identifies principal that issued the JWT. | |
| Subject | Identifies the subject of the JWT. | |
| Audience | Identifies the recipients that the JWT is intended for. Each principal intended to process the JWT must identify itself with a value in the audience claim. If the principal processing the claim does not identify itself with a value in the claim when this claim is present, then the JWT must be rejected. | |
| Expiration Time | Identifies the expiration time on and after which the JWT must not be accepted for processing. The value must be a NumericDate: either an integer or decimal, representing seconds past 1970-01-01 00:00:00Z. | |
| Not Before | Identifies the time on which the JWT will start to be accepted for processing. The value must be a NumericDate. | |
| Issued at | Identifies the time at which the JWT was issued. The value must be a NumericDate. | |
| JWT ID | Case sensitive unique identifier of the token even among different issuers. |
The following fields are commonly used in the header of a JWT:
| code | name | description |
|---|---|---|
| Token type | If present, it is recommended to set this to . | |
| Content type | If nested signing or encryption is employed, it is recommended to set this to ; otherwise, omit this field. | |
| Message authentication code algorithm | The issuer can freely set an algorithm to verify the signature on the token. However, some supported algorithms are insecure. | |
| Key ID | A hint indicating which key the client used to generate the token signature. The server will match this value to a key on file in order to verify that the signature is valid and the token is authentic. | |
| x.509 Certificate Chain | A certificate chain in RFC4945 format corresponding to the private key used to generate the token signature. The server will use this information to verify that the signature is valid and the token is authentic. | |
| x.509 Certificate Chain URL | A URL where the server can retrieve a certificate chain corresponding to the private key used to generate the token signature. The server will retrieve and use this information to verify that the signature is authentic. | |
| Critical | A list of headers that must be understood by the server in order to accept the token as valid |