The Mercedes driver's 55th career win was hard-fought and tense, with Ferrari's Vettel seizing the lead from Hamilton at the start and in the hunt all the way as the tyre strategies played out.
Hamilton took the chequered flag 3.4 seconds clear of the German, who did all he could to beat him with a thrilling move to pass the Briton's team mate Valtteri Bottas that will be replayed for some time to come.
"That's how racing should be and I loved it," said a happy Hamilton, who raced side by side with Vettel at turn one after the German's final pitstop.
"This is what the sport needs to be every single race...to have that close battle with him, a four times champion, is awesome," added the triple champion.
Vettel now has 104 points to Hamilton's 98.
The Briton, who made the move stick seven laps later and then made his tyres last to the finish, had sounded strangely breathless during the race when fretting about a strategy that turned out to be spot on.
"Lewis saved the race with an incredible drive. The strategy was great, but it was the driver who won it," said Mercedes F1's non-executive chairman Niki Lauda.
The win was Hamilton's second in Barcelona and ended a run of 10 different winners in 10 years.
"I was eight seconds ahead and then...I don't know how we managed to lose that," said Vettel, who made the most of Hamilton's poor start.
On an afternoon that saw the two multiple world champions in a battle of their own with the rest so far behind they might as well have been in another race, Daniel Ricciardo finished 75.8 seconds behind the winner.
Ricciardo was the only driver not lapped by the top two in a race after inheriting third, and his first podium of the year, from Bottas after the Finn's engine expired.
On a day of Finns failing to finish, Ferrari's Kimi Raikkonen went out at the start after a collision with Red Bull's 2016 winner Max Verstappen.
Bottas, who celebrated his first career victory in Russia two weeks ago, made glancing contact with his compatriot and the impact pitched Raikkonen into Verstappen on the other side, smashing the suspension on both cars.
The 19-year-old Dutch driver, with a legion of fans around the Circuit de Catalunya, shook his fist as the field disappeared in front of him and Raikkonen stopped.
Stewards investigated the incident and decided to take no further action.
Bottas retired 38 laps later, having started with an engine that had already done four races and was hurriedly shoe-horned into the car overnight on Friday after a water leak was discovered.
Spain's double world champion Fernando Alonso, starting seventh for McLaren after a troubled practice, was forced into the gravel in a tussle with Williams's Felipe Massa and dropped to 13th. He finished 12th.
The virtual safety car was deployed when Alonso's team mate Stoffel Vandoorne broke his car's suspension in a collision with Massa on lap 34.
Mexican Sergio Perez finished fourth, his 15th successive points finish, for Force India with French rookie team mate Esteban Ocon fifth.
Germany's Nico Hulkenberg was sixth for Renault, with compatriot Pascal Wehrlein eighth for Sauber after a five second time penalty was applied -- the Swiss team's first points of the season -- and Carlos Sainz seventh for Toro Rosso.
Russian Daniil Kvyat was eighth for Toro Rosso and French driver Romain Grosjean took the final point for Haas.
Enter a name and e-mail address and we will e-mail this website link too them.