Files
marketing-parser/a.md
2025-11-29 17:19:44 +05:00

9.5 KiB
Raw Permalink Blame History

Извлечение информации о пользователе из 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 добавлена зависимость:

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

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

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

Создание JwtService

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

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:

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

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 для пользователя

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

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

@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));
    }
}

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

@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: Неподдерживаемый тип токена

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

@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));
    }
}