9.5 KiB
9.5 KiB
Извлечение информации о пользователе из 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 для бизнес-логики
// ...
}
}
Важные замечания
-
Валидация токена: Метод
parseAndValidateавтоматически проверяет:- Подпись токена
- Срок действия (expiration)
- Формат токена
-
Безопасность: Никогда не логируйте полный JWT токен или секретный ключ.
-
Секретный ключ: Должен совпадать с ключом в сервисе, выдающем токены.
-
Обработка исключений:
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));
}
}