# Извлечение информации о пользователе из JWT токена

## Обзор

Данная документация описывает, как извлечь информацию о пользователе из JWT токена в микросервисе на Spring Boot.

## Структура JWT токена

JWT токен содержит следующую информацию:

- **Subject (sub)**: Email пользователя
- **Custom Claims**:
  - `uid`: ID пользователя (Long)
  - `roles`: Роли пользователя (String, разделённые запятыми, например: "ROLE_USER,ROLE_ADMIN")
- **Стандартные поля**: `iat` (issued at), `exp` (expiration)

## Зависимости

Убедитесь, что в `pom.xml` добавлена зависимость:

```xml
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.12.3</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.12.3</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.12.3</version>
    <scope>runtime</scope>
</dependency>
```

## Конфигурация

В `application.properties` или `application.yml`:

```properties
security.jwt.secret-base64=<base64-encoded-secret-key>
security.jwt.access-ttl-seconds=3600
```

**Важно**: Используйте тот же `secret-base64`, что и в сервисе, выдающем токены.

## Создание JwtService

```java
package com.example.service;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.security.Key;

@Service
public class JwtService {

    private final Key signingKey;

    public JwtService(
            @Value("${security.jwt.secret-base64}") String base64Secret) {
        this.signingKey = Keys.hmacShaKeyFor(Decoders.BASE64.decode(base64Secret));
    }

    public Claims parseAndValidate(String token) {
        return Jwts.parserBuilder()
                .setSigningKey(signingKey)
                .build()
                .parseClaimsJws(token)
                .getBody();
    }
}
```

## Извлечение информации о пользователе

### Вариант 1: Из заголовка Authorization

```java
import io.jsonwebtoken.Claims;
import org.springframework.http.HttpHeaders;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api")
public class UserController {

    private final JwtService jwtService;

    public UserController(JwtService jwtService) {
        this.jwtService = jwtService;
    }

    @GetMapping("/user-info")
    public ResponseEntity<UserInfo> getUserInfo(
            @RequestHeader(HttpHeaders.AUTHORIZATION) String authHeader) {

        // Извлекаем токен из заголовка "Bearer <token>"
        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
            return ResponseEntity.status(401).build();
        }

        String token = authHeader.substring(7);

        try {
            Claims claims = jwtService.parseAndValidate(token);

            // Извлекаем информацию
            String email = claims.getSubject();
            Long userId = claims.get("uid", Long.class);
            String rolesString = claims.get("roles", String.class);

            // Парсим роли
            List<String> roles = rolesString == null || rolesString.isBlank()
                    ? List.of()
                    : Arrays.stream(rolesString.split(","))
                            .map(String::trim)
                            .filter(s -> !s.isEmpty())
                            .collect(Collectors.toList());

            UserInfo userInfo = new UserInfo(userId, email, roles);
            return ResponseEntity.ok(userInfo);

        } catch (Exception e) {
            // Токен невалиден или истёк
            return ResponseEntity.status(401).build();
        }
    }
}
```

### Вариант 2: Использование Spring Security (рекомендуется)

Если в вашем микросервисе настроен Spring Security с JWT фильтром, используйте `Principal`:

```java
import java.security.Principal;
import org.springframework.security.access.prepost.PreAuthorize;

@RestController
@RequestMapping("/api")
public class UserController {

    private final JwtService jwtService;

    public UserController(JwtService jwtService) {
        this.jwtService = jwtService;
    }

    @GetMapping("/me")
    @PreAuthorize("isAuthenticated()")
    public ResponseEntity<UserInfo> getCurrentUser(Principal principal) {
        // Principal.getName() возвращает subject (email) из JWT
        String email = principal.getName();

        // Если нужны дополнительные данные (uid, roles),
        // можно извлечь их из SecurityContext или извлечь токен из запроса
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();

        // Или получить токен из запроса и распарсить
        // (см. Вариант 1 для полного извлечения всех claims)

        return ResponseEntity.ok(new UserInfo(null, email, List.of()));
    }
}
```

### Вариант 3: Полное извлечение через HttpServletRequest

```java
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api")
public class UserController {

    private final JwtService jwtService;

    @GetMapping("/profile")
    public ResponseEntity<UserInfo> getProfile(HttpServletRequest request) {
        String authHeader = request.getHeader(HttpHeaders.AUTHORIZATION);

        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
            return ResponseEntity.status(401).build();
        }

        String token = authHeader.substring(7);
        Claims claims = jwtService.parseAndValidate(token);

        String email = claims.getSubject();
        Long userId = claims.get("uid", Long.class);
        String rolesString = claims.get("roles", String.class);
        List<String> roles = parseRoles(rolesString);

        return ResponseEntity.ok(new UserInfo(userId, email, roles));
    }

    private List<String> parseRoles(String rolesString) {
        if (rolesString == null || rolesString.isBlank()) {
            return List.of();
        }
        return Arrays.stream(rolesString.split(","))
                .map(String::trim)
                .filter(s -> !s.isEmpty())
                .collect(Collectors.toList());
    }
}
```

## DTO для пользователя

```java
public record UserInfo(
    Long userId,
    String email,
    List<String> roles
) {}
```

## Обработка ошибок

```java
@ControllerAdvice
public class JwtExceptionHandler {

    @ExceptionHandler(JwtException.class)
    public ResponseEntity<ErrorResponse> handleJwtException(JwtException e) {
        return ResponseEntity.status(401)
                .body(new ErrorResponse("Invalid or expired token", 401));
    }
}
```

## Пример использования в сервисном слое

```java
@Service
public class BusinessService {

    private final JwtService jwtService;

    public BusinessService(JwtService jwtService) {
        this.jwtService = jwtService;
    }

    public void processRequest(String token) {
        Claims claims = jwtService.parseAndValidate(token);
        Long userId = claims.get("uid", Long.class);
        String email = claims.getSubject();

        // Используйте userId и email для бизнес-логики
        // ...
    }
}
```

## Важные замечания

1. **Валидация токена**: Метод `parseAndValidate` автоматически проверяет:

   - Подпись токена
   - Срок действия (expiration)
   - Формат токена

2. **Безопасность**: Никогда не логируйте полный JWT токен или секретный ключ.

3. **Секретный ключ**: Должен совпадать с ключом в сервисе, выдающем токены.

4. **Обработка исключений**: `JwtException` и его подклассы (`ExpiredJwtException`, `MalformedJwtException`, и т.д.) должны обрабатываться корректно.

## Примеры исключений

- `ExpiredJwtException`: Токен истёк
- `MalformedJwtException`: Неверный формат токена
- `SignatureException`: Неверная подпись
- `UnsupportedJwtException`: Неподдерживаемый тип токена

## Тестирование

```java
@SpringBootTest
class JwtServiceTest {

    @Autowired
    private JwtService jwtService;

    @Test
    void testParseToken() {
        String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...";
        Claims claims = jwtService.parseAndValidate(token);

        assertEquals("user@example.com", claims.getSubject());
        assertEquals(123L, claims.get("uid", Long.class));
        assertEquals("ROLE_USER", claims.get("roles", String.class));
    }
}
```
