Refactor code structure and optimize performance across multiple modules
deploy / deploy (push) Has been cancelled
deploy / deploy (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,309 @@
|
||||
# Извлечение информации о пользователе из 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));
|
||||
}
|
||||
}
|
||||
```
|
||||
Reference in New Issue
Block a user