310 lines
9.5 KiB
Markdown
310 lines
9.5 KiB
Markdown
# Извлечение информации о пользователе из 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));
|
||
}
|
||
}
|
||
```
|