Car Dealership — безопасность и отказоустойчивость
Архитектурная схема (archify) →
Два инженерных решения на реальном коде: кто может смотреть чужой заказ, и что происходит, когда storage-service не отвечает.
Ролевой доступ: HTTP-уровень плюс точечная проверка
SecurityConfig — stateless JWT resource server, свой JSON вместо стандартной страницы ошибки на 401/403:
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt.jwtAuthenticationConverter(keycloakJwtConverter))
.authenticationEntryPoint((request, response, ex) -> {
writeError(response, HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized", ex.getMessage());
})
)
Этого достаточно для ролей вроде MANAGER/ADMIN — они проверяются декларативно на уровне HTTP. Но «клиент видит только свои заказы» ролью не выразить — это не про роль, а про конкретную запись. Для этого — OrderSecurityService, отдельный бин с методом isOwner:
@Component("orderSecurity")
public class OrderSecurityService {
public boolean isOwner(UUID orderId, Authentication authentication) {
Jwt jwt = (Jwt) authentication.getPrincipal();
String email = jwt.getClaim("email");
if (email == null) return false;
return orderRepository.findById(orderId)
.map(order -> email.equals(order.getClient().getEmail()))
.orElse(false);
}
}
Достаёт email прямо из JWT-claim (не из БД пользователей — токен уже содержит всё нужное), сравнивает с email клиента заказа. Подключается через @PreAuthorize("@orderSecurity.isOwner(#orderId, authentication)") на нужном методе — то есть проверка живёт рядом с бизнес-логикой, а не размазана по контроллеру.
gRPC: дедлайн в конфиге, деградация в коде
Дедлайн — не хардкод в клиенте, а настройка application.yml:
grpc:
client:
storage-service:
address: static://localhost:9091
deadline: 5000
Сам клиент просто вызывает stub и ловит StatusRuntimeException:
public List<CarResponse> getAvailableCars() {
try {
GetAvailableCarsResponse response = stub.getAvailableCars(...);
return response.getCarsList().stream().map(CarProtoMapper::toResponse).toList();
} catch (StatusRuntimeException e) {
throw CarProtoMapper.mapError(e);
}
}
Вся логика деградации — в одном месте, CarProtoMapper.mapError:
static RuntimeException mapError(StatusRuntimeException e) {
return switch (e.getStatus().getCode()) {
case NOT_FOUND -> new EntityNotFoundException(e.getStatus().getDescription());
case UNAVAILABLE, DEADLINE_EXCEEDED -> new ServiceUnavailableException("StorageService is unavailable");
default -> new RuntimeException("gRPC error: " + e.getStatus());
};
}
Если storage-service не отвечает 5 секунд (настроенный дедлайн) или физически недоступен — клиент REST-API order-service получает не зависшую ручку и не голый gRPC-статус, а понятный доменный ServiceUnavailableException, который дальше маппится в HTTP 503. Один switch на весь код деградации — не разбросанные по вызовам try/catch с одинаковой логикой в каждом месте.
Если спросят на собесе "а что если storage упадёт совсем": ответ ровно этот код — клиент order-service получит 503 максимум через 5 секунд, не зависнет, и это единая точка обработки, а не догадка на каждом конкретном вызове.