Files
2025-11-29 17:19:44 +05:00

310 lines
9.5 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Извлечение информации о пользователе из 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));
}
}
```